Заявление на увольнение #15
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user