обходной лист на исполнеии
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
|
APPROVAL_PHASE,
|
||||||
findUnassignedStep
|
findUnassignedStep
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
@@ -106,17 +107,42 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval
|
||||||
|
.getMatrixForRequest(editId)
|
||||||
|
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
||||||
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, [
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, [
|
||||||
{ fieldName: 'Position', value: data.Position }
|
{ fieldName: 'Position', value: data.Position }
|
||||||
]),
|
]),
|
||||||
[context, approval, editId, data.Employee, data.Position, requestTypeId]
|
[context, approval, editId, data.Employee, data.Position, requestTypeId]
|
||||||
);
|
);
|
||||||
|
const matrixReady = !!editId || (!!data.Employee && !!requestTypeId);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
`${data.Employee}-${editId ?? 'new'}-${data.Position}`,
|
`${data.Employee}-${editId ?? 'new'}-${data.Position}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
!!editId || (!!data.Employee && !!requestTypeId),
|
matrixReady,
|
||||||
|
(login) => approval.getJobTitle(login)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Фаза исполнения: тот же маршрут (условие «IsManager» по data.Position — см.
|
||||||
|
// комментарий у loadMatrix выше), только 'Execution' вместо 'Approval'. У
|
||||||
|
// открытой на редактирование заявки — строки уже записаны, читаем как есть.
|
||||||
|
const loadExecutors = useCallback(
|
||||||
|
() =>
|
||||||
|
editId
|
||||||
|
? approval
|
||||||
|
.getMatrixForRequest(editId)
|
||||||
|
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
||||||
|
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Execution', null, [
|
||||||
|
{ fieldName: 'Position', value: data.Position }
|
||||||
|
]),
|
||||||
|
[context, approval, editId, data.Employee, data.Position, requestTypeId]
|
||||||
|
);
|
||||||
|
const executors = useMatrixEditor(
|
||||||
|
loadExecutors,
|
||||||
|
`e-${data.Employee}-${editId ?? 'new'}-${data.Position}`,
|
||||||
|
!readOnly,
|
||||||
|
matrixReady,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,7 +174,14 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
if (!rtId) {
|
if (!rtId) {
|
||||||
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
||||||
}
|
}
|
||||||
const { id, code } = await saveClearanceRequest(requestId, rtId, data, toApprovers(matrix.rows), status);
|
const { id, code } = await saveClearanceRequest(
|
||||||
|
requestId,
|
||||||
|
rtId,
|
||||||
|
data,
|
||||||
|
toApprovers(matrix.rows),
|
||||||
|
toApprovers(executors.rows),
|
||||||
|
status
|
||||||
|
);
|
||||||
setRequestId(id);
|
setRequestId(id);
|
||||||
setRequestStatus(status);
|
setRequestStatus(status);
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -179,6 +212,10 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
if (unassigned) {
|
if (unassigned) {
|
||||||
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`;
|
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`;
|
||||||
}
|
}
|
||||||
|
const unassignedExecutor = findUnassignedStep(executors.rows);
|
||||||
|
if (unassignedExecutor) {
|
||||||
|
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`;
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,6 +257,18 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
{/* Блок исполнителя показываем, только когда по маршруту реально есть
|
||||||
|
исполнители (условие «IsManager» и т.п. могло не назначить ни одного). */}
|
||||||
|
{matrixReady && !executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение (после согласования)"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не заданы по маршруту."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
|||||||
@@ -55,13 +55,31 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
[approval]
|
[approval]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only), раздельно по
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// фазам: согласование и исполнение — два отдельных блока в UI (см. ниже).
|
||||||
|
// decisionSeq в 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((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution)),
|
||||||
|
[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 loadExecutors = useCallback(
|
||||||
|
() =>
|
||||||
|
approval
|
||||||
|
.getMatrixForRequest(itemId)
|
||||||
|
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution)),
|
||||||
|
[approval, itemId]
|
||||||
|
);
|
||||||
|
const executors = useMatrixEditor(loadExecutors, `e-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||||
|
approval.getJobTitle(l)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -196,6 +214,16 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не назначены."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
|||||||
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField } from '../../../shared/ui';
|
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField } from '../../../shared/ui';
|
||||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
|
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
|
||||||
|
|
||||||
// Должность/подразделение подтягиваются автоматически при выборе сотрудника
|
// Подразделение подтягивается автоматически при выборе сотрудника (см.
|
||||||
// (см. ClearanceForm.handleEmployeeChange — SharePoint User Profile) —
|
// ClearanceForm.handleEmployeeChange — SharePoint User Profile) — свободный
|
||||||
// свободный ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
// ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
||||||
const NOOP = (): void => undefined;
|
const NOOP = (): void => undefined;
|
||||||
|
|
||||||
interface IRequestSectionProps extends ISectionProps {
|
interface IRequestSectionProps extends ISectionProps {
|
||||||
@@ -32,12 +32,16 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
placeholder={EMPLOYEE_PLACEHOLDER}
|
placeholder={EMPLOYEE_PLACEHOLDER}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{/* Подтягивается из профиля при выборе сотрудника, но поле остаётся
|
||||||
|
редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст),
|
||||||
|
HR должен суметь ввести должность вручную — иначе маршрут
|
||||||
|
согласования (условие «IsManager» резолвится именно по ней) не
|
||||||
|
соберётся, и список исполнителей ниже останется пустым. */}
|
||||||
<FormField label="Должность" htmlFor="Position" required>
|
<FormField label="Должность" htmlFor="Position" required>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="Position"
|
id="Position"
|
||||||
value={data.Position}
|
value={data.Position}
|
||||||
onChange={NOOP}
|
onChange={(v) => set('Position', v)}
|
||||||
readOnly
|
|
||||||
placeholder="Подставится автоматически по выбранному сотруднику"
|
placeholder="Подставится автоматически по выбранному сотруднику"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|||||||
@@ -76,9 +76,8 @@ export interface ISaveResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить обходной лист: содержимое + маршрут согласования.
|
* Сохранить обходной лист: содержимое + маршрут (согласование + исполнение).
|
||||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||||
* Фазы исполнения у обходного листа нет — writeRoute вызывается с пустым списком исполнителей.
|
|
||||||
* Возвращает Id заявки (+ код при создании).
|
* Возвращает Id заявки (+ код при создании).
|
||||||
*/
|
*/
|
||||||
export async function saveClearanceRequest(
|
export async function saveClearanceRequest(
|
||||||
@@ -86,6 +85,7 @@ export async function saveClearanceRequest(
|
|||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
@@ -105,6 +105,6 @@ export async function saveClearanceRequest(
|
|||||||
await list.items.getById(id).update({ RequestUniqueCode: code });
|
await list.items.getById(id).update({ RequestUniqueCode: code });
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeRoute(clearanceApprovalService(), id, approvers, [], status);
|
await writeRoute(clearanceApprovalService(), id, approvers, executors, status);
|
||||||
return { id, code };
|
return { id, code };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user