Заявление на увольнение #15

Merged
gorbenko.pavel merged 15 commits from fired-request into main 2026-08-26 07:07:03 +00:00
12 changed files with 126 additions and 11 deletions
Showing only changes of commit 0e582dceb8 - Show all commits
@@ -49,8 +49,10 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова
заявления в портале.
Пара связана колонками-гиперссылками: `DismissalUrl` здесь → на заявление,
`ClearanceUrl` в заявлении → на лист. Форма их не показывает и не пишет — их
заполняет автосоздание (или вручную в SharePoint).
`ClearanceUrl` в заявлении → на лист. Форма их не пишет (заполняет автосоздание либо
вручную в SharePoint), но **показывает**: поле «Заявление на увольнение» —
`LinkField` из `shared/ui`, read-only, открывается в новой вкладке; у листов,
заведённых руками, ссылки нет и поле не рендерится.
## Секции формы
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма обучения · парковочная карта · карта СКУД)
@@ -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 {
@@ -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: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -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<IRequestSectionProps> = ({
readOnly={hrRecordsEditable ? false : undefined}
/>
</FormField>
{/* Есть только у листов, созданных автоматически по согласованному заявлению
на увольнение (см. forms/Dismissal/createClearance.ts). */}
<LinkField
label="Заявление на увольнение"
url={data.DismissalUrl}
text="Открыть заявление, по которому создан этот лист"
/>
</CollapseSection>
);
@@ -124,13 +124,17 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ 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)} — заведите его вручную.`
@@ -61,6 +61,11 @@ approval-route (`getPlannedMatrix` для фаз «Согласование» и
(`…?rpmode=approve&rptype=…&rpid=…&rplist=…`, см. `docs/routing.md`), а не на элемент
списка, и не ломается, когда парная заявка уезжает в другую партицию.
Ссылки **видны на обеих формах** и кликабельны: поле «Обходной лист» в заявлении и
«Заявление на увольнение» в обходном листе (`LinkField`, `shared/ui`) — read-only,
открываются в новой вкладке и появляются только когда связь есть. У согласующего,
только что одобрившего заявление, ссылка появляется сразу, без перезагрузки.
Что важно знать про это поведение:
- **Идемпотентность — по непустой `ClearanceUrl`.** Заявление можно согласовать
повторно (возврат на доработку → отправка → согласование), и без этой проверки
@@ -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 {
@@ -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: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -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<ISectionProps> = ({ data, set, context, on
<FormField label="Комментарий" htmlFor="Comment" full>
<TextArea id="Comment" value={data.Comment} onChange={(v) => set('Comment', v)} />
</FormField>
{/* Появляется, только когда обходной лист уже создан (автоматически после
согласования заявления — см. createClearance.ts). */}
<LinkField
label="Обходной лист"
url={data.ClearanceUrl}
text="Открыть обходной лист по этому увольнению"
/>
</CollapseSection>
);
@@ -0,0 +1,30 @@
@import '../../../../../styles/design-tokens';
// Read-only «карточка» со ссылкой — те же радиус/паддинг/приглушённый фон, что у
// OrganizationField (сигнал «подставлено автоматически, не редактируется»),
// отличается только тем, что содержимое кликабельно.
.box {
box-sizing: border-box;
padding: 11px 13px;
border: 1px solid $gray-200;
border-radius: $r-sm;
background: $gray-50;
font-family: $font-sans;
font-size: 13.5px;
color: $gray-700;
}
.link {
display: inline-flex;
align-items: center;
gap: $sp-2;
color: $navy-700;
text-decoration: none;
transition: color .18s;
&:hover,
&:focus {
color: $gold-600;
text-decoration: underline;
}
}
@@ -0,0 +1,38 @@
import * as React from 'react';
import styles from './LinkField.module.scss';
import { FormField, Icon } from '@tnsu/ui-kit';
interface ILinkFieldProps {
label: string;
/** Адрес. Пусто — поле не рендерится вовсе (см. ниже). */
url: string;
/** Текст ссылки. По умолчанию — сам адрес. */
text?: string;
}
/**
* Поле-ссылка: read-only, кликабельное, руками не редактируется.
*
* Пустой url — ничего не рендерим (а не пустую рамку): такие поля появляются
* только когда связь уже есть (напр. ClearanceUrl заявления на увольнение
* заполняется автосозданием обходного листа), и до этого момента их нет смысла
* показывать.
*
* Открывается в НОВОЙ вкладке: портал — одна страница, и переход по ссылке в
* текущей означал бы полную перезагрузку с потерей того, что открыто и, возможно,
* не сохранено.
*/
export const LinkField: React.FC<ILinkFieldProps> = ({ label, url, text }) => {
if (!url) return null;
return (
<FormField label={label} full>
<div className={styles.box}>
<a className={styles.link} href={url} target="_blank" rel="noreferrer">
<span>{text || url}</span>
<Icon name="chevron-right" size="sm" />
</a>
</div>
</FormField>
);
};
@@ -39,6 +39,7 @@ export { parseExcelRows } from './ItemsTable/parseExcelRows';
export { PeopleField } from './PeopleField/PeopleField';
export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti';
export { OrganizationField } from './OrganizationField/OrganizationField';
export { LinkField } from './LinkField/LinkField';
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
export { SubmitActions } from './SubmitActions/SubmitActions';
export { InitiatorLine } from './InitiatorLine/InitiatorLine';