feat: таблица исполнителей во всех формах — при заполнении и просмотре, если они есть в маршруте
Общие useFormExecutors/useViewExecutors и ExecutorsMatrix вместо копирования в каждой форме. Подключены в формах без фазы исполнения: ConcreteRequest, Dismissal, EmployeeTransfer, ServiceRequest, Ticket1C, UnpaidLeave, Vacation. Исполнители теперь пишутся в маршрут при подаче; таблица согласующих грузит только фазу согласования, чтобы не смешиваться с исполнением. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
@@ -25,7 +26,11 @@ import {
|
|||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage
|
unassignedStepMessage,
|
||||||
|
useFormExecutors,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
@@ -171,7 +176,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.SiteChiefEmail, approval, 'Approval', null, undefined, projectId),
|
: getPlannedMatrix(context, requestTypeId as number, data.SiteChiefEmail, approval, 'Approval', null, undefined, projectId),
|
||||||
[context, approval, editId, data.SiteChiefEmail, requestTypeId, projectId]
|
[context, approval, editId, data.SiteChiefEmail, requestTypeId, projectId]
|
||||||
);
|
);
|
||||||
@@ -183,6 +188,17 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Исполнители — из маршрута админки (заявитель их не правит); те же параметры
|
||||||
|
// резолва, что и у согласующих.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: data.SiteChiefEmail,
|
||||||
|
editId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
const hasItems = data.Items.some((i) => i.mixClass.trim().length > 0 && i.volume.trim().length > 0);
|
const hasItems = data.Items.some((i) => i.mixClass.trim().length > 0 && i.volume.trim().length > 0);
|
||||||
const isValid = !!(data.RequestDate && data.SiteChiefEmail && data.Project.trim() && hasItems);
|
const isValid = !!(data.RequestDate && data.SiteChiefEmail && data.Project.trim() && hasItems);
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
@@ -224,6 +240,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -238,7 +255,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||||
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
||||||
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -260,11 +277,21 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
// Без исполнителей — прежний статус «На согласовании»; с ними — как у рефералки
|
||||||
|
// (initialSubmitStatus учитывает маршрут без согласующих).
|
||||||
|
const submitStatus = executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrix.rows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval;
|
||||||
|
const handleSubmit = (): Promise<void> => submit.run('submit', save(submitStatus), validateSubmit);
|
||||||
|
|
||||||
const sectionProps = {
|
const sectionProps = {
|
||||||
data,
|
data,
|
||||||
@@ -331,6 +358,8 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
waitingText="Укажите начальника участка и проект, чтобы построить маршрут согласования."
|
waitingText="Укажите начальника участка и проект, чтобы построить маршрут согласования."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import {
|
|||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId,
|
||||||
|
useViewExecutors,
|
||||||
|
approvalPhaseRows
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
@@ -71,10 +74,11 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
|
|||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -153,6 +157,7 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить заявку на бетон: содержимое + маршрут согласования.
|
* Сохранить заявку на бетон: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
* Исполнители (фаза «Исполнение») приходят из маршрута админки и пишутся тем же writeRoute.
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadConcreteRequest). Имеет смысл
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadConcreteRequest). Имеет смысл
|
||||||
@@ -59,12 +59,13 @@ export async function saveConcreteRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
await writeRoute(concreteRequestApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(concreteRequestApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
@@ -25,7 +26,11 @@ import {
|
|||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage,
|
unassignedStepMessage,
|
||||||
resolveWriteListId
|
resolveWriteListId,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus,
|
||||||
|
useFormExecutors
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
@@ -159,7 +164,7 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, [
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, [
|
||||||
{ fieldName: 'Position', value: primaryPosition(data.Organization) }
|
{ fieldName: 'Position', value: primaryPosition(data.Organization) }
|
||||||
]),
|
]),
|
||||||
@@ -173,6 +178,17 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Исполнители самого заявления (фаза «Исполнение» маршрута из админки) — по
|
||||||
|
// тому же сотруднику и тем же fieldValues, что и согласующие выше.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: data.Employee,
|
||||||
|
editId,
|
||||||
|
fieldValues: [{ fieldName: 'Position', value: primaryPosition(data.Organization) }]
|
||||||
|
});
|
||||||
|
|
||||||
// Убрать компанию из заявления (совместитель, реально не увольняется из
|
// Убрать компанию из заявления (совместитель, реально не увольняется из
|
||||||
// ВСЕХ) — см. OrganizationsTable. Снимает компанию из data.Organization И, по
|
// ВСЕХ) — см. OrganizationsTable. Снимает компанию из data.Organization И, по
|
||||||
// возможности, соответствующего согласующего (директора/должностного
|
// возможности, соответствующего согласующего (директора/должностного
|
||||||
@@ -245,6 +261,7 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -259,7 +276,7 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||||
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
||||||
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -281,11 +298,27 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
// Исполнители в маршруте есть → статус стартует с учётом фаз (initialSubmitStatus:
|
||||||
|
// без согласующих сразу «Выполнение»); иначе прежний «На согласовании».
|
||||||
|
const handleSubmit = (): Promise<void> =>
|
||||||
|
submit.run(
|
||||||
|
'submit',
|
||||||
|
save(
|
||||||
|
executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrix.rows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval
|
||||||
|
),
|
||||||
|
validateSubmit
|
||||||
|
);
|
||||||
|
|
||||||
const sectionProps = {
|
const sectionProps = {
|
||||||
data,
|
data,
|
||||||
@@ -348,6 +381,8 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили, — поэтому история и в форме, а не только в просмотре.
|
отклонили, — поэтому история и в форме, а не только в просмотре.
|
||||||
Только у уже существующей заявки: у новой истории нет. */}
|
Только у уже существующей заявки: у новой истории нет. */}
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import {
|
|||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS
|
REQUEST_STATUS,
|
||||||
|
approvalPhaseRows,
|
||||||
|
useViewExecutors
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { getManagerEmail } from '../../shared/dictionary';
|
import { getManagerEmail } from '../../shared/dictionary';
|
||||||
@@ -87,10 +90,12 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
// Исполнители — отдельная фаза тех же строк маршрута; перечитываются вместе с согласующими.
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -282,6 +287,8 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* История согласования: маршрут пересохраняется патчем (см.
|
{/* История согласования: маршрут пересохраняется патчем (см.
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить заявление на увольнение: содержимое + маршрут согласования.
|
* Сохранить заявление на увольнение: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
* executors — исполнители самого заявления (фаза «Исполнение» маршрута из
|
||||||
|
* админки; пусто, если в маршруте их нет). Это не маршрут связанной заявки
|
||||||
|
* Clearance (см. createClearance.ts).
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadDismissalRequest). Имеет смысл
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadDismissalRequest). Имеет смысл
|
||||||
@@ -109,6 +111,7 @@ export async function saveDismissalRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
@@ -116,7 +119,7 @@ export async function saveDismissalRequest(
|
|||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
await writeRoute(dismissalApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(dismissalApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import {
|
|||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useFormExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
resolveWriteListId,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
@@ -155,7 +160,7 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
||||||
[context, approval, editId, data.Employee, requestTypeId]
|
[context, approval, editId, data.Employee, requestTypeId]
|
||||||
);
|
);
|
||||||
@@ -167,6 +172,16 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Исполнители — по тому же сотруднику, что и маршрут согласования. Только чтение:
|
||||||
|
// их назначает маршрут из админки, не заявитель.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: data.Employee,
|
||||||
|
editId
|
||||||
|
});
|
||||||
|
|
||||||
const isValid = !!(
|
const isValid = !!(
|
||||||
data.Employee &&
|
data.Employee &&
|
||||||
data.Position.trim() &&
|
data.Position.trim() &&
|
||||||
@@ -213,6 +228,7 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -227,7 +243,7 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||||
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
||||||
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -249,11 +265,27 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
// Есть исполнители — initialSubmitStatus учитывает маршрут без согласующих
|
||||||
|
// (сразу «Выполнение»); иначе прежний статус «На согласовании».
|
||||||
|
const handleSubmit = (): Promise<void> =>
|
||||||
|
submit.run(
|
||||||
|
'submit',
|
||||||
|
save(
|
||||||
|
executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrix.rows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval
|
||||||
|
),
|
||||||
|
validateSubmit
|
||||||
|
);
|
||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
@@ -310,6 +342,8 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useViewExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
@@ -70,10 +73,11 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
|
|||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -150,6 +154,7 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить заявку на перемещение: содержимое + маршрут согласования.
|
* Сохранить заявку на перемещение: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
* executors — исполнители из маршрута (фаза «Исполнение»); пустой список, если в
|
||||||
|
* маршруте их нет.
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadEmployeeTransferRequest). Имеет
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadEmployeeTransferRequest). Имеет
|
||||||
@@ -55,6 +56,7 @@ export async function saveEmployeeTransferRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
@@ -62,6 +64,6 @@ export async function saveEmployeeTransferRequest(
|
|||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
await writeRoute(employeeTransferApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(employeeTransferApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ import {
|
|||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage,
|
unassignedStepMessage,
|
||||||
resolveWriteListId
|
resolveWriteListId,
|
||||||
|
useFormExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import {
|
import {
|
||||||
uploadAttachments,
|
uploadAttachments,
|
||||||
@@ -181,13 +186,23 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, undefined, projectId),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, undefined, projectId),
|
||||||
[context, userEmail, approval, editId, projectId, requestTypeId]
|
[context, userEmail, approval, editId, projectId, requestTypeId]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(loadMatrix, `${userEmail}-${editId ?? 'new'}-${projectId}`, !readOnly, matrixReady, (login) =>
|
const matrix = useMatrixEditor(loadMatrix, `${userEmail}-${editId ?? 'new'}-${projectId}`, !readOnly, matrixReady, (login) =>
|
||||||
approval.getJobTitle(login)
|
approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
// Исполнители резолвятся тем же маршрутом и с тем же проектом, что и согласующие:
|
||||||
|
// пока проект не выбран, e-mail пустой — useFormExecutors не грузит маршрут (404).
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: matrixReady ? userEmail : '',
|
||||||
|
editId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
const hasCosts = hasAnyCostItem(data.Costs);
|
const hasCosts = hasAnyCostItem(data.Costs);
|
||||||
const isValid =
|
const isValid =
|
||||||
@@ -273,7 +288,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
||||||
}
|
}
|
||||||
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
const { id, code } = await saveServiceRequest(requestId, rtId, data, toApprovers(matrix.rows), status, writeListId);
|
const { id, code } = await saveServiceRequest(requestId, rtId, data, toApprovers(matrix.rows), executorsToWrite(executors.rows), status, writeListId);
|
||||||
setRequestId(id);
|
setRequestId(id);
|
||||||
setRequestStatus(status);
|
setRequestStatus(status);
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -294,7 +309,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
onSaved();
|
onSaved();
|
||||||
}
|
}
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -314,11 +329,26 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
const handleSubmit = (): Promise<void> => submit.run(
|
||||||
|
'submit',
|
||||||
|
// Есть исполнители — initialSubmitStatus сам выбирает стартовый статус (согласующих
|
||||||
|
// может не быть вовсе), иначе прежнее «На согласовании».
|
||||||
|
save(
|
||||||
|
executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrix.rows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval
|
||||||
|
),
|
||||||
|
validateSubmit
|
||||||
|
);
|
||||||
|
|
||||||
const sectionProps = {
|
const sectionProps = {
|
||||||
data,
|
data,
|
||||||
@@ -390,6 +420,8 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
|||||||
waitingText="Укажите заявителя и проект, чтобы построить маршрут согласования."
|
waitingText="Укажите заявителя и проект, чтобы построить маршрут согласования."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useViewExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
isEditableStatus
|
isEditableStatus
|
||||||
@@ -77,10 +80,11 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
||||||
|
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = React.useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = React.useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -180,6 +184,7 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* История согласования: маршрут пересохраняется патчем (см.
|
{/* История согласования: маршрут пересохраняется патчем (см.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
export type ISaveResult = ISaveItemResult;
|
export type ISaveResult = ISaveItemResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить «Заявку на услуги»: контент + маршрут согласования.
|
* Сохранить «Заявку на услуги»: контент + маршрут согласования и исполнения.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём
|
||||||
* новую. Возвращает Id заявки (+ уникальный код при создании).
|
* новую. Возвращает Id заявки (+ уникальный код при создании).
|
||||||
*
|
*
|
||||||
@@ -56,14 +56,13 @@ export async function saveServiceRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
// Фазы исполнения у заявки на услуги нет — договор дальше ведут вне портала,
|
await writeRoute(serviceRequestApprovalService(listId), result.id, approvers, executors, status);
|
||||||
// поэтому исполнителей не пишем (пустой массив).
|
|
||||||
await writeRoute(serviceRequestApprovalService(listId), result.id, approvers, [], status);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ import {
|
|||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
unassignedStepMessage
|
unassignedStepMessage,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
useFormExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
initialSubmitStatus
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import {
|
import {
|
||||||
uploadAttachments,
|
uploadAttachments,
|
||||||
@@ -172,7 +177,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, [
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, [
|
||||||
{ fieldName: 'WorkType', value: data.WorkType },
|
{ fieldName: 'WorkType', value: data.WorkType },
|
||||||
{ fieldName: 'Department', value: data.Department }
|
{ fieldName: 'Department', value: data.Department }
|
||||||
@@ -186,6 +191,20 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
!!editId || (!!data.WorkType && !!requestTypeId),
|
!!editId || (!!data.WorkType && !!requestTypeId),
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
// Исполнители (фаза «Исполнение») резолвятся с теми же fieldValues, что и
|
||||||
|
// согласующие: условные шаги маршрута зависят от вида работ и отдела.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail,
|
||||||
|
editId,
|
||||||
|
departmentId: null,
|
||||||
|
fieldValues: [
|
||||||
|
{ fieldName: 'WorkType', value: data.WorkType },
|
||||||
|
{ fieldName: 'Department', value: data.Department }
|
||||||
|
]
|
||||||
|
});
|
||||||
// Должность нового «Руководителя» — approval-route её не знает (резолвил шаг
|
// Должность нового «Руководителя» — approval-route её не знает (резолвил шаг
|
||||||
// на ДРУГОГО человека и отдал его должность), поэтому подтягиваем сами по
|
// на ДРУГОГО человека и отдал его должность), поэтому подтягиваем сами по
|
||||||
// e-mail из поля (SPS-JobTitle, тот же источник, что и у обычных согласующих,
|
// e-mail из поля (SPS-JobTitle, тот же источник, что и у обычных согласующих,
|
||||||
@@ -278,7 +297,13 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
// работ (раньше в этом случае заявка сразу становилась «Согласовано» в
|
// работ (раньше в этом случае заявка сразу становилась «Согласовано» в
|
||||||
// обход согласования; теперь это просто «На согласовании» с пустой
|
// обход согласования; теперь это просто «На согласовании» с пустой
|
||||||
// матрицей — сигнал администратору маршрутов донастроить условие).
|
// матрицей — сигнал администратору маршрутов донастроить условие).
|
||||||
const finalStatus = intendedStatus === REQUEST_STATUS.draft ? REQUEST_STATUS.draft : REQUEST_STATUS.onApproval;
|
// Есть исполнители — статус отправки по initialSubmitStatus (без согласующих
|
||||||
|
// сразу «Выполнение»); иначе, как раньше, «На согласовании» (в т.ч. при пустом маршруте).
|
||||||
|
const submitStatus =
|
||||||
|
executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrixRows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval;
|
||||||
|
const finalStatus = intendedStatus === REQUEST_STATUS.draft ? REQUEST_STATUS.draft : submitStatus;
|
||||||
|
|
||||||
// requestId уже задан → редактируем существующую заявку, пишем в ту же
|
// requestId уже задан → редактируем существующую заявку, пишем в ту же
|
||||||
// партицию (editListId). Иначе — создаём новую, и партицию для записи
|
// партицию (editListId). Иначе — создаём новую, и партицию для записи
|
||||||
@@ -293,6 +318,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
requestId,
|
requestId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrixRows),
|
toApprovers(matrixRows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
finalStatus,
|
finalStatus,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -315,7 +341,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Передача в Planner теперь происходит только после реального решения
|
// Передача в Planner теперь происходит только после реального решения
|
||||||
// согласующего (см. Ticket1CView.handoffToPlanner) — здесь заявка всегда
|
// согласующего (см. Ticket1CView.handoffToPlanner) — здесь заявка всегда
|
||||||
// отправляется на согласование, минуя Planner напрямую.
|
// отправляется на согласование, минуя Planner напрямую.
|
||||||
if (finalStatus === REQUEST_STATUS.onApproval) {
|
if (finalStatus === REQUEST_STATUS.onApproval || finalStatus === REQUEST_STATUS.execution) {
|
||||||
// Имена списков — той же партиции, куда фактически записали (writeListId),
|
// Имена списков — той же партиции, куда фактически записали (writeListId),
|
||||||
// не обязательно текущей на момент рендера формы: поток ищет строку
|
// не обязательно текущей на момент рендера формы: поток ищет строку
|
||||||
// маршрута именно в НЕЙ.
|
// маршрута именно в НЕЙ.
|
||||||
@@ -345,6 +371,11 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -424,6 +455,8 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С."
|
emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import {
|
|||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
REQUEST_STATUS
|
REQUEST_STATUS,
|
||||||
|
approvalPhaseRows,
|
||||||
|
useViewExecutors,
|
||||||
|
ExecutorsMatrix
|
||||||
} 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';
|
||||||
@@ -112,10 +115,11 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
// decisionSeq входит в reloadKey: после решения строки маршрута меняются на
|
// decisionSeq входит в reloadKey: после решения строки маршрута меняются на
|
||||||
// сервере, поэтому счётчик инкрементируется и useMatrixEditor перечитывает матрицу.
|
// сервере, поэтому счётчик инкрементируется и useMatrixEditor перечитывает матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -246,6 +250,8 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
search={(q) => approval.searchUsers(q)}
|
search={(q) => approval.searchUsers(q)}
|
||||||
emptyText="Согласование не требовалось для этого вида работ."
|
emptyText="Согласование не требовалось для этого вида работ."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* История согласования: маршрут пересохраняется патчем (см.
|
{/* История согласования: маршрут пересохраняется патчем (см.
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
export type ISaveResult = ISaveItemResult;
|
export type ISaveResult = ISaveItemResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить заявку в отдел 1С: содержимое + маршрут согласования.
|
* Сохранить заявку в отдел 1С: содержимое + маршрут (согласующие и исполнители).
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет: после согласования заявка уходит в Planner, а не исполнителю
|
* executors — исполнители из маршрута админки (фаза «Исполнение»), могут быть пустыми:
|
||||||
* из матрицы. Возвращает Id заявки (+ код при создании).
|
* тогда после согласования заявка уходит в Planner. Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
||||||
* ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest), при создании — партиция,
|
* ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest), при создании — партиция,
|
||||||
@@ -49,13 +49,14 @@ export async function saveTicketRequest(
|
|||||||
existingId: number | undefined,
|
existingId: number | undefined,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, TICKET1C_CODE_PREFIX, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, TICKET1C_CODE_PREFIX, item);
|
||||||
await writeRoute(ticket1cApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(ticket1cApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useFormExecutors,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
@@ -156,7 +161,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
||||||
[context, approval, editId, data.Employee, requestTypeId]
|
[context, approval, editId, data.Employee, requestTypeId]
|
||||||
);
|
);
|
||||||
@@ -168,6 +173,15 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Исполнители фазы «Исполнение» — по маршруту из админки, только чтение.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: data.Employee,
|
||||||
|
editId
|
||||||
|
});
|
||||||
|
|
||||||
// Даты корректны, если конец не раньше начала (обе обязательны).
|
// Даты корректны, если конец не раньше начала (обе обязательны).
|
||||||
const datesValid = !!(data.StartDate && data.EndDate && data.EndDate >= data.StartDate);
|
const datesValid = !!(data.StartDate && data.EndDate && data.EndDate >= data.StartDate);
|
||||||
// Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала
|
// Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала
|
||||||
@@ -225,6 +239,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -239,7 +254,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||||
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
// Имена списков — той же партиции, куда фактически записана заявка (writeListId),
|
||||||
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
// не текущей: поток ищет строку маршрута именно в НЕЙ.
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -261,11 +276,22 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
// Есть исполнители — initialSubmitStatus учитывает маршрут без согласующих.
|
||||||
|
const handleSubmit = (): Promise<void> =>
|
||||||
|
submit.run(
|
||||||
|
'submit',
|
||||||
|
save(executors.rows.length > 0 ? initialSubmitStatus(matrix.rows.length, executors.rows.length) : REQUEST_STATUS.onApproval),
|
||||||
|
validateSubmit
|
||||||
|
);
|
||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
@@ -322,6 +348,8 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import {
|
|||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
|
ExecutorsMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useViewExecutors,
|
||||||
|
approvalPhaseRows,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
@@ -70,7 +73,8 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
@@ -150,6 +154,8 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить заявку без содержания: содержимое + маршрут согласования.
|
* Сохранить заявку без содержания: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения нет — writeRoute вызывается с пустым списком исполнителей.
|
* executors — исполнители фазы «Исполнение» из маршрута (пусто, если в маршруте их нет).
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadUnpaidLeaveRequest). Имеет смысл
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadUnpaidLeaveRequest). Имеет смысл
|
||||||
@@ -75,6 +75,7 @@ export async function saveUnpaidLeaveRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
@@ -84,6 +85,6 @@ export async function saveUnpaidLeaveRequest(
|
|||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee, substituteId);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee, substituteId);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
await writeRoute(unpaidLeaveApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(unpaidLeaveApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import {
|
|||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useFormExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
initialSubmitStatus,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
resolveWriteListId,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
@@ -157,7 +162,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId).then(approvalPhaseRows)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
|
||||||
[context, approval, editId, data.Employee, requestTypeId]
|
[context, approval, editId, data.Employee, requestTypeId]
|
||||||
);
|
);
|
||||||
@@ -169,6 +174,16 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Исполнители из маршрута админки (фаза «Исполнение»): только чтение, таблица
|
||||||
|
// и запись в заявку — когда они в маршруте есть.
|
||||||
|
const executors = useFormExecutors({
|
||||||
|
context,
|
||||||
|
approval,
|
||||||
|
requestTypeId,
|
||||||
|
userEmail: data.Employee,
|
||||||
|
editId
|
||||||
|
});
|
||||||
|
|
||||||
const daysNum = Number(data.DaysCount);
|
const daysNum = Number(data.DaysCount);
|
||||||
// Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала
|
// Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала
|
||||||
// отпуска — календарь (min у input type=date, см. RequestSection) не даёт
|
// отпуска — календарь (min у input type=date, см. RequestSection) не даёт
|
||||||
@@ -223,6 +238,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
rtId,
|
rtId,
|
||||||
data,
|
data,
|
||||||
toApprovers(matrix.rows),
|
toApprovers(matrix.rows),
|
||||||
|
executorsToWrite(executors.rows),
|
||||||
status,
|
status,
|
||||||
writeListId
|
writeListId
|
||||||
);
|
);
|
||||||
@@ -238,7 +254,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Имена списков — той же партиции, куда фактически записали (writeListId),
|
// Имена списков — той же партиции, куда фактически записали (writeListId),
|
||||||
// не обязательно текущей на момент рендера формы: поток ищет строку
|
// не обязательно текущей на момент рендера формы: поток ищет строку
|
||||||
// маршрута именно в НЕЙ.
|
// маршрута именно в НЕЙ.
|
||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval || status === REQUEST_STATUS.execution) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
@@ -260,11 +276,27 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
|
||||||
);
|
);
|
||||||
if (unassignedError) return unassignedError;
|
if (unassignedError) return unassignedError;
|
||||||
|
const unassignedExecutorError = unassignedStepMessage(
|
||||||
|
executors.rows,
|
||||||
|
(r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — обратитесь к администратору маршрута.`
|
||||||
|
);
|
||||||
|
if (unassignedExecutorError) return unassignedExecutorError;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
const handleSaveDraft = (): Promise<void> => submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||||
const handleSubmit = (): Promise<void> => submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
// Есть исполнители — initialSubmitStatus учтёт и маршрут без согласующих (сразу «Выполнение»);
|
||||||
|
// иначе прежнее поведение: «На согласовании».
|
||||||
|
const handleSubmit = (): Promise<void> =>
|
||||||
|
submit.run(
|
||||||
|
'submit',
|
||||||
|
save(
|
||||||
|
executors.rows.length > 0
|
||||||
|
? initialSubmitStatus(matrix.rows.length, executors.rows.length)
|
||||||
|
: REQUEST_STATUS.onApproval
|
||||||
|
),
|
||||||
|
validateSubmit
|
||||||
|
);
|
||||||
|
|
||||||
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
const sectionProps = { data, set, context, onEmployeeChange: handleEmployeeChange };
|
||||||
|
|
||||||
@@ -321,6 +353,8 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Автору на доработке важнее всего видеть, почему вернули или
|
{/* Автору на доработке важнее всего видеть, почему вернули или
|
||||||
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
отклонили. Только у уже существующей заявки: у новой истории нет. */}
|
||||||
{editId && (
|
{editId && (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
|
useViewExecutors,
|
||||||
|
ExecutorsMatrix,
|
||||||
|
approvalPhaseRows,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
@@ -78,10 +81,11 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId).then(approvalPhaseRows), [approval, itemId]);
|
||||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
approval.getJobTitle(l)
|
approval.getJobTitle(l)
|
||||||
);
|
);
|
||||||
|
const executors = useViewExecutors(context, approval, itemId, decisionSeq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -180,6 +184,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export type ISaveResult = ISaveItemResult;
|
|||||||
/**
|
/**
|
||||||
* Сохранить заявку на отпуск: содержимое + маршрут согласования.
|
* Сохранить заявку на отпуск: содержимое + маршрут согласования.
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения у отпуска нет — writeRoute вызывается с пустым списком исполнителей.
|
* executors — исполнители фазы «Исполнение» из маршрута админки (пусто, если в маршруте их нет).
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*
|
*
|
||||||
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
* listId — партиция, в которую фактически пишем: при редактировании — партиция
|
||||||
@@ -70,6 +70,7 @@ export async function saveVacationRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
listId?: string
|
listId?: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
@@ -79,6 +80,6 @@ export async function saveVacationRequest(
|
|||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee, substituteId);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee, substituteId);
|
||||||
|
|
||||||
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item);
|
||||||
await writeRoute(vacationApprovalService(listId), result.id, approvers, [], status);
|
await writeRoute(vacationApprovalService(listId), result.id, approvers, executors, status);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,19 @@ export { notifyActiveStep } from './notify.service';
|
|||||||
export type { INotifyTarget } from './notify.service';
|
export type { INotifyTarget } from './notify.service';
|
||||||
export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
|
export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
|
||||||
export type { IUseMatrixEditor } from './useMatrixEditor';
|
export type { IUseMatrixEditor } from './useMatrixEditor';
|
||||||
|
export {
|
||||||
|
approvalPhaseRows,
|
||||||
|
executionPhaseRows,
|
||||||
|
executorsToWrite,
|
||||||
|
useFormExecutors,
|
||||||
|
useViewExecutors
|
||||||
|
} from './useRouteExecutors';
|
||||||
export { resolveApproverOrgInfo } from './approverOrgInfo';
|
export { resolveApproverOrgInfo } from './approverOrgInfo';
|
||||||
export type { IApproverOrgInfo } from './approverOrgInfo';
|
export type { IApproverOrgInfo } from './approverOrgInfo';
|
||||||
export { canEditRoute, composeRemoveRowRule } from './routeEditRights';
|
export { canEditRoute, composeRemoveRowRule } from './routeEditRights';
|
||||||
export { canEditAllFields } from './fullEditRights';
|
export { canEditAllFields } from './fullEditRights';
|
||||||
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
||||||
|
export { ExecutorsMatrix } from './ui/ExecutorsMatrix';
|
||||||
export { ApprovalHistory } from './history/ApprovalHistory';
|
export { ApprovalHistory } from './history/ApprovalHistory';
|
||||||
export { ApprovalHistoryFeed } from './history/ApprovalHistoryFeed';
|
export { ApprovalHistoryFeed } from './history/ApprovalHistoryFeed';
|
||||||
export { HISTORY_TEXT } from './history/historyTexts';
|
export { HISTORY_TEXT } from './history/historyTexts';
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ISiteUser } from '../models/ISiteUser';
|
||||||
|
import { IUseMatrixEditor } from '../useMatrixEditor';
|
||||||
|
import { ApprovalMatrix } from './ApprovalMatrix';
|
||||||
|
|
||||||
|
interface IExecutorsMatrixProps {
|
||||||
|
editor: IUseMatrixEditor;
|
||||||
|
search: (query: string) => Promise<ISiteUser[]>;
|
||||||
|
/** Заголовок; по умолчанию «Исполнение». */
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Таблица исполнителей — рисуется только когда в маршруте из админки они есть:
|
||||||
|
* у форм без фазы исполнения блока нет вовсе.
|
||||||
|
*/
|
||||||
|
export const ExecutorsMatrix: React.FC<IExecutorsMatrixProps> = ({ editor, search, title = 'Исполнение' }) => {
|
||||||
|
if (editor.loading || editor.rows.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={editor}
|
||||||
|
search={search}
|
||||||
|
title={title}
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не назначены."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
|
||||||
|
import { getPlannedMatrix } from './approvalSubmission.service';
|
||||||
|
import { resolveApproverOrgInfo } from './approverOrgInfo';
|
||||||
|
import { IResolveFieldValue } from './models/IApprovalRouteApi';
|
||||||
|
import { IMatrixRow } from './models/IMatrixRow';
|
||||||
|
import { APPROVAL_PHASE } from './models/statuses';
|
||||||
|
import { IUseMatrixEditor, useMatrixEditor } from './useMatrixEditor';
|
||||||
|
|
||||||
|
/** Строки фазы согласования (всё, что не «Исполнение») — для таблицы согласующих. */
|
||||||
|
export const approvalPhaseRows = (rows: IMatrixRow[]): IMatrixRow[] =>
|
||||||
|
rows.filter((r) => r.phase !== APPROVAL_PHASE.execution);
|
||||||
|
|
||||||
|
/** Строки фазы исполнения — для таблицы исполнителей. */
|
||||||
|
export const executionPhaseRows = (rows: IMatrixRow[]): IMatrixRow[] =>
|
||||||
|
rows.filter((r) => r.phase === APPROVAL_PHASE.execution);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Строки матрицы исполнителей → то, что пишет writeRoute. Номер шага
|
||||||
|
* нормализуется к 1..N внутри фазы: у уже записанных строк он сквозной (со
|
||||||
|
* смещением на согласующих), а writeRoute смещает фазу сам.
|
||||||
|
*/
|
||||||
|
export const executorsToWrite = (rows: IMatrixRow[]): IApproverToWrite[] => {
|
||||||
|
const base = rows.reduce((min, r) => Math.min(min, r.order), Number.MAX_SAFE_INTEGER);
|
||||||
|
return rows.map((r) => ({
|
||||||
|
email: r.email,
|
||||||
|
position: r.position,
|
||||||
|
allowReassign: r.allowReassign,
|
||||||
|
order: r.order - base + 1,
|
||||||
|
slaHours: r.slaHours,
|
||||||
|
askOverdueReason: r.askOverdueReason,
|
||||||
|
autoApprove: r.autoApprove
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IFormExecutorsSource {
|
||||||
|
context: WebPartContext;
|
||||||
|
approval: ApprovalListService;
|
||||||
|
requestTypeId: number | undefined;
|
||||||
|
/** E-mail, по которому резолвится маршрут (инициатор/выбранный сотрудник). */
|
||||||
|
userEmail: string;
|
||||||
|
/** Редактируемая заявка — исполнители читаются из её строк, а не планируются заново. */
|
||||||
|
editId: number | undefined;
|
||||||
|
/** Те же параметры, что форма отдаёт getPlannedMatrix для согласующих. */
|
||||||
|
departmentId?: string | null; // eslint-disable-line @rushstack/no-new-null -- транзитом в resolveRoute
|
||||||
|
fieldValues?: IResolveFieldValue[];
|
||||||
|
projectId?: string | null; // eslint-disable-line @rushstack/no-new-null -- транзитом в resolveRoute
|
||||||
|
/** Дополнительный ключ перезагрузки (значение поля, влияющее на маршрут). */
|
||||||
|
reloadSuffix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнители формы заполнения (фаза «Исполнение»): планируемые по маршруту из
|
||||||
|
* админки для новой заявки, из записанных строк — для черновика/доработки.
|
||||||
|
* Только чтение: исполнителя назначает маршрут, не заявитель.
|
||||||
|
*/
|
||||||
|
export function useFormExecutors(source: IFormExecutorsSource): IUseMatrixEditor {
|
||||||
|
const { context, approval, requestTypeId, userEmail, editId, departmentId, fieldValues, projectId, reloadSuffix = '' } = source;
|
||||||
|
const fieldValuesKey = JSON.stringify(fieldValues ?? []);
|
||||||
|
const load = useCallback(
|
||||||
|
() =>
|
||||||
|
editId
|
||||||
|
? approval.getMatrixForRequest(editId).then(executionPhaseRows)
|
||||||
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', departmentId, fieldValues, projectId),
|
||||||
|
// fieldValues сравниваем по содержимому: форма пересоздаёт массив на каждый рендер.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[context, approval, editId, requestTypeId, userEmail, departmentId, fieldValuesKey, projectId]
|
||||||
|
);
|
||||||
|
return useMatrixEditor(
|
||||||
|
load,
|
||||||
|
`x-${userEmail}-${editId ?? 'new'}-${departmentId ?? ''}-${projectId ?? ''}-${fieldValuesKey}-${reloadSuffix}`,
|
||||||
|
false,
|
||||||
|
!!editId || (!!userEmail && !!requestTypeId),
|
||||||
|
(login) => approval.getJobTitle(login),
|
||||||
|
resolveApproverOrgInfo(context)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Исполнители вьюхи просмотра — из фактических строк заявки. decisionSeq в
|
||||||
|
* ключе: после решения/замены строки на сервере меняются — перечитываем.
|
||||||
|
*/
|
||||||
|
export function useViewExecutors(
|
||||||
|
context: WebPartContext,
|
||||||
|
approval: ApprovalListService,
|
||||||
|
itemId: number,
|
||||||
|
decisionSeq: number
|
||||||
|
): IUseMatrixEditor {
|
||||||
|
const load = useCallback(
|
||||||
|
() => approval.getMatrixForRequest(itemId).then(executionPhaseRows),
|
||||||
|
[approval, itemId]
|
||||||
|
);
|
||||||
|
return useMatrixEditor(
|
||||||
|
load,
|
||||||
|
`e-${itemId}-${decisionSeq}`,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
(l) => approval.getJobTitle(l),
|
||||||
|
resolveApproverOrgInfo(context)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user