feat: редакторы маршрута добавляют и удаляют исполнителей при составлении заявки
canEditExecutors: те же люди, что в canEditRoute (в т.ч. gorbenko.pavel), могут править матрицу «Исполнение» во всех формах с фазой исполнения; блок виден им и пустым. Остальным исполнители по-прежнему только для чтения. Реестр особых прав обновлён. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|---|---|---|---|---|
|
||||
| Удалять согласующих, которые ещё ждут очереди | Алтынай Бултурукова, Павел Горбенко | email в коде | Подбор, Приём, Реферальная рекомендация: и при составлении, и в поданной заявке | `shared/approval/routeEditRights.ts` → `canEditRoute` |
|
||||
| Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
|
||||
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
||||
| Править **все поля** заявки на своём активном шаге согласования (без вложений) | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
||||
|
||||
@@ -191,6 +191,7 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители — из маршрута админки (заявитель их не правит); те же параметры
|
||||
// резолва, что и у согласующих.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -181,6 +181,7 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители самого заявления (фаза «Исполнение» маршрута из админки) — по
|
||||
// тому же сотруднику и тем же fieldValues, что и согласующие выше.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -175,6 +175,7 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители — по тому же сотруднику, что и маршрут согласования. Только чтение:
|
||||
// их назначает маршрут из админки, не заявитель.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
canEditExecutors,
|
||||
getPlannedMatrix,
|
||||
resolveWriteListId,
|
||||
notifyActiveStep,
|
||||
@@ -204,7 +205,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
matrixReady,
|
||||
(login) => approval.getJobTitle(login),
|
||||
resolveApproverOrgInfo(context)
|
||||
@@ -460,12 +461,13 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
||||
/>
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
canEditExecutors,
|
||||
getPlannedMatrix,
|
||||
resolveWriteListId,
|
||||
notifyActiveStep,
|
||||
@@ -233,7 +234,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
matrixReady,
|
||||
(login) => approval.getJobTitle(login),
|
||||
resolveApproverOrgInfo(context)
|
||||
@@ -450,12 +451,13 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
||||
/>
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
canEditExecutors,
|
||||
getPlannedMatrix,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId,
|
||||
@@ -240,7 +241,7 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${data.Referrer}-${editId ?? 'new'}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
!!editId || (!!data.Referrer && !!requestTypeId),
|
||||
(login) => approval.getJobTitle(login),
|
||||
resolveApproverOrgInfo(context)
|
||||
@@ -469,12 +470,13 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
|
||||
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
||||
/>
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title={noApprovalPhase ? 'Исполнение' : 'Исполнение (после согласования)'}
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -196,6 +196,7 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители резолвятся тем же маршрутом и с тем же проектом, что и согласующие:
|
||||
// пока проект не выбран, e-mail пустой — useFormExecutors не грузит маршрут (404).
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import {
|
||||
composeRemoveRowRule,
|
||||
ApprovalMatrix,
|
||||
canEditExecutors,
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
notifyActiveStep,
|
||||
@@ -136,7 +137,8 @@ const SkudExplanationForm: React.FC<IRequestFormProps> = ({
|
||||
requestId: editId,
|
||||
employeeEmail: data.Employee,
|
||||
requestTypeId,
|
||||
editable: !readOnly
|
||||
editable: !readOnly,
|
||||
executorsEditable: canEditExecutors(context, readOnly)
|
||||
});
|
||||
|
||||
const validate = (): string | undefined => {
|
||||
@@ -276,12 +278,13 @@ const SkudExplanationForm: React.FC<IRequestFormProps> = ({
|
||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||
/>
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ type MatrixEditor = ReturnType<typeof useMatrixEditor>;
|
||||
export interface ISkudRoute {
|
||||
/** Согласующие — редактируемы заявителем в новой заявке / черновике / доработке. */
|
||||
approvers: MatrixEditor;
|
||||
/** Исполнители по маршруту (фаза «Исполнение») — только чтение. */
|
||||
/** Исполнители по маршруту (фаза «Исполнение») — только чтение, кроме редактора маршрута. */
|
||||
executors: MatrixEditor;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ interface IRouteSource {
|
||||
requestTypeId: number | undefined;
|
||||
/** Можно ли править согласующих. */
|
||||
editable: boolean;
|
||||
/** Можно ли править исполнителей (только редактору маршрута, см. canEditExecutors). */
|
||||
executorsEditable?: boolean;
|
||||
/** Меняется после решения согласующего — перечитать строки. */
|
||||
reloadSeq?: number;
|
||||
}
|
||||
@@ -40,7 +42,7 @@ const byPhase = (execution: boolean) => (rows: IMatrixRow[]): IMatrixRow[] =>
|
||||
* маршрута «пропадал» бы. Паттерн тот же, что в Referral.
|
||||
*/
|
||||
export function useSkudRoute(source: IRouteSource): ISkudRoute {
|
||||
const { context, approval, requestId, employeeEmail, requestTypeId, editable, reloadSeq = 0 } = source;
|
||||
const { context, approval, requestId, employeeEmail, requestTypeId, editable, executorsEditable = false, reloadSeq = 0 } = source;
|
||||
const canLoad = !!requestId || (!!employeeEmail && !!requestTypeId);
|
||||
const reloadKey = `${employeeEmail}-${requestId ?? 'new'}-${reloadSeq}`;
|
||||
|
||||
@@ -62,6 +64,6 @@ export function useSkudRoute(source: IRouteSource): ISkudRoute {
|
||||
const jobTitle = (login: string): Promise<string> => approval.getJobTitle(login);
|
||||
|
||||
const approvers = useMatrixEditor(loadApprovers, reloadKey, editable, canLoad, jobTitle, resolveApproverOrgInfo(context));
|
||||
const executors = useMatrixEditor(loadExecutors, reloadKey, false, canLoad, jobTitle, resolveApproverOrgInfo(context));
|
||||
const executors = useMatrixEditor(loadExecutors, reloadKey, executorsEditable, canLoad, jobTitle, resolveApproverOrgInfo(context));
|
||||
return { approvers, executors };
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители (фаза «Исполнение») резолвятся с теми же fieldValues, что и
|
||||
// согласующие: условные шаги маршрута зависят от вида работ и отдела.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
canEditExecutors,
|
||||
getPlannedMatrix,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId,
|
||||
@@ -314,7 +315,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${userEmail}-${editId ?? 'new'}-${projectId}-${targetProjectId}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
executorsReady,
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
@@ -507,12 +508,13 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
{/* Блок исполнителя показываем, только когда по маршруту реально есть
|
||||
исполнители (грузится по проектам, см. executorsReady). */}
|
||||
{executorsReady && !executors.loading && executors.rows.length > 0 && (
|
||||
{executorsReady && !executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -175,6 +175,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
// Исполнители фазы «Исполнение» — по маршруту из админки, только чтение.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -177,6 +177,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
||||
// Исполнители из маршрута админки (фаза «Исполнение»): только чтение, таблица
|
||||
// и запись в заявку — когда они в маршруте есть.
|
||||
const executors = useFormExecutors({
|
||||
readOnly,
|
||||
context,
|
||||
approval,
|
||||
requestTypeId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ApprovalHistory,
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
canEditExecutors,
|
||||
getPlannedMatrix,
|
||||
resolveWriteListId,
|
||||
notifyActiveStep,
|
||||
@@ -213,7 +214,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${userEmail}-${editId ?? 'new'}-${data.DepartmentId}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
!!editId || !!requestTypeId,
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
@@ -398,12 +399,13 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||
/>
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение (после согласования)"
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не заданы по маршруту."
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -72,7 +72,7 @@ export {
|
||||
} from './useRouteExecutors';
|
||||
export { resolveApproverOrgInfo } from './approverOrgInfo';
|
||||
export type { IApproverOrgInfo } from './approverOrgInfo';
|
||||
export { canEditRoute, composeRemoveRowRule } from './routeEditRights';
|
||||
export { canEditRoute, canEditExecutors, composeRemoveRowRule } from './routeEditRights';
|
||||
export { canEditAllFields } from './fullEditRights';
|
||||
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
||||
export { ExecutorsMatrix } from './ui/ExecutorsMatrix';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { effectiveUserEmail } from '../impersonation';
|
||||
|
||||
/**
|
||||
* Кто может удалять согласующих из маршрута (и при составлении заявки, и на
|
||||
* своём шаге согласования) — сверх обычных прав инициатора/согласующего.
|
||||
@@ -30,3 +33,15 @@ export function canEditRoute(email: string | undefined): boolean {
|
||||
export function composeRemoveRowRule(email: string | undefined): (() => boolean) | undefined {
|
||||
return canEditRoute(email) ? () => true : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Может ли текущий пользователь править состав ИСПОЛНИТЕЛЕЙ (добавлять и удалять)
|
||||
* при составлении заявки: те же редакторы маршрута, что и для согласующих.
|
||||
* Исполнителей по умолчанию назначает маршрут из админки, заявителю они только
|
||||
* показываются; редактору маршрута матрица «Исполнение» становится редактируемой.
|
||||
* Учитывает подмену личности (effectiveUserEmail). readOnly — заявка уже не
|
||||
* редактируется (отправлена), тогда править нельзя никому.
|
||||
*/
|
||||
export function canEditExecutors(context: WebPartContext, readOnly: boolean): boolean {
|
||||
return !readOnly && canEditRoute(effectiveUserEmail(context));
|
||||
}
|
||||
|
||||
@@ -12,16 +12,18 @@ interface IExecutorsMatrixProps {
|
||||
|
||||
/**
|
||||
* Таблица исполнителей — рисуется только когда в маршруте из админки они есть:
|
||||
* у форм без фазы исполнения блока нет вовсе.
|
||||
* у форм без фазы исполнения блока нет вовсе (кроме редактора маршрута, см. canEditExecutors).
|
||||
*/
|
||||
export const ExecutorsMatrix: React.FC<IExecutorsMatrixProps> = ({ editor, search, title = 'Исполнение' }) => {
|
||||
if (editor.loading || editor.rows.length === 0) return null;
|
||||
// Редактор маршрута видит блок и пустым — иначе исполнителя некуда добавить.
|
||||
if (editor.loading || (editor.rows.length === 0 && !editor.editable)) return null;
|
||||
return (
|
||||
<ApprovalMatrix
|
||||
editor={editor}
|
||||
search={search}
|
||||
title={title}
|
||||
roleLabel="Исполнитель"
|
||||
pickerPlaceholder="Добавить исполнителя: начните вводить ФИО…"
|
||||
emptyText="Исполнители не назначены."
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { resolveApproverOrgInfo } from './approverOrgInfo';
|
||||
import { IResolveFieldValue } from './models/IApprovalRouteApi';
|
||||
import { IMatrixRow } from './models/IMatrixRow';
|
||||
import { APPROVAL_PHASE } from './models/statuses';
|
||||
import { canEditExecutors } from './routeEditRights';
|
||||
import { IUseMatrixEditor, useMatrixEditor } from './useMatrixEditor';
|
||||
|
||||
/** Строки фазы согласования (всё, что не «Исполнение») — для таблицы согласующих. */
|
||||
@@ -46,6 +47,8 @@ interface IFormExecutorsSource {
|
||||
departmentId?: string | null; // eslint-disable-line @rushstack/no-new-null -- транзитом в resolveRoute
|
||||
fieldValues?: IResolveFieldValue[];
|
||||
projectId?: string | null; // eslint-disable-line @rushstack/no-new-null -- транзитом в resolveRoute
|
||||
/** Заявка не редактируется — правка состава исполнителей закрыта для всех. */
|
||||
readOnly?: boolean;
|
||||
/** Дополнительный ключ перезагрузки (значение поля, влияющее на маршрут). */
|
||||
reloadSuffix?: string;
|
||||
}
|
||||
@@ -53,10 +56,11 @@ interface IFormExecutorsSource {
|
||||
/**
|
||||
* Исполнители формы заполнения (фаза «Исполнение»): планируемые по маршруту из
|
||||
* админки для новой заявки, из записанных строк — для черновика/доработки.
|
||||
* Только чтение: исполнителя назначает маршрут, не заявитель.
|
||||
* Заявителю — только чтение: исполнителя назначает маршрут. Редактор маршрута
|
||||
* (см. canEditExecutors) может добавлять и удалять исполнителей.
|
||||
*/
|
||||
export function useFormExecutors(source: IFormExecutorsSource): IUseMatrixEditor {
|
||||
const { context, approval, requestTypeId, userEmail, editId, departmentId, fieldValues, projectId, reloadSuffix = '' } = source;
|
||||
const { context, approval, requestTypeId, userEmail, editId, departmentId, fieldValues, projectId, reloadSuffix = '', readOnly = false } = source;
|
||||
const fieldValuesKey = JSON.stringify(fieldValues ?? []);
|
||||
const load = useCallback(
|
||||
() =>
|
||||
@@ -70,7 +74,7 @@ export function useFormExecutors(source: IFormExecutorsSource): IUseMatrixEditor
|
||||
return useMatrixEditor(
|
||||
load,
|
||||
`x-${userEmail}-${editId ?? 'new'}-${departmentId ?? ''}-${projectId ?? ''}-${fieldValuesKey}-${reloadSuffix}`,
|
||||
false,
|
||||
canEditExecutors(context, readOnly),
|
||||
!!editId || (!!userEmail && !!requestTypeId),
|
||||
(login) => approval.getJobTitle(login),
|
||||
resolveApproverOrgInfo(context)
|
||||
|
||||
Reference in New Issue
Block a user