Разные фиксы #8

Merged
gorbenko.pavel merged 8 commits from fix-webpart-properties into main 2026-08-18 08:45:42 +00:00
4 changed files with 95 additions and 14 deletions
Showing only changes of commit 5ad90bb7cb - Show all commits
@@ -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 };
}