feat(ct): кнопка «На доработку» у согласующего; после доработки маршрут идёт с начала
Вернуть СТ инициатору на исправление было нельзя — только «Отклонить». Теперь у согласующего (шаг 4 и шаг 3 по ссылке из Teams) есть «На доработку» с обязательным комментарием. Строка и СТ получают статус «На доработке», СТ сразу открыта инициатору для правки, на шаге 4 — плашка, кто вернул и что исправить. Повторная отправка переписывает маршрут целиком — согласование с начала. «Отклонить» с перезапуском работает как раньше. Решение rework добавлено в общий ApproverService/DecisionPanel, но кнопка включена только в СТ ОМТС (allowRework); в ОИД/КПС статус недостижим. В SP в Choice status «Сравнительная таблица» и «Согласование СТ» добавлено «На доработке» (и «Отклонено» в статус СТ, которое форма уже писала). Уведомление инициатору при доработке — в потоке Power Automate (вне репо).
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
"internalName": "status",
|
||||
"title": "Статус",
|
||||
"type": "Choice",
|
||||
"choices": ["Черновик", "Согласовано", "На согласовании", "Отклонено"]
|
||||
"choices": ["Черновик", "Согласовано", "На согласовании", "Отклонено", "На доработке"]
|
||||
},
|
||||
{
|
||||
"internalName": "winSupplier",
|
||||
@@ -292,7 +292,8 @@
|
||||
"Рекомендую",
|
||||
"Черновик",
|
||||
"Отклонено",
|
||||
"Ждёт очереди"
|
||||
"Ждёт очереди",
|
||||
"На доработке"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,14 +4,17 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
'Черновик': 'Сформирована',
|
||||
'На согласовании': 'На согласовании',
|
||||
'Согласовано': 'Согласована',
|
||||
'Отклонено': 'Отклонена'
|
||||
'Отклонено': 'Отклонена',
|
||||
'На доработке': 'На доработке'
|
||||
};
|
||||
|
||||
const STATUS_VARIANT: Record<string, DashboardRow['statusVariant']> = {
|
||||
'Черновик': 'neutral',
|
||||
'На согласовании': 'info',
|
||||
'Согласовано': 'success',
|
||||
'Отклонено': 'error'
|
||||
'Отклонено': 'error',
|
||||
// В реестре нет жёлтого варианта — как «На согласовании»: процесс идёт, ждём действий инициатора.
|
||||
'На доработке': 'info'
|
||||
};
|
||||
|
||||
export function createCtDocumentConfig(): DocumentTypeConfig {
|
||||
|
||||
@@ -21,9 +21,10 @@ const STATUS_LABELS: Record<CTStatus, string> = {
|
||||
on_approval: 'На согласовании',
|
||||
approved: 'Согласована',
|
||||
rejected: 'Отклонена',
|
||||
rework: 'На доработке',
|
||||
};
|
||||
|
||||
const STATUS_VARIANT: Record<CTStatus, 'neutral' | 'info' | 'success' | 'danger'> = {
|
||||
const STATUS_VARIANT: Record<CTStatus, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
||||
draft: 'neutral',
|
||||
kp_loading: 'neutral',
|
||||
kp_loaded: 'neutral',
|
||||
@@ -31,6 +32,7 @@ const STATUS_VARIANT: Record<CTStatus, 'neutral' | 'info' | 'success' | 'danger'
|
||||
on_approval: 'info',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
rework: 'warning',
|
||||
};
|
||||
|
||||
// Адаптер над Stepper/FormHeader из @tnsu/ui-kit (по образцу KPS Breadcrumbs): контракт props формы
|
||||
|
||||
+2
@@ -17,6 +17,7 @@ const RU_STATUS_TO_CT_STATUS: Record<string, CTStatus> = {
|
||||
Отклонено: 'rejected',
|
||||
Согласовано: 'approved',
|
||||
'На согласовании': 'on_approval',
|
||||
'На доработке': 'rework',
|
||||
};
|
||||
|
||||
// Инлайн-цепочка согласования Шага 3 — для согласующего, открывшего СТ по Teams-ссылке (?ctid=),
|
||||
@@ -56,6 +57,7 @@ export const InlineApprovalChain: React.FC<Props> = ({ service, formData, onSave
|
||||
onDecided={handleDecided}
|
||||
search={(q) => service.searchUsers(q)}
|
||||
onDelegated={() => setDecisionSeq((n) => n + 1)}
|
||||
allowRework
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Alert, Spinner, Dialog, Icon } from "@tnsu/ui-kit";
|
||||
import { CTStatus, FormStep } from "../types/domain";
|
||||
import { CtService } from "../../services/CtService";
|
||||
import { CTFormData } from "../hooks/useCtForm";
|
||||
import { useIncore } from "./useIncore";
|
||||
import { findReworkRow } from "../utils/approvalAccess";
|
||||
import {
|
||||
ApprovalMatrix,
|
||||
DecisionPanel,
|
||||
@@ -30,6 +31,7 @@ const RU_STATUS_TO_CT_STATUS: Record<string, CTStatus> = {
|
||||
Отклонено: "rejected",
|
||||
Согласовано: "approved",
|
||||
"На согласовании": "on_approval",
|
||||
"На доработке": "rework",
|
||||
};
|
||||
|
||||
const INCORE_STEP_LABELS: Record<string, string> = {
|
||||
@@ -57,6 +59,20 @@ export const Step4Approval: React.FC<Props> = ({ service, formData, onPrev, onSa
|
||||
|
||||
const incore = useIncore({ service, formData, isInitiator });
|
||||
|
||||
// Возврат на доработку: кто вернул и что исправить — из записанного маршрута. Сам шаг сейчас показывает
|
||||
// новый планируемый маршрут для повторной отправки (идёт с начала), старых строк в нём не видно.
|
||||
const [reworkRow, setReworkRow] = useState<IMatrixRow | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (status !== "rework" || !ctId) {
|
||||
setReworkRow(undefined);
|
||||
return;
|
||||
}
|
||||
service
|
||||
.getApprovalMatrixForRequest(ctId)
|
||||
.then((rows) => setReworkRow(findReworkRow(rows)))
|
||||
.catch((err) => console.warn("[Шаг 4] не удалось загрузить комментарий возврата на доработку:", err));
|
||||
}, [service, ctId, status]);
|
||||
|
||||
const loadMatrix = useMemo(
|
||||
() => (): Promise<IMatrixRow[]> =>
|
||||
isSubmitted && ctId
|
||||
@@ -134,6 +150,13 @@ export const Step4Approval: React.FC<Props> = ({ service, formData, onPrev, onSa
|
||||
{status === "approved" && <Alert variant="ok">СТ согласована всеми участниками.</Alert>}
|
||||
{status === "rejected" && <Alert variant="err">СТ отклонена. Требуется корректировка и повторная отправка.</Alert>}
|
||||
{status === "on_approval" && <Alert variant="info">СТ находится на согласовании.</Alert>}
|
||||
{status === "rework" && (
|
||||
<Alert variant="warn">
|
||||
СТ возвращена на доработку{reworkRow?.who ? ` (${reworkRow.who})` : ""}.
|
||||
{reworkRow?.comment ? ` Что исправить: ${reworkRow.comment}.` : ""} Внесите правки и отправьте на согласование
|
||||
заново — маршрут пойдёт с начала.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ApprovalMatrix editor={editor} search={(q) => service.searchUsers(q)} />
|
||||
|
||||
@@ -201,6 +224,7 @@ export const Step4Approval: React.FC<Props> = ({ service, formData, onPrev, onSa
|
||||
onDecided={handleDecided}
|
||||
search={(q) => service.searchUsers(q)}
|
||||
onDelegated={() => setDecisionSeq((n) => n + 1)}
|
||||
allowRework
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ export type CTStatus =
|
||||
| "formed"
|
||||
| "on_approval"
|
||||
| "approved"
|
||||
| "rejected";
|
||||
| "rejected"
|
||||
| "rework"; // возвращена согласующим на доработку — инициатор правит и отправляет заново
|
||||
|
||||
export interface Supplier {
|
||||
id: string; // ключ колонки Шага 3: `${spId}_${kpFileSpId}` (см. utils/supplierColumns), у старых данных — String(spId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isUserInApprovalChain } from './approvalAccess';
|
||||
import { findReworkRow, isUserInApprovalChain } from './approvalAccess';
|
||||
import { IMatrixRow } from '../../../../shared/approval';
|
||||
|
||||
function makeRow(overrides: Partial<IMatrixRow>): IMatrixRow {
|
||||
@@ -33,3 +33,18 @@ describe('isUserInApprovalChain (кому видны цены и КП при с
|
||||
expect(isUserInApprovalChain([], { email: 'ivanov@tnsu.kz', displayName: 'Иванов Иван' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findReworkRow (кто вернул СТ на доработку и что исправить)', () => {
|
||||
it('находит строку со статусом «На доработке»', () => {
|
||||
const matrix = [
|
||||
makeRow({ order: 1, status: 'Согласовано' }),
|
||||
makeRow({ order: 2, who: 'Петров Пётр', status: 'На доработке', comment: 'Уточните цены поставщика' }),
|
||||
];
|
||||
|
||||
expect(findReworkRow(matrix)).toMatchObject({ who: 'Петров Пётр', comment: 'Уточните цены поставщика' });
|
||||
});
|
||||
|
||||
it('нет возврата — undefined', () => {
|
||||
expect(findReworkRow([makeRow({ status: 'Согласовано' })])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IMatrixRow } from '../../../../shared/approval';
|
||||
import { REQUEST_STATUS } from '../../../../shared/approval/models/statuses';
|
||||
|
||||
export interface CurrentUser {
|
||||
email: string;
|
||||
@@ -16,3 +17,8 @@ export function isUserInApprovalChain(matrix: IMatrixRow[], user: CurrentUser):
|
||||
return row.who === user.displayName;
|
||||
});
|
||||
}
|
||||
|
||||
// Строка маршрута, которой СТ вернули на доработку: инициатору показываем, кто вернул и что исправить.
|
||||
export function findReworkRow(matrix: IMatrixRow[]): IMatrixRow | undefined {
|
||||
return matrix.find(row => row.status === REQUEST_STATUS.rework);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../components/types/domain";
|
||||
import {
|
||||
ApprovalListService,
|
||||
ApprovalStatusKey,
|
||||
ApproverService,
|
||||
IApprovalListFields,
|
||||
IApproverToWrite,
|
||||
@@ -89,10 +90,11 @@ const APPROVAL_FIELDS: IApprovalListFields = {
|
||||
};
|
||||
|
||||
// Статус заявки (ApproverService.decide) → Choice «Статус» шапки СТ.
|
||||
const APPROVAL_STATUS_TO_CT_STATUS: Record<"onApproval" | "approved" | "rejected", CTStatus> = {
|
||||
const APPROVAL_STATUS_TO_CT_STATUS: Record<ApprovalStatusKey, CTStatus> = {
|
||||
onApproval: "on_approval",
|
||||
approved: "approved",
|
||||
rejected: "rejected",
|
||||
rework: "rework",
|
||||
};
|
||||
|
||||
// Обратный маппинг для чтения статуса шапки СТ (loadCTById) — раньше status не читался вовсе,
|
||||
@@ -103,6 +105,7 @@ const CHOICE_TO_CT_STATUS: Record<string, CTStatus> = {
|
||||
"На согласовании": "on_approval",
|
||||
Согласовано: "approved",
|
||||
Отклонено: "rejected",
|
||||
"На доработке": "rework",
|
||||
};
|
||||
|
||||
// Контактные поля поставщика (из «База данных поставщиков») для saveSupplierContacts.
|
||||
@@ -299,6 +302,7 @@ export class CtService {
|
||||
approved: "Согласовано",
|
||||
formed: "Черновик",
|
||||
rejected: "Отклонено",
|
||||
rework: "На доработке",
|
||||
};
|
||||
const spStatus = statusMap[status];
|
||||
if (!spStatus) return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions, AadHttpClient } from '@microsoft/sp-http';
|
||||
import {
|
||||
ApprovalStatusKey,
|
||||
ApprovalListService,
|
||||
ApproverService,
|
||||
REQUEST_STATUS,
|
||||
@@ -164,10 +165,12 @@ const CRITERIA_FIELDS = {
|
||||
const KPS_REQUEST_TYPE_PAGE_URL = 'kps';
|
||||
|
||||
// Статус заявки (ApproverService.decide) → Choice «Статус» шапки СТ.
|
||||
const APPROVAL_STATUS_TO_CT_STATUS: Record<'onApproval' | 'approved' | 'rejected', CtStatus> = {
|
||||
const APPROVAL_STATUS_TO_CT_STATUS: Record<ApprovalStatusKey, CtStatus> = {
|
||||
onApproval: 'on_approval',
|
||||
approved: 'approved',
|
||||
rejected: 'rejected'
|
||||
rejected: 'rejected',
|
||||
// Кнопки «На доработку» в КПС нет (пока только СТ ОМТС) — статус недостижим; на всякий случай как отклонение.
|
||||
rework: 'rejected'
|
||||
};
|
||||
|
||||
// Округление денег до 2 знаков (срезаем хвост float перед записью в SP).
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from "../components/types/domain";
|
||||
import {
|
||||
ApprovalListService,
|
||||
ApprovalStatusKey,
|
||||
ApproverService,
|
||||
IApprovalListFields,
|
||||
IApproverToWrite,
|
||||
@@ -68,10 +69,12 @@ const APPROVAL_FIELDS: IApprovalListFields = {
|
||||
};
|
||||
|
||||
// Статус заявки (ApproverService.decide) → Choice «Статус» шапки СТ ОИД.
|
||||
const APPROVAL_STATUS_TO_OID_STATUS: Record<"onApproval" | "approved" | "rejected", OIDStatus> = {
|
||||
const APPROVAL_STATUS_TO_OID_STATUS: Record<ApprovalStatusKey, OIDStatus> = {
|
||||
onApproval: "on_approval",
|
||||
approved: "approved",
|
||||
rejected: "rejected",
|
||||
// Кнопки «На доработку» в ОИД нет (пока только СТ ОМТС) — статус недостижим; на всякий случай как отклонение.
|
||||
rework: "rejected",
|
||||
};
|
||||
|
||||
const EXT_API_RESOURCE = "9232d9e2-9807-4753-808f-07250e5474b0";
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApproverService } from './approverDecision.service';
|
||||
import { ApprovalListService } from './approvalList.service';
|
||||
import { IApprovalRow } from './models/IApprovalRow';
|
||||
import { REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
function makeRow(id: number, status: string): IApprovalRow {
|
||||
return { id, order: id, status } as unknown as IApprovalRow;
|
||||
}
|
||||
|
||||
// Поддельный список «Согласование …»: хранит строки в памяти.
|
||||
function makeApproval(rows: IApprovalRow[]): { approval: ApprovalListService; activated: number[] } {
|
||||
const activated: number[] = [];
|
||||
const fake = {
|
||||
setDecision: async (rowId: number, status: string) => {
|
||||
const row = rows.find(r => r.id === rowId);
|
||||
if (row) row.status = status;
|
||||
},
|
||||
activateNextQueued: async (requestId: number) => { activated.push(requestId); },
|
||||
getForRequest: async () => rows,
|
||||
};
|
||||
return { approval: fake as unknown as ApprovalListService, activated };
|
||||
}
|
||||
|
||||
describe('ApproverService.decide — «На доработку»', () => {
|
||||
// Раньше у согласующего были только «Согласовать» и «Отклонить»: вернуть СТ на исправление было
|
||||
// нельзя, приходилось отклонять.
|
||||
it('строка получает «На доработке», СТ — статус доработки, очередь дальше не идёт', async () => {
|
||||
const rows = [makeRow(1, REQUEST_STATUS.approved), makeRow(2, REQUEST_STATUS.onApproval), makeRow(3, REQUEST_STATUS.queued)];
|
||||
const { approval, activated } = makeApproval(rows);
|
||||
const computed: string[] = [];
|
||||
const service = new ApproverService(approval, async status => { computed.push(status); });
|
||||
|
||||
const label = await service.decide(360, 2, 'rework', 'Уточните цены поставщика');
|
||||
|
||||
expect(rows[1].status).toBe(REQUEST_STATUS.rework);
|
||||
expect(computed).toEqual(['rework']);
|
||||
expect(label).toBe(REQUEST_STATUS.rework);
|
||||
expect(activated).toEqual([]);
|
||||
});
|
||||
|
||||
it('«Согласовать» по-прежнему двигает очередь', async () => {
|
||||
const rows = [makeRow(1, REQUEST_STATUS.onApproval), makeRow(2, REQUEST_STATUS.queued)];
|
||||
const { approval, activated } = makeApproval(rows);
|
||||
const service = new ApproverService(approval, async () => undefined);
|
||||
|
||||
await service.decide(360, 1, 'approve', '');
|
||||
|
||||
expect(activated).toEqual([360]);
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,12 @@ import { REQUEST_STATUS } from './models/statuses';
|
||||
import { IApprovalRow } from './models/IApprovalRow';
|
||||
import { ISiteUser } from './models/ISiteUser';
|
||||
|
||||
// approve — согласовать; reject — отклонить (нет фазы исполнения и статуса
|
||||
// «возврат на доработку» — как в прежнем KPS-механизме).
|
||||
export type ApproverDecision = 'approve' | 'reject';
|
||||
// approve — согласовать; reject — отклонить; rework — вернуть инициатору на доработку
|
||||
// (после исправлений он отправляет заново, маршрут идёт с начала). Нет фазы исполнения.
|
||||
export type ApproverDecision = 'approve' | 'reject' | 'rework';
|
||||
|
||||
// Статус заявки, вычисленный из строк маршрута, — форма пишет его в своё поле статуса.
|
||||
export type ApprovalStatusKey = 'onApproval' | 'approved' | 'rejected' | 'rework';
|
||||
|
||||
// Активная строка текущего пользователя: либо он сам назначен на неё
|
||||
// (isDelegator=false), либо это его делегат недоступен и он, как делегировавший,
|
||||
@@ -16,7 +19,8 @@ export interface IPendingRow extends IApprovalRow {
|
||||
|
||||
const ROW_STATUS: Record<ApproverDecision, string> = {
|
||||
approve: REQUEST_STATUS.approved,
|
||||
reject: REQUEST_STATUS.rejected
|
||||
reject: REQUEST_STATUS.rejected,
|
||||
rework: REQUEST_STATUS.rework
|
||||
};
|
||||
|
||||
// Действия согласующего: определение его строки, применение решения,
|
||||
@@ -30,7 +34,7 @@ export class ApproverService {
|
||||
*/
|
||||
constructor(
|
||||
private readonly approval: ApprovalListService,
|
||||
private readonly onStatusComputed: (status: 'onApproval' | 'approved' | 'rejected') => Promise<void>
|
||||
private readonly onStatusComputed: (status: ApprovalStatusKey) => Promise<void>
|
||||
) {}
|
||||
|
||||
// Ожидающая строка текущего пользователя, если сейчас его очередь; иначе undefined.
|
||||
@@ -91,9 +95,12 @@ export class ApproverService {
|
||||
return status.label;
|
||||
}
|
||||
|
||||
// Статус заявки из строк маршрута: отклонение любой строки → «Отклонено»;
|
||||
// все согласовали → «Согласовано»; иначе ещё «На согласовании».
|
||||
private computeRequestStatus(rows: IApprovalRow[]): { key: 'onApproval' | 'approved' | 'rejected'; label: string } {
|
||||
// Статус заявки из строк маршрута: возврат на доработку любой строкой → «На доработке»;
|
||||
// отклонение → «Отклонено»; все согласовали → «Согласовано»; иначе ещё «На согласовании».
|
||||
private computeRequestStatus(rows: IApprovalRow[]): { key: ApprovalStatusKey; label: string } {
|
||||
if (rows.some((r) => r.status === REQUEST_STATUS.rework)) {
|
||||
return { key: 'rework', label: REQUEST_STATUS.rework };
|
||||
}
|
||||
if (rows.some((r) => r.status === REQUEST_STATUS.rejected)) {
|
||||
return { key: 'rejected', label: REQUEST_STATUS.rejected };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export { getRequestTypes, getRequestTypesCached, getRequestTypeIdByPageUrl, reso
|
||||
export { ApprovalListService, DEFAULT_APPROVAL_FIELDS } from './approvalList.service';
|
||||
export type { IApproverToWrite, IApprovalListFields } from './approvalList.service';
|
||||
export { ApproverService } from './approverDecision.service';
|
||||
export type { ApproverDecision, IPendingRow } from './approverDecision.service';
|
||||
export type { ApproverDecision, ApprovalStatusKey, IPendingRow } from './approverDecision.service';
|
||||
export { getPlannedMatrix, writeRoute } from './approvalSubmission.service';
|
||||
export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
|
||||
export type { IUseMatrixEditor } from './useMatrixEditor';
|
||||
|
||||
@@ -12,6 +12,8 @@ export const REQUEST_STATUS = {
|
||||
onApproval: 'На согласовании',
|
||||
approved: 'Согласовано',
|
||||
rejected: 'Отклонено',
|
||||
// Возвращено инициатору на исправление; после повторной отправки маршрут идёт с начала.
|
||||
rework: 'На доработке',
|
||||
} as const;
|
||||
|
||||
// Значения Choice-колонки статуса строки маршрута при провижининге.
|
||||
@@ -21,4 +23,5 @@ export const APPROVAL_ROW_STATUS_CHOICES: string[] = [
|
||||
REQUEST_STATUS.onApproval,
|
||||
REQUEST_STATUS.approved,
|
||||
REQUEST_STATUS.rejected,
|
||||
REQUEST_STATUS.rework,
|
||||
];
|
||||
|
||||
@@ -16,11 +16,13 @@ interface IDecisionPanelProps {
|
||||
search?: (query: string) => Promise<ISiteUser[]>;
|
||||
/** Вызывается после успешного переназначения (обновить экран/матрицу). */
|
||||
onDelegated?: () => void;
|
||||
/** Кнопка «На доработку» — вернуть инициатору на исправление. Пока включена только в СТ ОМТС. */
|
||||
allowRework?: boolean;
|
||||
}
|
||||
|
||||
// Панель решения текущего шага: согласующий (Согласовать/Отклонить). Если у шага
|
||||
// разрешено переназначение (allowReassign) — можно передать шаг другому сотруднику.
|
||||
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestId, onDecided, search, onDelegated }) => {
|
||||
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestId, onDecided, search, onDelegated, allowRework }) => {
|
||||
const [checking, setChecking] = useState<boolean>(true);
|
||||
const [pendingRow, setPendingRow] = useState<IPendingRow | undefined>(undefined);
|
||||
const [comment, setComment] = useState<string>('');
|
||||
@@ -61,6 +63,10 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
setError('Для отклонения укажите комментарий.');
|
||||
return;
|
||||
}
|
||||
if (decision === 'rework' && comment.trim().length === 0) {
|
||||
setError('Укажите в комментарии, что нужно доработать.');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
try {
|
||||
@@ -206,13 +212,24 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
<div className={styles.title}>Ваше решение</div>
|
||||
{error && <Alert variant="err">{error}</Alert>}
|
||||
<FormField label="Комментарий" htmlFor="decisionComment">
|
||||
<TextArea id="decisionComment" value={comment} onChange={setComment} placeholder="Комментарий (обязателен при отклонении)" />
|
||||
<TextArea
|
||||
id="decisionComment"
|
||||
value={comment}
|
||||
onChange={setComment}
|
||||
placeholder={allowRework ? 'Комментарий (обязателен при отклонении и возврате на доработку)' : 'Комментарий (обязателен при отклонении)'}
|
||||
/>
|
||||
</FormField>
|
||||
<div className={styles.actions}>
|
||||
<Button variant="accent" disabled={submitting} onClick={() => decide('approve')}>
|
||||
<Icon name="check" size="sm" />
|
||||
Согласовать
|
||||
</Button>
|
||||
{allowRework && (
|
||||
<Button variant="secondary" disabled={submitting} onClick={() => decide('rework')}>
|
||||
<Icon name="refresh" size="sm" />
|
||||
На доработку
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="danger" disabled={submitting} onClick={() => decide('reject')}>
|
||||
<Icon name="close" size="sm" />
|
||||
Отклонить
|
||||
|
||||
Reference in New Issue
Block a user