Разные фиксы #8
@@ -21,6 +21,7 @@ import {
|
||||
REQUEST_STATUS,
|
||||
isEditableStatus,
|
||||
IApproverToWrite,
|
||||
APPROVAL_PHASE,
|
||||
findUnassignedStep
|
||||
} from '../../shared/approval';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
@@ -106,17 +107,42 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
||||
const loadMatrix = useCallback(
|
||||
() =>
|
||||
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, [
|
||||
{ fieldName: 'Position', value: data.Position }
|
||||
]),
|
||||
[context, approval, editId, data.Employee, data.Position, requestTypeId]
|
||||
);
|
||||
const matrixReady = !!editId || (!!data.Employee && !!requestTypeId);
|
||||
const matrix = useMatrixEditor(
|
||||
loadMatrix,
|
||||
`${data.Employee}-${editId ?? 'new'}-${data.Position}`,
|
||||
!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)
|
||||
);
|
||||
|
||||
@@ -148,7 +174,14 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
||||
if (!rtId) {
|
||||
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);
|
||||
setRequestStatus(status);
|
||||
if (code) {
|
||||
@@ -179,6 +212,10 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
||||
if (unassigned) {
|
||||
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`;
|
||||
}
|
||||
const unassignedExecutor = findUnassignedStep(executors.rows);
|
||||
if (unassignedExecutor) {
|
||||
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -220,6 +257,18 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
||||
|
||||
<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
|
||||
submit={submit}
|
||||
isValid={isValid}
|
||||
|
||||
@@ -55,13 +55,31 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
[approval]
|
||||
);
|
||||
|
||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||
// Матрица маршрута из фактических строк заявки (read-only), раздельно по
|
||||
// фазам: согласование и исполнение — два отдельных блока в UI (см. ниже).
|
||||
// decisionSeq в reloadKey: после решения строки на сервере меняются —
|
||||
// перечитываем матрицу.
|
||||
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) =>
|
||||
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(() => {
|
||||
let alive = true;
|
||||
@@ -196,6 +214,16 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
||||
</FormCard>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField } from '../../../shared/ui';
|
||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
|
||||
|
||||
// Должность/подразделение подтягиваются автоматически при выборе сотрудника
|
||||
// (см. ClearanceForm.handleEmployeeChange — SharePoint User Profile) —
|
||||
// свободный ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
||||
// Подразделение подтягивается автоматически при выборе сотрудника (см.
|
||||
// ClearanceForm.handleEmployeeChange — SharePoint User Profile) — свободный
|
||||
// ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
||||
const NOOP = (): void => undefined;
|
||||
|
||||
interface IRequestSectionProps extends ISectionProps {
|
||||
@@ -32,12 +32,16 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
||||
placeholder={EMPLOYEE_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
{/* Подтягивается из профиля при выборе сотрудника, но поле остаётся
|
||||
редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст),
|
||||
HR должен суметь ввести должность вручную — иначе маршрут
|
||||
согласования (условие «IsManager» резолвится именно по ней) не
|
||||
соберётся, и список исполнителей ниже останется пустым. */}
|
||||
<FormField label="Должность" htmlFor="Position" required>
|
||||
<TextInput
|
||||
id="Position"
|
||||
value={data.Position}
|
||||
onChange={NOOP}
|
||||
readOnly
|
||||
onChange={(v) => set('Position', v)}
|
||||
placeholder="Подставится автоматически по выбранному сотруднику"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -76,9 +76,8 @@ export interface ISaveResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить обходной лист: содержимое + маршрут согласования.
|
||||
* Сохранить обходной лист: содержимое + маршрут (согласование + исполнение).
|
||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||
* Фазы исполнения у обходного листа нет — writeRoute вызывается с пустым списком исполнителей.
|
||||
* Возвращает Id заявки (+ код при создании).
|
||||
*/
|
||||
export async function saveClearanceRequest(
|
||||
@@ -86,6 +85,7 @@ export async function saveClearanceRequest(
|
||||
requestTypeId: number,
|
||||
data: IFormData,
|
||||
approvers: IApproverToWrite[],
|
||||
executors: IApproverToWrite[],
|
||||
status: string
|
||||
): Promise<ISaveResult> {
|
||||
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 writeRoute(clearanceApprovalService(), id, approvers, [], status);
|
||||
await writeRoute(clearanceApprovalService(), id, approvers, executors, status);
|
||||
return { id, code };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user