From 0e582dceb814ba25683327d2170ae2bd951081fb Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Tue, 25 Aug 2026 13:04:52 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=B6=D0=B4=D1=83=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BD=D0=B0=20=D1=83=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=BD=D1=8B=D0=BC=20=D0=BB=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=BC=20=D0=B2=D0=B8=D0=B4=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поле-ссылка LinkField (shared/ui): read-only «карточка» как у OrganizationField, но кликабельная, открывается в новой вкладке (портал — одна страница, переход в текущей означал бы перезагрузку с потерей открытого). Пустой адрес — поле не рендерится: связь есть не у всех заявок. В заявлении показывается «Обходной лист» (ClearanceUrl), в обходном листе — «Заявление на увольнение» (DismissalUrl); обе колонки читаются в load.ts (адрес из объекта {Url, Description}) и не пишутся формами. Согласующему, который только что одобрил заявление, ссылка дописывается в модель сразу после автосоздания листа — иначе появилась бы только после перезагрузки. Co-Authored-By: Claude Opus 5 --- .../requestPortal/forms/Clearance/README.md | 6 ++- .../requestPortal/forms/Clearance/load.ts | 5 ++- .../requestPortal/forms/Clearance/model.ts | 9 ++++- .../Clearance/sections/RequestSection.tsx | 9 ++++- .../forms/Dismissal/DismissalView.tsx | 10 +++-- .../requestPortal/forms/Dismissal/README.md | 5 +++ .../requestPortal/forms/Dismissal/load.ts | 5 ++- .../requestPortal/forms/Dismissal/model.ts | 9 ++++- .../Dismissal/sections/RequestSection.tsx | 10 ++++- .../shared/ui/LinkField/LinkField.module.scss | 30 +++++++++++++++ .../shared/ui/LinkField/LinkField.tsx | 38 +++++++++++++++++++ src/webparts/requestPortal/shared/ui/index.ts | 1 + 12 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 src/webparts/requestPortal/shared/ui/LinkField/LinkField.module.scss create mode 100644 src/webparts/requestPortal/shared/ui/LinkField/LinkField.tsx diff --git a/src/webparts/requestPortal/forms/Clearance/README.md b/src/webparts/requestPortal/forms/Clearance/README.md index 818e830..23e6407 100644 --- a/src/webparts/requestPortal/forms/Clearance/README.md +++ b/src/webparts/requestPortal/forms/Clearance/README.md @@ -49,8 +49,10 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова заявления в портале. Пара связана колонками-гиперссылками: `DismissalUrl` здесь → на заявление, -`ClearanceUrl` в заявлении → на лист. Форма их не показывает и не пишет — их -заполняет автосоздание (или вручную в SharePoint). +`ClearanceUrl` в заявлении → на лист. Форма их не пишет (заполняет автосоздание либо +вручную в SharePoint), но **показывает**: поле «Заявление на увольнение» — +`LinkField` из `shared/ui`, read-only, открывается в новой вкладке; у листов, +заведённых руками, ссылки нет и поле не рендерится. ## Секции формы RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма обучения · парковочная карта · карта СКУД) diff --git a/src/webparts/requestPortal/forms/Clearance/load.ts b/src/webparts/requestPortal/forms/Clearance/load.ts index 01e3276..197fadc 100644 --- a/src/webparts/requestPortal/forms/Clearance/load.ts +++ b/src/webparts/requestPortal/forms/Clearance/load.ts @@ -32,6 +32,7 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro 'AccessCard', 'TrainingAmount', 'Organization', + 'DismissalUrl', 'RequestStatus', 'RequestUniqueCode', 'Employee/Title', @@ -52,7 +53,9 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro ParkingCard: !!item.ParkingCard, AccessCard: !!item.AccessCard, TrainingAmount: amount === null || amount === undefined ? '' : String(amount), - Organization: (item.Organization as string) || '' + Organization: (item.Organization as string) || '', + // Гиперссылка приходит объектом {Url, Description} — модели нужен только адрес. + DismissalUrl: (item.DismissalUrl as { Url?: string } | undefined)?.Url || '' }; return { diff --git a/src/webparts/requestPortal/forms/Clearance/model.ts b/src/webparts/requestPortal/forms/Clearance/model.ts index 217e533..359bd65 100644 --- a/src/webparts/requestPortal/forms/Clearance/model.ts +++ b/src/webparts/requestPortal/forms/Clearance/model.ts @@ -31,6 +31,12 @@ export interface IFormData { * read-only (см. shared/ui/OrganizationField). */ Organization: string; + /** + * Ссылка на заявление на увольнение, по которому создан этот лист — колонка- + * гиперссылка `DismissalUrl`. Форма её не пишет (заполняет автосоздание, см. + * forms/Dismissal/createClearance.ts) и не даёт править — только показывает. + */ + DismissalUrl: string; } /** Обновление одного поля формы с сохранением типа значения. */ @@ -56,7 +62,8 @@ export const EMPTY_DATA: IFormData = { ParkingCard: false, AccessCard: false, TrainingAmount: '', - Organization: '' + Organization: '', + DismissalUrl: '' }; /** Плейсхолдер для поля выбора сотрудника. */ diff --git a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx index 79fd408..a37caf9 100644 --- a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField } from '../../../shared/ui'; +import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui'; import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model'; // Подразделение подтягивается автоматически при выборе сотрудника (см. @@ -89,5 +89,12 @@ export const RequestSection: React.FC = ({ readOnly={hrRecordsEditable ? false : undefined} /> + {/* Есть только у листов, созданных автоматически по согласованному заявлению + на увольнение (см. forms/Dismissal/createClearance.ts). */} + ); diff --git a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx index ec8d621..976cc51 100644 --- a/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/DismissalView.tsx @@ -124,13 +124,17 @@ const DismissalView: React.FC = ({ context, itemId, formKey const createClearance = (): void => { setClearanceError(''); ensureClearanceForDismissal(context, itemId, listId) - .then((result) => + .then((result) => { setClearance( result.existing ? 'Обходной лист по этому заявлению уже был создан.' : `Обходной лист создан автоматически${result.code ? ` — №${result.code}` : ''} и отправлен на согласование.` - ) - ) + ); + // Поле-ссылка в секции заполняется из данных заявки, а они прочитаны ДО + // создания листа — дописываем адрес в модель, иначе ссылка появилась бы + // только после перезагрузки страницы. + setData((prev) => ({ ...prev, ClearanceUrl: result.url })); + }) .catch((e) => setClearanceError( `Заявление согласовано, но обходной лист создать не удалось: ${friendlyErrorMessage(e)} — заведите его вручную.` diff --git a/src/webparts/requestPortal/forms/Dismissal/README.md b/src/webparts/requestPortal/forms/Dismissal/README.md index 858fe0f..fecf596 100644 --- a/src/webparts/requestPortal/forms/Dismissal/README.md +++ b/src/webparts/requestPortal/forms/Dismissal/README.md @@ -61,6 +61,11 @@ approval-route (`getPlannedMatrix` для фаз «Согласование» и (`…?rpmode=approve&rptype=…&rpid=…&rplist=…`, см. `docs/routing.md`), а не на элемент списка, и не ломается, когда парная заявка уезжает в другую партицию. +Ссылки **видны на обеих формах** и кликабельны: поле «Обходной лист» в заявлении и +«Заявление на увольнение» в обходном листе (`LinkField`, `shared/ui`) — read-only, +открываются в новой вкладке и появляются только когда связь есть. У согласующего, +только что одобрившего заявление, ссылка появляется сразу, без перезагрузки. + Что важно знать про это поведение: - **Идемпотентность — по непустой `ClearanceUrl`.** Заявление можно согласовать повторно (возврат на доработку → отправка → согласование), и без этой проверки diff --git a/src/webparts/requestPortal/forms/Dismissal/load.ts b/src/webparts/requestPortal/forms/Dismissal/load.ts index 512438a..79abe79 100644 --- a/src/webparts/requestPortal/forms/Dismissal/load.ts +++ b/src/webparts/requestPortal/forms/Dismissal/load.ts @@ -32,6 +32,7 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro 'Phone', 'Comment', 'Organization', + 'ClearanceUrl', 'RequestStatus', 'RequestUniqueCode', 'Employee/Title', @@ -51,7 +52,9 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro LastWorkingDay: lastDay.slice(0, 10), // ISO → YYYY-MM-DD для input type=date Phone: (item.Phone as string) || '', Comment: (item.Comment as string) || '', - Organization: (item.Organization as string) || '' + Organization: (item.Organization as string) || '', + // Гиперссылка приходит объектом {Url, Description} — модели нужен только адрес. + ClearanceUrl: (item.ClearanceUrl as { Url?: string } | undefined)?.Url || '' }; return { diff --git a/src/webparts/requestPortal/forms/Dismissal/model.ts b/src/webparts/requestPortal/forms/Dismissal/model.ts index 1530ddf..00ff560 100644 --- a/src/webparts/requestPortal/forms/Dismissal/model.ts +++ b/src/webparts/requestPortal/forms/Dismissal/model.ts @@ -38,6 +38,12 @@ export interface IFormData { * read-only (см. shared/ui/OrganizationField). */ Organization: string; + /** + * Ссылка на обходной лист, созданный по этому заявлению — колонка-гиперссылка + * `ClearanceUrl`. Форма её не пишет (заполняет автосоздание, см. + * createClearance.ts) и не даёт править — только показывает переходом. + */ + ClearanceUrl: string; } /** Обновление одного поля формы с сохранением типа значения. */ @@ -64,7 +70,8 @@ export const EMPTY_DATA: IFormData = { LastWorkingDay: '', Phone: '', Comment: '', - Organization: '' + Organization: '', + ClearanceUrl: '' }; /** Плейсхолдер для поля выбора сотрудника. */ diff --git a/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx index 68277c7..a4b886a 100644 --- a/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx @@ -6,7 +6,8 @@ import { TextArea, PhoneInput, PeopleField, - OrganizationField + OrganizationField, + LinkField } from '../../../shared/ui'; import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model'; import { isoDatePlusDays } from '../../../shared/dateUtils'; @@ -76,5 +77,12 @@ export const RequestSection: React.FC = ({ data, set, context, on