права на редактирование определенных полей
This commit is contained in:
@@ -70,6 +70,17 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [editId]);
|
}, [editId]);
|
||||||
|
|
||||||
|
// Новая заявка — дата расторжения по умолчанию сегодняшняя (типичный случай:
|
||||||
|
// обходной лист оформляют в день увольнения). При открытии на редактирование
|
||||||
|
// уже сохранённую дату не перезатираем.
|
||||||
|
useEffect(() => {
|
||||||
|
if (editId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today }));
|
||||||
|
}, [editId]);
|
||||||
|
|
||||||
// Должность/подразделение больше не вводятся вручную — подтягиваются из
|
// Должность/подразделение больше не вводятся вручную — подтягиваются из
|
||||||
// SharePoint User Profile (SPS-JobTitle/SPS-Department) при выборе сотрудника.
|
// SharePoint User Profile (SPS-JobTitle/SPS-Department) при выборе сотрудника.
|
||||||
// Если профиль не синхронизирован и поле там пусто — пока просто остаётся
|
// Если профиль не синхронизирован и поле там пусто — пока просто остаётся
|
||||||
|
|||||||
@@ -12,3 +12,35 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-bottom: $sp-4;
|
margin-bottom: $sp-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Полоса «вы можете заполнить свои отметки» для профильного согласующего
|
||||||
|
// (КДП / отдел подбора) — см. approverRights.ts.
|
||||||
|
.reviewBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-3;
|
||||||
|
margin-bottom: $sp-4;
|
||||||
|
padding: $sp-3 $sp-4;
|
||||||
|
border: 1px solid $gold-300;
|
||||||
|
border-radius: $r-md;
|
||||||
|
background: $gold-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewHint {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: $fs-sm;
|
||||||
|
color: $navy-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savedNote {
|
||||||
|
font-size: $fs-sm;
|
||||||
|
color: $green-600;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveError {
|
||||||
|
font-size: $fs-sm;
|
||||||
|
color: $red-500;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import styles from './ClearanceView.module.scss';
|
import styles from './ClearanceView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
|
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -10,11 +10,19 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId,
|
||||||
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadClearanceRequest } from './load';
|
import { loadClearanceRequest } from './load';
|
||||||
import { clearanceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import {
|
||||||
|
clearanceApprovalService,
|
||||||
|
updateApproverFields,
|
||||||
|
IApproverEditableFields,
|
||||||
|
DATA_LIST_TITLE,
|
||||||
|
APPROVAL_LIST_TITLE
|
||||||
|
} from './submit';
|
||||||
|
import { canEditHrRecordsFields, canEditParkingField } from './approverRights';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
|
||||||
interface IClearanceViewProps {
|
interface IClearanceViewProps {
|
||||||
@@ -24,18 +32,22 @@ interface IClearanceViewProps {
|
|||||||
formKey: string;
|
formKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
const NOOP_SET: SetField = () => undefined;
|
|
||||||
|
|
||||||
// Обходной лист только для просмотра (режим согласующего). Переиспользует ту
|
// Обходной лист в режиме согласующего. Форма read-only, КРОМЕ отметок, которые
|
||||||
// же секцию, что и форма подачи; редактирование выключено через
|
// заполняет профильный отдел на своём шаге: КДП — карта СКУД и сумма обучения,
|
||||||
// <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
|
// отдел подбора — парковочная карта (см. approverRights.ts).
|
||||||
const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey }) => {
|
const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey }) => {
|
||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [hrRecordsEditable, setHrRecordsEditable] = useState<boolean>(false);
|
||||||
|
const [parkingEditable, setParkingEditable] = useState<boolean>(false);
|
||||||
|
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||||
|
|
||||||
|
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
const approval = useMemo(() => clearanceApprovalService(), []);
|
const approval = useMemo(() => clearanceApprovalService(), []);
|
||||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||||
@@ -73,6 +85,55 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
};
|
};
|
||||||
}, [itemId]);
|
}, [itemId]);
|
||||||
|
|
||||||
|
// Права на отметки — по должности ТЕКУЩЕГО активного шага пользователя.
|
||||||
|
// isDelegator намеренно не даёт прав: делегировавший не решает за делегата
|
||||||
|
// (см. DecisionPanel), значит и отметки за него ставить не должен.
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
approverService
|
||||||
|
.getPendingRowForCurrentUser(itemId)
|
||||||
|
.then((pending) => {
|
||||||
|
if (!alive) return;
|
||||||
|
const active = !!pending && !pending.isDelegator && pending.phase !== APPROVAL_PHASE.execution;
|
||||||
|
setHrRecordsEditable(active && canEditHrRecordsFields(pending?.position));
|
||||||
|
setParkingEditable(active && canEditParkingField(pending?.position));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (alive) {
|
||||||
|
setHrRecordsEditable(false);
|
||||||
|
setParkingEditable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
// decisionSeq: после решения/делегирования активный шаг меняется — права пересчитываем.
|
||||||
|
}, [itemId, approverService, decisionSeq]);
|
||||||
|
|
||||||
|
// Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он
|
||||||
|
// затёр бы чужие поля значениями, которые видел у себя на экране.
|
||||||
|
const saveApproverFields = useCallback((): Promise<void> => {
|
||||||
|
const fields: Partial<IApproverEditableFields> = {};
|
||||||
|
if (hrRecordsEditable) {
|
||||||
|
fields.AccessCard = data.AccessCard;
|
||||||
|
fields.TrainingAmount = data.TrainingAmount;
|
||||||
|
}
|
||||||
|
if (parkingEditable) {
|
||||||
|
fields.ParkingCard = data.ParkingCard;
|
||||||
|
}
|
||||||
|
return updateApproverFields(itemId, fields);
|
||||||
|
}, [itemId, hrRecordsEditable, parkingEditable, data.AccessCard, data.TrainingAmount, data.ParkingCard]);
|
||||||
|
|
||||||
|
const handleSaveFields = async (): Promise<void> => {
|
||||||
|
setSaveState('saving');
|
||||||
|
try {
|
||||||
|
await saveApproverFields();
|
||||||
|
setSaveState('saved');
|
||||||
|
} catch {
|
||||||
|
setSaveState('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||||
}
|
}
|
||||||
@@ -84,7 +145,8 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionProps = { data, set: NOOP_SET, context };
|
const canEditAnything = hrRecordsEditable || parkingEditable;
|
||||||
|
const sectionProps = { data, set, context, hrRecordsEditable, parkingEditable };
|
||||||
|
|
||||||
const notify = (): void => {
|
const notify = (): void => {
|
||||||
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
|
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
|
||||||
@@ -104,6 +166,21 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
<PrintButton />
|
<PrintButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canEditAnything && (
|
||||||
|
<div className={`${styles.reviewBar} rp-no-print`}>
|
||||||
|
<span className={styles.reviewHint}>
|
||||||
|
{hrRecordsEditable
|
||||||
|
? 'Как согласующий вы можете отметить карту СКУД и указать сумму за обучение.'
|
||||||
|
: 'Как согласующий вы можете отметить парковочную карту.'}
|
||||||
|
</span>
|
||||||
|
<Button variant="secondary" onClick={handleSaveFields} disabled={saveState === 'saving'}>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
{saveState === 'saved' && <span className={styles.savedNote}>Сохранено</span>}
|
||||||
|
{saveState === 'error' && <span className={styles.saveError}>Ошибка сохранения</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Обходной лист"
|
title="Обходной лист"
|
||||||
@@ -125,6 +202,9 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
service={approverService}
|
service={approverService}
|
||||||
requestId={itemId}
|
requestId={itemId}
|
||||||
search={(q) => approval.searchUsers(q)}
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
// Отметки сохраняются вместе с решением — иначе согласующий, забывший
|
||||||
|
// нажать «Сохранить», согласовал бы заявку со старыми значениями.
|
||||||
|
onBeforeDecide={canEditAnything ? saveApproverFields : undefined}
|
||||||
onDecided={(newStatus) => {
|
onDecided={(newStatus) => {
|
||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Кто из согласующих какие поля обходного листа может править на своём шаге.
|
||||||
|
*
|
||||||
|
* Инициатор (HR) отправляет заявку с пустыми отметками — фактические данные знают
|
||||||
|
* профильные подразделения, каждое своё:
|
||||||
|
* - отдел кадрового делопроизводства — карта СКУД и сумма за обучение;
|
||||||
|
* - отдел подбора и адаптации персонала — парковочная карта.
|
||||||
|
*
|
||||||
|
* Должность берётся из строки согласования (колонка Title, см. IApprovalRow.position),
|
||||||
|
* то есть та, что подставил маршрут при планировании. Сравнение — по ключевым словам,
|
||||||
|
* а не по точной строке: формулировки в справочнике различаются («Начальник отдела…»,
|
||||||
|
* «Руководитель отдела…»), а тратить на это отдельную настройку в админке смысла нет.
|
||||||
|
*
|
||||||
|
* ВАЖНО про требование управленческого маркера (начальник/руководитель): без него
|
||||||
|
* права получили бы и рядовые сотрудники этих отделов («Специалист по направлению
|
||||||
|
* кадрового делопроизводства» в справочнике есть), оказавшиеся согласующими на любом
|
||||||
|
* другом шаге. Если понадобится отдать права и специалистам — убрать MANAGER_MARKERS
|
||||||
|
* из проверки.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MANAGER_MARKERS = ['начальник', 'руководитель'];
|
||||||
|
|
||||||
|
/** Отдел кадрового делопроизводства (КДП) — правит карту СКУД и сумму обучения. */
|
||||||
|
const HR_RECORDS_MARKERS = ['кадров', 'делопроизводств'];
|
||||||
|
|
||||||
|
/** Отдел подбора и адаптации персонала — правит парковочную карту. */
|
||||||
|
const RECRUITING_MARKERS = ['подбор', 'адаптац'];
|
||||||
|
|
||||||
|
function normalize(position: string | undefined): string {
|
||||||
|
return (position || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesAll(position: string, markers: string[]): boolean {
|
||||||
|
return markers.every((m) => position.indexOf(m) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManagerOf(position: string | undefined, departmentMarkers: string[]): boolean {
|
||||||
|
const normalized = normalize(position);
|
||||||
|
if (!normalized) return false;
|
||||||
|
return (
|
||||||
|
MANAGER_MARKERS.some((m) => normalized.indexOf(m) !== -1) && matchesAll(normalized, departmentMarkers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Карта СКУД и сумма обучения — начальник отдела кадрового делопроизводства. */
|
||||||
|
export function canEditHrRecordsFields(position: string | undefined): boolean {
|
||||||
|
return isManagerOf(position, HR_RECORDS_MARKERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Парковочная карта — начальник отдела подбора и адаптации персонала. */
|
||||||
|
export function canEditParkingField(position: string | undefined): boolean {
|
||||||
|
return isManagerOf(position, RECRUITING_MARKERS);
|
||||||
|
}
|
||||||
@@ -7,8 +7,22 @@ import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
|
|||||||
// свободный ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
// свободный ввод здесь намеренно убран, чтобы не расходиться с профилем.
|
||||||
const NOOP = (): void => undefined;
|
const NOOP = (): void => undefined;
|
||||||
|
|
||||||
|
interface IRequestSectionProps extends ISectionProps {
|
||||||
|
/** Согласующий из КДП правит карту СКУД и сумму обучения даже в режиме просмотра. */
|
||||||
|
hrRecordsEditable?: boolean;
|
||||||
|
/** Согласующий из отдела подбора правит парковочную карту. */
|
||||||
|
parkingEditable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** Единственная секция формы «Обходной лист»: увольняемый, должность, подразделение, даты, карты, обучение. */
|
/** Единственная секция формы «Обходной лист»: увольняемый, должность, подразделение, даты, карты, обучение. */
|
||||||
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange }) => (
|
export const RequestSection: React.FC<IRequestSectionProps> = ({
|
||||||
|
data,
|
||||||
|
set,
|
||||||
|
context,
|
||||||
|
onEmployeeChange,
|
||||||
|
hrRecordsEditable,
|
||||||
|
parkingEditable
|
||||||
|
}) => (
|
||||||
<CollapseSection title="Обходной лист" icon="people">
|
<CollapseSection title="Обходной лист" icon="people">
|
||||||
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
||||||
<PeopleField
|
<PeopleField
|
||||||
@@ -44,14 +58,31 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
|
|||||||
onChange={(v) => set('TerminationDate', v)}
|
onChange={(v) => set('TerminationDate', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{/* Явный readOnly={false} перебивает read-only контекст просмотра (профильный
|
||||||
|
согласующий правит своё поле); undefined — обычное поведение из FormMode. */}
|
||||||
<FormField label="Обучение: сумма" htmlFor="TrainingAmount">
|
<FormField label="Обучение: сумма" htmlFor="TrainingAmount">
|
||||||
<MoneyInput id="TrainingAmount" value={data.TrainingAmount} onChange={(v) => set('TrainingAmount', v)} />
|
<MoneyInput
|
||||||
|
id="TrainingAmount"
|
||||||
|
value={data.TrainingAmount}
|
||||||
|
onChange={(v) => set('TrainingAmount', v)}
|
||||||
|
readOnly={hrRecordsEditable ? false : undefined}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Парковочная карта">
|
<FormField label="Парковочная карта">
|
||||||
<Toggle checked={data.ParkingCard} onChange={(v) => set('ParkingCard', v)} onLabel="Да" />
|
<Toggle
|
||||||
|
checked={data.ParkingCard}
|
||||||
|
onChange={(v) => set('ParkingCard', v)}
|
||||||
|
onLabel="Да"
|
||||||
|
readOnly={parkingEditable ? false : undefined}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Карта СКУД">
|
<FormField label="Карта СКУД">
|
||||||
<Toggle checked={data.AccessCard} onChange={(v) => set('AccessCard', v)} onLabel="Да" />
|
<Toggle
|
||||||
|
checked={data.AccessCard}
|
||||||
|
onChange={(v) => set('AccessCard', v)}
|
||||||
|
onLabel="Да"
|
||||||
|
readOnly={hrRecordsEditable ? false : undefined}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,34 @@ export function clearanceApprovalService(): ApprovalListService {
|
|||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Отметки, которые проставляет не инициатор, а профильный согласующий (см. approverRights.ts). */
|
||||||
|
export interface IApproverEditableFields {
|
||||||
|
AccessCard: boolean;
|
||||||
|
TrainingAmount: string;
|
||||||
|
ParkingCard: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить только те отметки, что заполняет согласующий на своём шаге.
|
||||||
|
* Пишутся ровно переданные поля: у каждого отдела свой набор прав, и лишние
|
||||||
|
* ключи затёрли бы чужие отметки значениями из формы этого согласующего.
|
||||||
|
*/
|
||||||
|
export async function updateApproverFields(
|
||||||
|
id: number,
|
||||||
|
fields: Partial<IApproverEditableFields>
|
||||||
|
): Promise<void> {
|
||||||
|
const item: Record<string, unknown> = {};
|
||||||
|
if (fields.AccessCard !== undefined) item.AccessCard = fields.AccessCard;
|
||||||
|
if (fields.ParkingCard !== undefined) item.ParkingCard = fields.ParkingCard;
|
||||||
|
if (fields.TrainingAmount !== undefined) {
|
||||||
|
const amount = Number(fields.TrainingAmount);
|
||||||
|
item.TrainingAmount =
|
||||||
|
Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null;
|
||||||
|
}
|
||||||
|
if (Object.keys(item).length === 0) return;
|
||||||
|
await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update(item);
|
||||||
|
}
|
||||||
|
|
||||||
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
||||||
async function ensureEmployee(login: string): Promise<{ Id: number; Title: string }> {
|
async function ensureEmployee(login: string): Promise<{ Id: number; Title: string }> {
|
||||||
const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`;
|
const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`;
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export class ApprovalListService {
|
|||||||
.items.filter(`${FIELDS.ticket}Id eq ${requestId}`)
|
.items.filter(`${FIELDS.ticket}Id eq ${requestId}`)
|
||||||
.select(
|
.select(
|
||||||
'Id',
|
'Id',
|
||||||
|
'Title',
|
||||||
FIELDS.status,
|
FIELDS.status,
|
||||||
FIELDS.order,
|
FIELDS.order,
|
||||||
FIELDS.allowReassign,
|
FIELDS.allowReassign,
|
||||||
@@ -224,6 +225,7 @@ export class ApprovalListService {
|
|||||||
approverId: approver?.Id ?? 0,
|
approverId: approver?.Id ?? 0,
|
||||||
order: (r[FIELDS.order] as number) ?? 0,
|
order: (r[FIELDS.order] as number) ?? 0,
|
||||||
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
||||||
|
position: (r.Title as string) || '',
|
||||||
// Пустая фаза (старые строки без колонки) трактуется как «Согласование».
|
// Пустая фаза (старые строки без колонки) трактуется как «Согласование».
|
||||||
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
||||||
delegatedById: delegatedBy?.Id ?? 0,
|
delegatedById: delegatedBy?.Id ?? 0,
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export interface IApprovalRow {
|
|||||||
approverId: number; // Id пользователя сайта в поле «Согласующий»
|
approverId: number; // Id пользователя сайта в поле «Согласующий»
|
||||||
order: number; // колонка Order (очередь согласования)
|
order: number; // колонка Order (очередь согласования)
|
||||||
allowReassign: boolean; // AllowReassignToOtherApprovers
|
allowReassign: boolean; // AllowReassignToOtherApprovers
|
||||||
|
/** Должность/роль шага — колонка Title строки (см. createSteps: `a.position || a.email`).
|
||||||
|
* Нужна формам, где ПРАВА согласующего зависят от его должности (напр. «Обходной
|
||||||
|
* лист»: КДП правит СКУД и сумму обучения, подбор — парковочную карту). */
|
||||||
|
position: string;
|
||||||
phase: string; // Phase: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
phase: string; // Phase: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
||||||
/** Id пользователя сайта, который делегировал ЭТУ строку (DelegatedBy). 0 — строка не делегирована
|
/** Id пользователя сайта, который делегировал ЭТУ строку (DelegatedBy). 0 — строка не делегирована
|
||||||
* (пришла из маршрута напрямую). Позволяет делегировавшему переназначить шаг ещё раз,
|
* (пришла из маршрута напрямую). Позволяет делегировавшему переназначить шаг ещё раз,
|
||||||
|
|||||||
Reference in New Issue
Block a user