feat: согласованное заявление на увольнение само создаёт обходной лист
Как только маршрут заявления пройден целиком (статус «Согласовано»), портал заводит обходной лист по нему: форма не открывается — маршрут резолвится через approval-route (обе фазы, по Employee и Position заявления), элемент пишется в текущую партицию списка «Обходной лист … — данные» и сразу уходит на согласование с оповещением активного шага. Поля выводятся из заявления (сотрудник, должность, подразделение, организация, последний рабочий день → дата расторжения договора); отметки парковки/СКУД/обучения остаются профильным согласующим. Пара связывается перекрёстными гиперссылками ClearanceUrl / DismissalUrl — для них добавлен requestLinkUrl: абсолютная ссылка на заявку без посторонних параметров страницы, в отличие от routeUrl. Повторное создание отсекается непустой ClearanceUrl: заявление можно согласовать второй раз после возврата на доработку, и без этой проверки на каждый цикл появлялся бы новый обходной лист. Создание идёт в браузере последнего согласующего — если оно упало, согласование не отменяется, а портал показывает предупреждение «заведите вручную». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -46,7 +46,7 @@
|
|||||||
| [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ |
|
| [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ |
|
||||||
| [`it-1c-ticket`](../src/webparts/requestPortal/forms/Ticket1C/README.md) | Заявка в отдел 1С | IT | да | нет | да | контакты предзаполняются из Graph (/me); вместо фазы исполнения — карточка в Microsoft Planner (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ |
|
| [`it-1c-ticket`](../src/webparts/requestPortal/forms/Ticket1C/README.md) | Заявка в отдел 1С | IT | да | нет | да | контакты предзаполняются из Graph (/me); вместо фазы исполнения — карточка в Microsoft Planner (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ |
|
||||||
| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker); условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД), сумма обучения | ✅ |
|
| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker); условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД), сумма обучения | ✅ |
|
||||||
| [`hr-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | по умолчанию на себя, но можно за другого; подразделение из профиля (read-only), должность/организация из справочника; последний рабочий день не в прошлом | ⏳ тип заявки не заведён в админке approval-route |
|
| [`hr-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | по умолчанию на себя, но можно за другого; подразделение из профиля (read-only), должность/организация из справочника; последний рабочий день не в прошлом; **после согласования сам создаёт обходной лист** и связывает пару ссылками | ⏳ тип заявки не заведён в админке approval-route |
|
||||||
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
||||||
|
|
||||||
Легенда: **Согл.** — есть фаза согласования; **Исп.** — есть фаза исполнения;
|
Легенда: **Согл.** — есть фаза согласования; **Исп.** — есть фаза исполнения;
|
||||||
|
|||||||
@@ -40,10 +40,17 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова
|
|||||||
- **Заголовок элемента** (`Title`): `Обходной лист — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Обходной лист — {ФИО} — дд.мм.гггг`.
|
||||||
|
|
||||||
## Связь с заявлением на увольнение
|
## Связь с заявлением на увольнение
|
||||||
В списке данных есть колонка-гиперссылка `DismissalUrl` — ссылка на заявление на
|
Обходной лист может появиться **сам, без этой формы**: когда согласовано «Заявление
|
||||||
увольнение (обратная ей `ClearanceUrl` — в списке заявления). Колонки существуют
|
на увольнение» (`hr-dismissal`), портал создаёт лист по нему и сразу отправляет на
|
||||||
только в списках: форма их не пишет и не показывает, ссылка ставится вручную в
|
согласование — см. `forms/Dismissal/createClearance.ts` и README той формы. Такой
|
||||||
SharePoint. См. `forms/Dismissal/README.md`.
|
лист приходит уже с заполненными сотрудником, должностью, подразделением и датой
|
||||||
|
расторжения; отметки (парковка, СКУД, обучение) по-прежнему проставляют профильные
|
||||||
|
согласующие. Подача этой формы руками остаётся — для увольнений, оформленных без
|
||||||
|
заявления в портале.
|
||||||
|
|
||||||
|
Пара связана колонками-гиперссылками: `DismissalUrl` здесь → на заявление,
|
||||||
|
`ClearanceUrl` в заявлении → на лист. Форма их не показывает и не пишет — их
|
||||||
|
заполняет автосоздание (или вручную в SharePoint).
|
||||||
|
|
||||||
## Секции формы
|
## Секции формы
|
||||||
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма обучения · парковочная карта · карта СКУД)
|
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма обучения · парковочная карта · карта СКУД)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId,
|
||||||
|
REQUEST_STATUS
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadDismissalRequest } from './load';
|
import { loadDismissalRequest } from './load';
|
||||||
import { dismissalApprovalService } from './submit';
|
import { dismissalApprovalService } from './submit';
|
||||||
|
import { ensureClearanceForDismissal } from './createClearance';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
|
||||||
interface IDismissalViewProps {
|
interface IDismissalViewProps {
|
||||||
@@ -45,6 +47,9 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
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);
|
||||||
|
// Итог автосоздания обходного листа после согласования (см. onDecided ниже).
|
||||||
|
const [clearance, setClearance] = useState<string>('');
|
||||||
|
const [clearanceError, setClearanceError] = useState<string>('');
|
||||||
|
|
||||||
// listId уже проверен в ApproveFeature (не смонтировались бы сюда иначе) —
|
// listId уже проверен в ApproveFeature (не смонтировались бы сюда иначе) —
|
||||||
// dismissalApprovalService можно звать не опасаясь исключения.
|
// dismissalApprovalService можно звать не опасаясь исключения.
|
||||||
@@ -112,12 +117,38 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Заявление согласовано целиком → сразу заводим обходной лист по нему (данные
|
||||||
|
// выводятся из заявления, заполнять его руками нечего — см. createClearance.ts).
|
||||||
|
// Свой статус решения при этом уже применён: неудача автосоздания не отменяет
|
||||||
|
// согласование, поэтому она показывается предупреждением, а не ошибкой заявки.
|
||||||
|
const createClearance = (): void => {
|
||||||
|
setClearanceError('');
|
||||||
|
ensureClearanceForDismissal(context, itemId, listId)
|
||||||
|
.then((result) =>
|
||||||
|
setClearance(
|
||||||
|
result.existing
|
||||||
|
? 'Обходной лист по этому заявлению уже был создан.'
|
||||||
|
: `Обходной лист создан автоматически${result.code ? ` — №${result.code}` : ''} и отправлен на согласование.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch((e) =>
|
||||||
|
setClearanceError(
|
||||||
|
`Заявление согласовано, но обходной лист создать не удалось: ${friendlyErrorMessage(e)} — заведите его вручную.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintButton />
|
<PrintButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rp-no-print">
|
||||||
|
{clearance && <Alert variant="ok">{clearance}</Alert>}
|
||||||
|
{clearanceError && <Alert variant="err">{clearanceError}</Alert>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявление на увольнение"
|
title="Заявление на увольнение"
|
||||||
@@ -148,6 +179,12 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
notify();
|
notify();
|
||||||
|
// «Согласовано» = маршрут заявления пройден целиком (фазы исполнения у
|
||||||
|
// него нет, см. README формы) — значит увольнение подтверждено и пора
|
||||||
|
// заводить обходной лист.
|
||||||
|
if (newStatus === REQUEST_STATUS.approved) {
|
||||||
|
createClearance();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onDelegated={() => {
|
onDelegated={() => {
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
|
|||||||
@@ -40,14 +40,39 @@ RequestTypeId» в `docs/forms.md`). Тип нужно **завести в ад
|
|||||||
отдельного поля нет: в шаблоне формулировка фиксированная («по собственному желанию»).
|
отдельного поля нет: в шаблоне формулировка фиксированная («по собственному желанию»).
|
||||||
- **Заголовок элемента** (`Title`): `Заявление на увольнение — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Заявление на увольнение — {ФИО} — дд.мм.гггг`.
|
||||||
|
|
||||||
## Связь с обходным листом
|
## Согласовано → обходной лист создаётся сам
|
||||||
В списках данных обеих форм есть **перекрёстные колонки-гиперссылки** (тип `Url`):
|
Как только заявление получает статус **«Согласовано»** (маршрут пройден целиком),
|
||||||
`ClearanceUrl` здесь — на обходной лист, `DismissalUrl` в «Обходном листе» — на это
|
портал заводит по нему **обходной лист** — `ensureClearanceForDismissal`
|
||||||
заявление. Одно увольнение = заявление + обходной лист, и по ссылке видно парную
|
(`createClearance.ts`), вызывается из `DismissalView` в `onDecided` последнего
|
||||||
заявку. Не Lookup намеренно: ссылка ведёт на **страницу портала** (`…?rpmode=approve&
|
согласующего.
|
||||||
rptype=…&rpid=…`, см. `docs/routing.md`), а не на элемент списка, и не ломается,
|
|
||||||
когда парная заявка уезжает в другую партицию. Пока это колонки **только в списках** —
|
Форма обходного листа при этом **не открывается**: маршрут резолвится через
|
||||||
формы их не пишут и не показывают, ссылка ставится вручную в SharePoint.
|
approval-route (`getPlannedMatrix` для фаз «Согласование» и «Исполнение» — по
|
||||||
|
`Employee` и `Position` заявления), элемент пишется в текущую партицию списка
|
||||||
|
«Обходной лист … — данные» и сразу уходит на согласование
|
||||||
|
(`initialSubmitStatus` + `notifyActiveStep`, как при обычной подаче). Поля целиком
|
||||||
|
выводятся из заявления: сотрудник, должность, подразделение, организация, а
|
||||||
|
последний рабочий день становится **датой расторжения договора**; отметки
|
||||||
|
(парковка, СКУД, обучение) остаются пустыми — их заполняют профильные согласующие.
|
||||||
|
|
||||||
|
Пара связывается **перекрёстными колонками-гиперссылками** (тип `Url`):
|
||||||
|
`ClearanceUrl` в заявлении → на обходной лист, `DismissalUrl` в обходном листе → на
|
||||||
|
заявление. Не Lookup намеренно: ссылка ведёт на страницу портала
|
||||||
|
(`…?rpmode=approve&rptype=…&rpid=…&rplist=…`, см. `docs/routing.md`), а не на элемент
|
||||||
|
списка, и не ломается, когда парная заявка уезжает в другую партицию.
|
||||||
|
|
||||||
|
Что важно знать про это поведение:
|
||||||
|
- **Идемпотентность — по непустой `ClearanceUrl`.** Заявление можно согласовать
|
||||||
|
повторно (возврат на доработку → отправка → согласование), и без этой проверки
|
||||||
|
на каждый цикл появлялся бы новый обходной лист.
|
||||||
|
- **Работает в браузере последнего согласующего.** Если создание упало (или он
|
||||||
|
закрыл вкладку в этот момент), заявление всё равно согласовано, а обходного листа
|
||||||
|
нет — портал покажет предупреждение «заведите вручную». Серверного повтора нет:
|
||||||
|
фонового обработчика у портала пока не существует, всё делает клиент.
|
||||||
|
- **Автором обходного листа** в SharePoint станет этот согласующий (в «Мои заявки»
|
||||||
|
лист попадёт к нему). На маршрут это не влияет — он резолвится по `Employee`.
|
||||||
|
- Тип заявки `clearance-form` должен быть заведён в админке approval-route — иначе
|
||||||
|
создание падает на резолве `RequestTypeId`.
|
||||||
|
|
||||||
## Отличия от «Обходной лист» (clearance-form)
|
## Отличия от «Обходной лист» (clearance-form)
|
||||||
Обходной лист оформляет HR **после** решения об увольнении (обход подразделений,
|
Обходной лист оформляет HR **после** решения об увольнении (обход подразделений,
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import {
|
||||||
|
IApproverToWrite,
|
||||||
|
IMatrixRow,
|
||||||
|
getPlannedMatrix,
|
||||||
|
getRequestTypeIdByPageUrl,
|
||||||
|
initialSubmitStatus,
|
||||||
|
notifyActiveStep,
|
||||||
|
resolveWriteListId
|
||||||
|
} from '../../shared/approval';
|
||||||
|
import { resolveDataList } from '../../shared/dataList';
|
||||||
|
import {
|
||||||
|
approvalListTitle,
|
||||||
|
approvalListTitleForListId,
|
||||||
|
dataListTitle,
|
||||||
|
dataListTitleForListId
|
||||||
|
} from '../../shared/listTitles';
|
||||||
|
import { requestLinkUrl } from '../../shared/routing';
|
||||||
|
import { FORM_ID as CLEARANCE_FORM_ID, EMPTY_DATA as CLEARANCE_EMPTY, IFormData as IClearanceData } from '../Clearance/model';
|
||||||
|
import { clearanceApprovalService, saveClearanceRequest } from '../Clearance/submit';
|
||||||
|
import { FORM_ID } from './model';
|
||||||
|
|
||||||
|
/** Результат автосоздания обходного листа по согласованному заявлению. */
|
||||||
|
export interface IClearanceForDismissal {
|
||||||
|
/** Id обходного листа. undefined — лист уже был создан раньше (ссылка есть, Id из неё не выводим). */
|
||||||
|
id?: number;
|
||||||
|
/** Уникальный код заявки, если обходной лист создан именно сейчас. */
|
||||||
|
code?: string;
|
||||||
|
/** Ссылка на обходной лист (та же, что записана в колонку ClearanceUrl). */
|
||||||
|
url: string;
|
||||||
|
/** true — ничего не создавали, обходной лист по этому заявлению уже был. */
|
||||||
|
existing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Гиперссылка в SharePoint — объект {Url, Description}, а не строка. */
|
||||||
|
interface ISpUrlValue {
|
||||||
|
Url?: string;
|
||||||
|
Description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Матрица планируемого маршрута → строки для записи в список согласования. */
|
||||||
|
function toApprovers(rows: IMatrixRow[]): IApproverToWrite[] {
|
||||||
|
return rows.map((r) => ({
|
||||||
|
email: r.email,
|
||||||
|
position: r.position,
|
||||||
|
allowReassign: r.allowReassign,
|
||||||
|
order: r.order,
|
||||||
|
slaHours: r.slaHours,
|
||||||
|
askOverdueReason: r.askOverdueReason
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать обходной лист по СОГЛАСОВАННОМУ заявлению на увольнение и связать их
|
||||||
|
* перекрёстными ссылками (ClearanceUrl / DismissalUrl).
|
||||||
|
*
|
||||||
|
* Зачем: увольнение — это всегда пара «заявление + обходной лист», и раньше HR
|
||||||
|
* заводил второй руками, переписывая те же сотрудника, должность, подразделение и
|
||||||
|
* дату. Данные для обходного листа целиком выводятся из заявления, поэтому после
|
||||||
|
* его согласования заводить лист вручную незачем.
|
||||||
|
*
|
||||||
|
* **Идемпотентность** — по колонке ClearanceUrl самого заявления: она заполняется
|
||||||
|
* последним шагом, и непустая означает «лист уже есть». Это важнее, чем кажется:
|
||||||
|
* заявление может быть согласовано повторно (вернули на доработку → отправили
|
||||||
|
* снова → снова одобрили), и без этой проверки на каждый цикл появлялся бы новый
|
||||||
|
* обходной лист. Проверка не абсолютна (два одновременных решения по одной заявке
|
||||||
|
* прошли бы оба), но параллельных решений по одному последнему шагу не бывает:
|
||||||
|
* шаг назначен на одного человека.
|
||||||
|
*
|
||||||
|
* **Кто автор.** Вызов идёт из браузера последнего согласующего, поэтому в
|
||||||
|
* SharePoint автором обходного листа окажется он, а не увольняемый или HR. На
|
||||||
|
* маршрут это не влияет (он резолвится по полю Employee, см. ниже), но в «Мои
|
||||||
|
* заявки» этого согласующего лист попадёт.
|
||||||
|
*
|
||||||
|
* Ошибки НЕ глушим: вызывающая сторона (DismissalView) показывает их отдельным
|
||||||
|
* предупреждением — согласование при этом уже применено, и молчаливая потеря
|
||||||
|
* обходного листа хуже, чем сообщение «создайте вручную».
|
||||||
|
*/
|
||||||
|
export async function ensureClearanceForDismissal(
|
||||||
|
context: WebPartContext,
|
||||||
|
dismissalId: number,
|
||||||
|
dismissalListId?: string
|
||||||
|
): Promise<IClearanceForDismissal> {
|
||||||
|
const dismissalList = resolveDataList(FORM_ID, dismissalListId);
|
||||||
|
const item = (await dismissalList.items
|
||||||
|
.getById(dismissalId)
|
||||||
|
.select(
|
||||||
|
'Id',
|
||||||
|
'Position',
|
||||||
|
'Department',
|
||||||
|
'LastWorkingDay',
|
||||||
|
'Organization',
|
||||||
|
'ClearanceUrl',
|
||||||
|
'Employee/EMail'
|
||||||
|
)
|
||||||
|
.expand('Employee')()) as Record<string, unknown>;
|
||||||
|
|
||||||
|
const existingLink = (item.ClearanceUrl as ISpUrlValue | undefined)?.Url;
|
||||||
|
if (existingLink) {
|
||||||
|
return { url: existingLink, existing: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeEmail = (item.Employee as { EMail?: string } | undefined)?.EMail || '';
|
||||||
|
if (!employeeEmail) {
|
||||||
|
throw new Error('В заявлении не заполнен сотрудник — обходной лист не создан.');
|
||||||
|
}
|
||||||
|
const position = (item.Position as string) || '';
|
||||||
|
|
||||||
|
// Тип заявки и партиция для записи — как у обычной подачи обходного листа
|
||||||
|
// (см. ClearanceForm.save): партицию спрашиваем прямо сейчас, именно этот вызов
|
||||||
|
// проверяет порог элементов и запускает архивацию.
|
||||||
|
const requestTypeId = await getRequestTypeIdByPageUrl(context, CLEARANCE_FORM_ID);
|
||||||
|
const writeListId = await resolveWriteListId(context, CLEARANCE_FORM_ID, requestTypeId, undefined, undefined);
|
||||||
|
const approval = clearanceApprovalService(writeListId);
|
||||||
|
|
||||||
|
// Маршрут обходного листа резолвится по УВОЛЬНЯЕМОМУ (Employee заявления) и его
|
||||||
|
// должности — условие «IsManager» на бэкенде считается по ней (см. README формы
|
||||||
|
// «Обходной лист»). Обе фазы: согласование и исполнение.
|
||||||
|
const fieldValues = [{ fieldName: 'Position', value: position }];
|
||||||
|
const approvers = await getPlannedMatrix(context, requestTypeId, employeeEmail, approval, 'Approval', null, fieldValues);
|
||||||
|
const executors = await getPlannedMatrix(context, requestTypeId, employeeEmail, approval, 'Execution', null, fieldValues);
|
||||||
|
|
||||||
|
const data: IClearanceData = {
|
||||||
|
...CLEARANCE_EMPTY,
|
||||||
|
Employee: employeeEmail,
|
||||||
|
Position: position,
|
||||||
|
Department: (item.Department as string) || '',
|
||||||
|
// Последний рабочий день заявления = дата расторжения договора в обходном листе.
|
||||||
|
TerminationDate: ((item.LastWorkingDay as string) || '').slice(0, 10),
|
||||||
|
Organization: (item.Organization as string) || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Статус — как при обычной отправке: с согласующими «На согласовании», без них
|
||||||
|
// маршрут сразу уходит в исполнение/согласовано (см. initialSubmitStatus).
|
||||||
|
const status = initialSubmitStatus(approvers.length, executors.length);
|
||||||
|
const { id, code } = await saveClearanceRequest(
|
||||||
|
undefined,
|
||||||
|
requestTypeId,
|
||||||
|
data,
|
||||||
|
toApprovers(approvers),
|
||||||
|
toApprovers(executors),
|
||||||
|
status,
|
||||||
|
writeListId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Перекрёстные ссылки: в заявлении — на лист, в листе — на заявление.
|
||||||
|
const clearanceUrl = requestLinkUrl(CLEARANCE_FORM_ID, id, writeListId);
|
||||||
|
const dismissalUrl = requestLinkUrl(FORM_ID, dismissalId, dismissalListId);
|
||||||
|
await dismissalList.items.getById(dismissalId).update({
|
||||||
|
ClearanceUrl: { Url: clearanceUrl, Description: `Обходной лист${code ? ` №${code}` : ''}` }
|
||||||
|
});
|
||||||
|
await resolveDataList(CLEARANCE_FORM_ID, writeListId)
|
||||||
|
.items.getById(id)
|
||||||
|
.update({ DismissalUrl: { Url: dismissalUrl, Description: 'Заявление на увольнение' } });
|
||||||
|
|
||||||
|
// Оповещение активного шага — как после обычной отправки. Некритично: сам
|
||||||
|
// notifyActiveStep ошибки глотает, заявка уже создана и связана.
|
||||||
|
await notifyActiveStep(context, approval, id, {
|
||||||
|
requestTypeId,
|
||||||
|
dataListName: writeListId ? dataListTitleForListId(CLEARANCE_FORM_ID, writeListId) : dataListTitle(CLEARANCE_FORM_ID),
|
||||||
|
approvalListName: writeListId
|
||||||
|
? approvalListTitleForListId(CLEARANCE_FORM_ID, writeListId)
|
||||||
|
: approvalListTitle(CLEARANCE_FORM_ID)
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id, code, url: clearanceUrl, existing: false };
|
||||||
|
}
|
||||||
@@ -412,6 +412,30 @@ export function routeUrl(route: IRoute): string {
|
|||||||
return buildQuery(route);
|
return buildQuery(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* АБСОЛЮТНАЯ ссылка на просмотр заявки — для того, что переживёт текущую сессию:
|
||||||
|
* колонка-гиперссылка в списке SharePoint, письмо, сообщение в чат.
|
||||||
|
*
|
||||||
|
* Отличается от routeUrl() двумя вещами, и обе важны именно для «хранимой» ссылки:
|
||||||
|
* она с origin (в списке относительный путь бесполезен — его открывают из
|
||||||
|
* представления SharePoint, а не со страницы портала) и без посторонних
|
||||||
|
* query-параметров текущей страницы (routeUrl их намеренно сохраняет, потому что
|
||||||
|
* это навигация ВНУТРИ открытой страницы).
|
||||||
|
*
|
||||||
|
* Страница портала берётся из адреса текущей — портал живёт на одной странице,
|
||||||
|
* другого способа узнать её путь у клиентского кода нет.
|
||||||
|
*/
|
||||||
|
export function requestLinkUrl(formKey: string, itemId: number, listId?: string): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('rpmode', 'approve');
|
||||||
|
params.set('rptype', formKey);
|
||||||
|
params.set('rpid', String(itemId));
|
||||||
|
if (listId) {
|
||||||
|
params.set('rplist', listId);
|
||||||
|
}
|
||||||
|
return `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Текущий маршрут, реактивно обновляется по navigate() и кнопке «назад». */
|
/** Текущий маршрут, реактивно обновляется по navigate() и кнопке «назад». */
|
||||||
export function useRoute(): IRoute {
|
export function useRoute(): IRoute {
|
||||||
const [route, setRoute] = React.useState<IRoute>(() => parseRoute());
|
const [route, setRoute] = React.useState<IRoute>(() => parseRoute());
|
||||||
|
|||||||
Reference in New Issue
Block a user