fix: заявление на отпуск не уходило в ЭДО, если в маршруте есть исполнитель
Отправка срабатывала только на статус «Согласовано», а при исполнителе в маршруте последний согласующий переводит заявку сразу в «Выполнение» — запрос в ЭДО не шёл вообще. Теперь отправка срабатывает на решение, завершившее фазу согласования (isApprovalPhaseCompleted: «На согласовании» → «Согласовано»/«Выполнение»/«Закрыта»); решение исполнителя повторно её не запускает. В зелёное сообщение «Решение сохранено» дописывается «Документы отправлены на подписание в ЭДО…» (onDecided панели решения может вернуть строку). Заодно пять чужих предупреждений линтера: eqeqeq в SubmitActions, явные типы возврата в approverOrgInfo и useSkudRoute, зависимость эффекта в ServiceRequestView, null → undefined в ячейках календаря СКУД. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
|||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
approvalListTitleForListId,
|
approvalListTitleForListId,
|
||||||
@@ -57,6 +58,9 @@ interface IServiceRequestViewProps {
|
|||||||
|
|
||||||
// Заявка «Заявка на услуги» только для просмотра (режим согласующего).
|
// Заявка «Заявка на услуги» только для просмотра (режим согласующего).
|
||||||
const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
|
// onCodeLoaded — колбэк родителя, его ссылка меняется на каждом рендере; в зависимостях эффекта
|
||||||
|
// загрузки идёт стабильная обёртка, иначе заявка перезапрашивалась бы при каждом рендере родителя.
|
||||||
|
const notifyCode = useStableCallback(onCodeLoaded);
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
@@ -95,7 +99,7 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
setStatus(loaded.status);
|
setStatus(loaded.status);
|
||||||
setCode(loaded.code);
|
setCode(loaded.code);
|
||||||
onCodeLoaded?.(loaded.code);
|
notifyCode(loaded.code);
|
||||||
setAttachments(files);
|
setAttachments(files);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -110,7 +114,7 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [itemId, listId]);
|
}, [itemId, listId, notifyCode]);
|
||||||
|
|
||||||
// Ссылки на скачивание для файлов строк «Приложения» (см. AnnexesSection).
|
// Ссылки на скачивание для файлов строк «Приложения» (см. AnnexesSection).
|
||||||
// Считаем ДО ранних return'ов ниже: хук, вызванный только на части рендеров,
|
// Считаем ДО ранних return'ов ниже: хук, вызванный только на части рендеров,
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ describe('cellKind / toMissedKind', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('месяц начинается с понедельника: 1 июля 2026 — среда, два пустых места', () => {
|
it('месяц начинается с понедельника: 1 июля 2026 — среда, два пустых места', () => {
|
||||||
expect(cells[0]).toBeNull();
|
expect(cells[0]).toBeUndefined();
|
||||||
expect(cells[1]).toBeNull();
|
expect(cells[1]).toBeUndefined();
|
||||||
expect(cells[2]?.day).toBe(1);
|
expect(cells[2]?.day).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,12 +130,12 @@ export function monthWorkedHours(byDay: Map<string, IDayInfo>): number {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ячейки месяца: ведущие null — пустые места до первого числа (неделя с понедельника). */
|
/** Ячейки месяца: ведущие undefined — пустые места до первого числа (неделя с понедельника). */
|
||||||
export function buildMonthCells(year: number, month: number, byDay: Map<string, IDayInfo>): (IDayInfo | null)[] {
|
export function buildMonthCells(year: number, month: number, byDay: Map<string, IDayInfo>): (IDayInfo | undefined)[] {
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
const firstColumn = (new Date(year, month, 1).getDay() + 6) % DAYS_IN_WEEK;
|
const firstColumn = (new Date(year, month, 1).getDay() + 6) % DAYS_IN_WEEK;
|
||||||
const cells: (IDayInfo | null)[] = [];
|
const cells: (IDayInfo | undefined)[] = [];
|
||||||
for (let i = 0; i < firstColumn; i += 1) cells.push(null);
|
for (let i = 0; i < firstColumn; i += 1) cells.push(undefined);
|
||||||
for (let d = 1; d <= daysInMonth; d += 1) {
|
for (let d = 1; d <= daysInMonth; d += 1) {
|
||||||
const key = toDateKey(year, month, d);
|
const key = toDateKey(year, month, d);
|
||||||
cells.push(byDay.get(key) ?? { day: d, dateKey: key, events: [], first: '', last: '' });
|
cells.push(byDay.get(key) ?? { day: d, dateKey: key, events: [], first: '', last: '' });
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface IRouteSource {
|
|||||||
reloadSeq?: number;
|
reloadSeq?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const byPhase = (execution: boolean) => (rows: IMatrixRow[]): IMatrixRow[] =>
|
const byPhase = (execution: boolean): ((rows: IMatrixRow[]) => IMatrixRow[]) => (rows) =>
|
||||||
rows.filter((r) => (r.phase === APPROVAL_PHASE.execution) === execution);
|
rows.filter((r) => (r.phase === APPROVAL_PHASE.execution) === execution);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
## Маршрут
|
## Маршрут
|
||||||
- Тип заявки в approval-route API (`/api/request-types`): id **11**, `pageUrl` «hr-vacation»,
|
- Тип заявки в approval-route API (`/api/request-types`): id **11**, `pageUrl` «hr-vacation»,
|
||||||
`listId` «/sites/requests/Lists/List5», section «Кадры».
|
`listId` «/sites/requests/Lists/List5», section «Кадры».
|
||||||
- Фазы: только согласование. Фазы исполнения нет — после согласования заявка
|
- Фазы: согласование, а при наличии исполнителей в маршруте (бывает: тип заявки может их
|
||||||
просто получает статус «Согласовано» (никакого Planner, в отличие от Ticket1C).
|
содержать) — ещё и исполнение. Без исполнителей после согласования заявка получает статус
|
||||||
|
«Согласовано», с исполнителем — сразу «Выполнение». Planner не используется (в отличие от Ticket1C).
|
||||||
- **Матрица редактируема заявителем** при новой заявке / черновике / возврате
|
- **Матрица редактируема заявителем** при новой заявке / черновике / возврате
|
||||||
(как RecruitingHire / WorkplaceSetup): `useMatrixEditor(editable = !readOnly)`.
|
(как RecruitingHire / WorkplaceSetup): `useMatrixEditor(editable = !readOnly)`.
|
||||||
Заявитель может добавлять/удалять/переставлять согласующих поверх планируемого
|
Заявитель может добавлять/удалять/переставлять согласующих поверх планируемого
|
||||||
@@ -29,8 +30,12 @@
|
|||||||
- **Заголовок элемента** (`Title`): `Отпуск — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Отпуск — {ФИО} — дд.мм.гггг`.
|
||||||
|
|
||||||
## Подписание в ЭДО
|
## Подписание в ЭДО
|
||||||
После того как все согласующие согласовали (статус заявки становится «Согласовано»), из браузера
|
После того как все согласующие согласовали, из браузера **последнего согласующего**
|
||||||
**последнего согласующего** (`VacationView` → `onDecided`) заявление уходит на подпись в TANSU ЭДО:
|
(`VacationView` → `onDecided`) заявление уходит на подпись в TANSU ЭДО. Срабатывает на решение, которое
|
||||||
|
завершило фазу согласования (`isApprovalPhaseCompleted`: «На согласовании» → «Согласовано» **или**
|
||||||
|
«Выполнение»), а не только на «Согласовано»: если в маршруте есть исполнитель, заявка сразу становится
|
||||||
|
«Выполнение». Решение исполнителя повторно отправку не запускает. В зелёном сообщении «Решение сохранено»
|
||||||
|
появляется строка «Документы отправлены на подписание в ЭДО…»:
|
||||||
- **Кто подписывает** — сотрудник, указанный в заявке (поле «Сотрудник»). Его почта передаётся и как
|
- **Кто подписывает** — сотрудник, указанный в заявке (поле «Сотрудник»). Его почта передаётся и как
|
||||||
получатель, и как `initiator_email`: ему уходит приглашение, и он же получает письмо, когда документ подписан.
|
получатель, и как `initiator_email`: ему уходит приглашение, и он же получает письмо, когда документ подписан.
|
||||||
- **Документ** — заявление, собранное в браузере как DOCX (`vacationDocument.ts`, JSZip); ЭДО сам
|
- **Документ** — заявление, собранное в браузере как DOCX (`vacationDocument.ts`, JSZip); ЭДО сам
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { REQUEST_STATUS } from '../../shared/approval/models/statuses';
|
import { isApprovalPhaseCompleted } from '../../shared/approval/models/statuses';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
@@ -145,20 +145,27 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
// поэтому неудача показывается отдельным сообщением и согласование не отменяет. Повторить можно
|
// поэтому неудача показывается отдельным сообщением и согласование не отменяет. Повторить можно
|
||||||
// кнопкой: запрос принимается целиком или отклоняется целиком, а повтор уже принятого ЭДО
|
// кнопкой: запрос принимается целиком или отклоняется целиком, а повтор уже принятого ЭДО
|
||||||
// отклонит как дубль — признака «уже отправлено» в заявке нет.
|
// отклонит как дубль — признака «уже отправлено» в заявке нет.
|
||||||
const sendToSigning = (): Promise<void> => {
|
// Возвращает фразу для зелёного сообщения панели решения, если документы ушли; при неудаче —
|
||||||
|
// ничего (ошибку и кнопку повтора показывает блок ниже).
|
||||||
|
const sendToSigning = (): Promise<string | undefined> => {
|
||||||
setSigningError('');
|
setSigningError('');
|
||||||
setSigningResult(undefined);
|
setSigningResult(undefined);
|
||||||
setSigningBusy(true);
|
setSigningBusy(true);
|
||||||
return sendVacationForSigning(context, { data, employeeName, itemId, code })
|
return sendVacationForSigning(context, { data, employeeName, itemId, code })
|
||||||
.then((result) => {
|
.then((result): string | undefined => {
|
||||||
setSigningResult(result);
|
setSigningResult(result);
|
||||||
|
return `Документы отправлены на подписание в ЭДО (${result.sent.join(', ')}): ссылка для подписания ушла на ${data.Employee}.`;
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e): string | undefined => {
|
||||||
setSigningError(
|
setSigningError(
|
||||||
`Заявка согласована, но отправить заявление на подпись не удалось: ${friendlyErrorMessage(e)}`
|
`Заявка согласована, но отправить заявление на подпись не удалось: ${friendlyErrorMessage(e)}`
|
||||||
);
|
);
|
||||||
|
return undefined;
|
||||||
})
|
})
|
||||||
.then(() => setSigningBusy(false));
|
.then((note) => {
|
||||||
|
setSigningBusy(false);
|
||||||
|
return note;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -198,11 +205,6 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
|
|
||||||
<div className="rp-no-print">
|
<div className="rp-no-print">
|
||||||
{signingBusy && <Alert variant="ok">Отправляем заявление на подпись в ЭДО…</Alert>}
|
{signingBusy && <Alert variant="ok">Отправляем заявление на подпись в ЭДО…</Alert>}
|
||||||
{!signingBusy && signingResult && (
|
|
||||||
<Alert variant="ok">
|
|
||||||
{`Заявление отправлено на подпись (${signingResult.sent.join(', ')}): письма со ссылкой ушли на ${data.Employee}.`}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{!signingBusy && signingResult && signingResult.skipped.length > 0 && (
|
{!signingBusy && signingResult && signingResult.skipped.length > 0 && (
|
||||||
<Alert variant="err">
|
<Alert variant="err">
|
||||||
{`Не отправлено по компаниям: ${signingResult.skipped.map((c) => `${c.name} — ${c.reason}`).join('; ')}.`}
|
{`Не отправлено по компаниям: ${signingResult.skipped.map((c) => `${c.name} — ${c.reason}`).join('; ')}.`}
|
||||||
@@ -224,10 +226,14 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
notify();
|
notify();
|
||||||
// «Согласовано» = маршрут пройден целиком (фазы исполнения у отпуска нет).
|
// Это решение завершило согласование: подписывать пора, а не когда заявка «Согласовано».
|
||||||
|
// Если в маршруте есть исполнитель, последний согласующий переводит заявку сразу в
|
||||||
|
// «Выполнение», и проверка только на «Согласовано» пропускала такие заявки. Предыдущий
|
||||||
|
// статус (`status` — до этого решения) отсекает решения исполнителя.
|
||||||
// Промис возвращаем: пока он не разрешится, панель решения не пишет «Решение
|
// Промис возвращаем: пока он не разрешится, панель решения не пишет «Решение
|
||||||
// сохранено» — иначе успех появлялся бы раньше, чем заявление ушло на подпись.
|
// сохранено» — иначе успех появлялся бы раньше, чем заявление ушло на подпись; а его
|
||||||
return newStatus === REQUEST_STATUS.approved ? sendToSigning() : undefined;
|
// результат панель допишет к зелёному сообщению.
|
||||||
|
return isApprovalPhaseCompleted(status, newStatus) ? sendToSigning() : undefined;
|
||||||
}}
|
}}
|
||||||
onDelegated={() => {
|
onDelegated={() => {
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface IApproverOrgInfo {
|
|||||||
* email не найден в кадровом справочнике (404) или пуст — getEmployeeOrganizations
|
* email не найден в кадровом справочнике (404) или пуст — getEmployeeOrganizations
|
||||||
* отдаёт [], здесь это просто пустые отдел/организация, не ошибка матрицы.
|
* отдаёт [], здесь это просто пустые отдел/организация, не ошибка матрицы.
|
||||||
*/
|
*/
|
||||||
export function resolveApproverOrgInfo(context: WebPartContext) {
|
export function resolveApproverOrgInfo(context: WebPartContext): (email: string) => Promise<IApproverOrgInfo> {
|
||||||
return async (email: string): Promise<IApproverOrgInfo> => {
|
return async (email: string): Promise<IApproverOrgInfo> => {
|
||||||
const records = await getEmployeeOrganizations(context, email);
|
const records = await getEmployeeOrganizations(context, email);
|
||||||
const first = records[0];
|
const first = records[0];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
REQUEST_STATUS_CHOICES,
|
REQUEST_STATUS_CHOICES,
|
||||||
initialSubmitStatus,
|
initialSubmitStatus,
|
||||||
|
isApprovalPhaseCompleted,
|
||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
isSubmittedStatus,
|
isSubmittedStatus,
|
||||||
} from './statuses';
|
} from './statuses';
|
||||||
@@ -54,6 +55,41 @@ describe('initialSubmitStatus', () => {
|
|||||||
// возвращает initialSubmitStatus, ОБЯЗАН считаться отправкой: иначе строки
|
// возвращает initialSubmitStatus, ОБЯЗАН считаться отправкой: иначе строки
|
||||||
// маршрута получают недопустимый статус, а активации не происходит вовсе —
|
// маршрута получают недопустимый статус, а активации не происходит вовсе —
|
||||||
// именно так заявка без согласующих перестала попадать в очередь исполнения.
|
// именно так заявка без согласующих перестала попадать в очередь исполнения.
|
||||||
|
describe('isApprovalPhaseCompleted', () => {
|
||||||
|
it('последний согласующий без исполнителей: «На согласовании» → «Согласовано»', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.approved)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тот самый случай: в маршруте есть исполнитель, и заявка сразу становится «Выполнение».
|
||||||
|
it('последний согласующий при наличии исполнителя: «На согласовании» → «Выполнение»', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.execution)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('согласование завершилось, исполнители уже отработали: → «Закрыта»', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.closed)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('согласуют не последние: заявка остаётся «На согласовании»', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.onApproval)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возврат и отклонение не завершают согласование', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.returned)).toBe(false);
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.onApproval, REQUEST_STATUS.rejected)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Решение исполнителя не должно второй раз запускать то, что делается по итогам согласования.
|
||||||
|
it('решение исполнителя — не завершение согласования', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.execution, REQUEST_STATUS.execution)).toBe(false);
|
||||||
|
expect(isApprovalPhaseCompleted(REQUEST_STATUS.execution, REQUEST_STATUS.closed)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('предыдущий статус неизвестен — не гадаем', () => {
|
||||||
|
expect(isApprovalPhaseCompleted(undefined, REQUEST_STATUS.approved)).toBe(false);
|
||||||
|
expect(isApprovalPhaseCompleted('', REQUEST_STATUS.execution)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isSubmittedStatus', () => {
|
describe('isSubmittedStatus', () => {
|
||||||
it('любой результат initialSubmitStatus считается отправкой', () => {
|
it('любой результат initialSubmitStatus считается отправкой', () => {
|
||||||
[initialSubmitStatus(1, 0), initialSubmitStatus(0, 1), initialSubmitStatus(0, 0)].forEach((s) =>
|
[initialSubmitStatus(1, 0), initialSubmitStatus(0, 1), initialSubmitStatus(0, 0)].forEach((s) =>
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ export function initialSubmitStatus(approversCount: number, executorsCount: numb
|
|||||||
return REQUEST_STATUS.approved;
|
return REQUEST_STATUS.approved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Это решение завершило фазу СОГЛАСОВАНИЯ: заявка была «На согласовании», а стала «Согласовано»,
|
||||||
|
* «Выполнение» или «Закрыта».
|
||||||
|
*
|
||||||
|
* Статус «Согласовано» бывает только у маршрута без исполнителей. Если исполнитель в маршруте
|
||||||
|
* есть, последний согласующий переводит заявку сразу в «Выполнение», и проверять только
|
||||||
|
* «Согласовано» — значит пропускать такие заявки. Сравнение с ПРЕДЫДУЩИМ статусом нужно затем,
|
||||||
|
* чтобы решение исполнителя («Выполнение» → «Выполнение»/«Закрыта») не принималось за
|
||||||
|
* завершение согласования и побочное действие не срабатывало второй раз.
|
||||||
|
*/
|
||||||
|
export function isApprovalPhaseCompleted(previous: string | undefined, next: string): boolean {
|
||||||
|
if (previous !== REQUEST_STATUS.onApproval) return false;
|
||||||
|
return next === REQUEST_STATUS.approved || next === REQUEST_STATUS.execution || next === REQUEST_STATUS.closed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Статус заявки означает «отправлена» (в отличие от черновика)?
|
* Статус заявки означает «отправлена» (в отличие от черновика)?
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ interface IDecisionPanelProps {
|
|||||||
* Отказ (reject) панель НЕ считает провалом решения — решение к этому моменту
|
* Отказ (reject) панель НЕ считает провалом решения — решение к этому моменту
|
||||||
* уже применено на сервере; сообщить о проблеме побочного действия должен сам
|
* уже применено на сервере; сообщить о проблеме побочного действия должен сам
|
||||||
* обработчик (у него для этого есть своя область экрана).
|
* обработчик (у него для этого есть своя область экрана).
|
||||||
|
*
|
||||||
|
* Может вернуть строку — её панель допишет к зелёному сообщению «Решение сохранено…»
|
||||||
|
* (например, «Документы отправлены на подписание в ЭДО»). Нет побочного действия или оно
|
||||||
|
* не удалось — не возвращайте ничего, сообщение останется прежним.
|
||||||
*/
|
*/
|
||||||
onDecided?: (status: string) => void | Promise<void>;
|
onDecided?: (status: string) => void | string | undefined | Promise<void | string | undefined>;
|
||||||
/** Выполнить ПЕРЕД применением решения (напр. сохранить поля, заполненные
|
/** Выполнить ПЕРЕД применением решения (напр. сохранить поля, заполненные
|
||||||
* согласующим). Если бросит — решение не применяется, показывается ошибка.
|
* согласующим). Если бросит — решение не применяется, показывается ошибка.
|
||||||
* decision — какое решение применяется (проверки, нужные только при
|
* decision — какое решение применяется (проверки, нужные только при
|
||||||
@@ -66,6 +70,8 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
|
|||||||
const [comment, setComment] = useState<string>('');
|
const [comment, setComment] = useState<string>('');
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||||
const [resultStatus, setResultStatus] = useState<string | undefined>(undefined);
|
const [resultStatus, setResultStatus] = useState<string | undefined>(undefined);
|
||||||
|
// Что обработчик onDecided велел дописать к сообщению об успехе.
|
||||||
|
const [resultNote, setResultNote] = useState<string | undefined>(undefined);
|
||||||
const [delegated, setDelegated] = useState<number>(0);
|
const [delegated, setDelegated] = useState<number>(0);
|
||||||
const [showDelegate, setShowDelegate] = useState<boolean>(false);
|
const [showDelegate, setShowDelegate] = useState<boolean>(false);
|
||||||
// Выбранные делеганты копятся здесь и пишутся одной пачкой по кнопке — иначе
|
// Выбранные делеганты копятся здесь и пишутся одной пачкой по кнопке — иначе
|
||||||
@@ -119,13 +125,16 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
|
|||||||
// отработал, кнопки остаются заблокированными (submitting), и пользователь не
|
// отработал, кнопки остаются заблокированными (submitting), и пользователь не
|
||||||
// видит успеха раньше, чем всё сделано. Провал onDecided на статус решения не
|
// видит успеха раньше, чем всё сделано. Провал onDecided на статус решения не
|
||||||
// влияет — оно уже применено, поэтому сообщение показываем всё равно.
|
// влияет — оно уже применено, поэтому сообщение показываем всё равно.
|
||||||
|
let note: string | undefined;
|
||||||
if (onDecided) {
|
if (onDecided) {
|
||||||
try {
|
try {
|
||||||
await onDecided(status);
|
const returned = await onDecided(status);
|
||||||
|
note = typeof returned === 'string' && returned ? returned : undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Побочное действие после решения не выполнилось', e);
|
console.error('Побочное действие после решения не выполнилось', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setResultNote(note);
|
||||||
setResultStatus(status);
|
setResultStatus(status);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -205,7 +214,7 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
|
|||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<Alert variant="ok">
|
<Alert variant="ok">
|
||||||
{isExecution ? 'Отмечено как выполнено' : 'Решение сохранено'}. Статус заявки: «
|
{isExecution ? 'Отмечено как выполнено' : 'Решение сохранено'}. Статус заявки: «
|
||||||
{resultStatus}».
|
{resultStatus}».{resultNote ? ` ${resultNote}` : ''}
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const SubmitActions: React.FC<ISubmitActionsProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const { saving, kind } = submit;
|
const { saving, kind } = submit;
|
||||||
const unfrozenForm = formId != null && UNFROZEN_FORMS.has(formId);
|
const unfrozenForm = formId !== undefined && UNFROZEN_FORMS.has(formId);
|
||||||
// false, пока не пришёл ответ о членстве в группе, — безопасный дефолт: не
|
// false, пока не пришёл ответ о членстве в группе, — безопасный дефолт: не
|
||||||
// мигать разблокированными кнопками до подтверждения, что это тестировщик.
|
// мигать разблокированными кнопками до подтверждения, что это тестировщик.
|
||||||
const [isTester, setIsTester] = React.useState<boolean>(false);
|
const [isTester, setIsTester] = React.useState<boolean>(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user