Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ad2a19a70 | |||
| 95b0f8f21e |
@@ -1 +1 @@
|
|||||||
@tnsu:registry=http://git.tnsu.kz/api/packages/Tansu/npm/
|
@tnsu:registry=https://git.tnsu.kz/api/packages/tansu/npm/
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ Info "Подключаем @tnsu/ui-kit..."
|
|||||||
# .npmrc: адрес registry для скоупа @tnsu (НЕ секрет, можно коммитить).
|
# .npmrc: адрес registry для скоупа @tnsu (НЕ секрет, можно коммитить).
|
||||||
# Токен на чтение — у каждого разработчика свой, в ~/.npmrc (в проект не кладём).
|
# Токен на чтение — у каждого разработчика свой, в ~/.npmrc (в проект не кладём).
|
||||||
$npmrc = @'
|
$npmrc = @'
|
||||||
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
|
@tnsu:registry=https://git.tnsu.kz/api/packages/tansu/npm/
|
||||||
'@
|
'@
|
||||||
$npmrc | Set-Content -Encoding UTF8 ".npmrc"
|
$npmrc | Set-Content -Encoding UTF8 ".npmrc"
|
||||||
Success ".npmrc создан (адрес registry @tnsu)."
|
Success ".npmrc создан (адрес registry @tnsu)."
|
||||||
@@ -282,7 +282,7 @@ try {
|
|||||||
} catch {
|
} catch {
|
||||||
Warn "Не удалось поставить @tnsu/ui-kit. Скорее всего нет токена @tnsu в ~/.npmrc."
|
Warn "Не удалось поставить @tnsu/ui-kit. Скорее всего нет токена @tnsu в ~/.npmrc."
|
||||||
Warn "Создай токен (Gitea → Настройки → Приложения → package: Read) и пропиши в ~/.npmrc:"
|
Warn "Создай токен (Gitea → Настройки → Приложения → package: Read) и пропиши в ~/.npmrc:"
|
||||||
Warn " //git.tnsu.kz/api/packages/timur.gorodetskiy/npm/:_authToken=ТВОЙ_ТОКЕН"
|
Warn " //git.tnsu.kz/api/packages/tansu/npm/:_authToken=ТВОЙ_ТОКЕН"
|
||||||
Warn "Потом выполни: npm install @tnsu/ui-kit"
|
Warn "Потом выполни: npm install @tnsu/ui-kit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -9887,7 +9887,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@tnsu/ui-kit": {
|
"node_modules/@tnsu/ui-kit": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "http://git.tnsu.kz/api/packages/Tansu/npm/%40tnsu%2Fui-kit/-/0.14.0/ui-kit-0.14.0.tgz",
|
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fui-kit/-/0.14.0/ui-kit-0.14.0.tgz",
|
||||||
"integrity": "sha512-Vun9AUM9jL8rttbsbKo5Yh6TUz1srkNqxiO6mmzebrSo1JOASNJqNjJPMTS+8Jt3MTrJPklvh0TbeqGQzw7qxQ==",
|
"integrity": "sha512-Vun9AUM9jL8rttbsbKo5Yh6TUz1srkNqxiO6mmzebrSo1JOASNJqNjJPMTS+8Jt3MTrJPklvh0TbeqGQzw7qxQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
getDraftDeleteHandler,
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit,
|
useRequestSubmit
|
||||||
FormNotice
|
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
@@ -45,16 +44,7 @@ import {
|
|||||||
dataListTitle,
|
dataListTitle,
|
||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import {
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, IOrganizationRecord, primaryPosition } from './model';
|
||||||
FORM_ID,
|
|
||||||
IFormData,
|
|
||||||
EMPTY_DATA,
|
|
||||||
SetField,
|
|
||||||
IOrganizationRecord,
|
|
||||||
primaryPosition,
|
|
||||||
NOTICE_TITLE,
|
|
||||||
NOTICE_ITEMS
|
|
||||||
} from './model';
|
|
||||||
import { saveDismissalRequest, dismissalApprovalService } from './submit';
|
import { saveDismissalRequest, dismissalApprovalService } from './submit';
|
||||||
import { loadDismissalRequest } from './load';
|
import { loadDismissalRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -384,8 +374,6 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -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 './DismissalView.module.scss';
|
import styles from './DismissalView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton, FormNotice } from '../../shared/ui';
|
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { parseRoute } from '../../shared/routing';
|
import { parseRoute } from '../../shared/routing';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadDismissalRequest } from './load';
|
import { loadDismissalRequest } from './load';
|
||||||
import { dismissalApprovalService, updateAssetsAcceptor } from './submit';
|
import { dismissalApprovalService, updateAssetsAcceptor } from './submit';
|
||||||
import { loadClearanceRequest } from '../Clearance/load';
|
import { loadClearanceRequest } from '../Clearance/load';
|
||||||
@@ -285,7 +285,6 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
}
|
}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ RequestTypeId» в `docs/forms.md`). Тип заведён в админке app
|
|||||||
- **Комментарий** (`Comment`) — необязательный свободный текст. Причины увольнения
|
- **Комментарий** (`Comment`) — необязательный свободный текст. Причины увольнения
|
||||||
отдельного поля нет: в шаблоне формулировка фиксированная («по собственному желанию»).
|
отдельного поля нет: в шаблоне формулировка фиксированная («по собственному желанию»).
|
||||||
- **Заголовок элемента** (`Title`): `Заявление на увольнение — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Заявление на увольнение — {ФИО} — дд.мм.гггг`.
|
||||||
- **Примечание** (с 10-29, `FormNotice`, тексты `NOTICE_TITLE`/`NOTICE_ITEMS` в `model.ts`) — плашка
|
|
||||||
вверху формы и просмотра про срок уведомления по ст. 56 ТК РК. Текст от HR, дословно; на печать не идёт.
|
|
||||||
|
|
||||||
## Совместители: организации таблицей, удаление синхронизировано с маршрутом
|
## Совместители: организации таблицей, удаление синхронизировано с маршрутом
|
||||||
Сотрудник может числиться сразу в НЕСКОЛЬКИХ компаниях (совместительство).
|
Сотрудник может числиться сразу в НЕСКОЛЬКИХ компаниях (совместительство).
|
||||||
|
|||||||
@@ -149,18 +149,3 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
||||||
/** Заявление нужно подавать не позднее чем за столько месяцев до последнего рабочего дня. */
|
/** Заявление нужно подавать не позднее чем за столько месяцев до последнего рабочего дня. */
|
||||||
export const MIN_NOTICE_MONTHS = 1;
|
export const MIN_NOTICE_MONTHS = 1;
|
||||||
|
|
||||||
/**
|
|
||||||
* Примечание вверху формы и просмотра (см. shared/ui/FormNotice). Текст — от HR,
|
|
||||||
* дословно. «Не менее чем за один месяц» — это MIN_NOTICE_MONTHS: поменяли срок —
|
|
||||||
* поправьте и текст.
|
|
||||||
*/
|
|
||||||
export const NOTICE_TITLE = 'Примечание';
|
|
||||||
export const NOTICE_ITEMS: string[] = [
|
|
||||||
'Срок уведомления. Согласно п. 1 ст. 56 Трудового кодекса РК, работник вправе по своей инициативе ' +
|
|
||||||
'расторгнуть трудовой договор, уведомив работодателя письменно не менее чем за один месяц, за исключением ' +
|
|
||||||
'случаев, предусмотренных п. 3 этой статьи. Если трудовым договором установлен более длительный срок ' +
|
|
||||||
'уведомления, применяется срок из трудового договора.',
|
|
||||||
'Увольнение до истечения срока уведомления. Дата увольнения раньше чем через месяц с даты подачи заявления ' +
|
|
||||||
'возможна только по согласованию с работодателем, либо в случаях, предусмотренных п. 3 ст. 56 ТК РК.'
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -33,11 +33,6 @@
|
|||||||
(черновик проверяется так же строго, как отправка).
|
(черновик проверяется так же строго, как отправка).
|
||||||
- **Замещающий** (`Substitute`, People) — необязателен.
|
- **Замещающий** (`Substitute`, People) — необязателен.
|
||||||
- **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`.
|
||||||
- **Место для подписи на печати** (с 10-29, `PrintSignature`) — как в «Заявке на отпуск»:
|
|
||||||
столбиком «ФИО / линия / (подпись)» в правом верхнем углу шапки под статусом; ФИО сотрудника,
|
|
||||||
а не инициатора.
|
|
||||||
- **Таблица «Исполнение» на печать не идёт** (с 10-29, `rp-no-print`) — как в «Заявке на отпуск»;
|
|
||||||
на печати только маршрут согласования.
|
|
||||||
|
|
||||||
## Отличия от «Заявка на отпуск» (hr-vacation)
|
## Отличия от «Заявка на отпуск» (hr-vacation)
|
||||||
- Те же «дата начала + количество дней»: даты окончания нет, праздники и выходные не считаются.
|
- Те же «дата начала + количество дней»: даты окончания нет, праздники и выходные не считаются.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
SubmitActions,
|
SubmitActions,
|
||||||
getDraftDeleteHandler,
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit,
|
useRequestSubmit,
|
||||||
FormNotice
|
LeaveNotice
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
@@ -335,7 +335,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
<LeaveNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
|
|||||||
@@ -15,12 +15,3 @@
|
|||||||
gap: $sp-3;
|
gap: $sp-3;
|
||||||
margin-bottom: $sp-4;
|
margin-bottom: $sp-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Правый угол шапки: статус, под ним (только в печати) подпись сотрудника.
|
|
||||||
// headerRight у FormCard раскладывает содержимое в строку — здесь нужен столбик.
|
|
||||||
.headerAside {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: $sp-3;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
FormNotice,
|
LeaveNotice
|
||||||
PrintSignature
|
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
@@ -65,8 +64,6 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
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>('');
|
||||||
// Ф.И.О. сотрудника (Employee/Title) — для строки подписи на печатном заявлении.
|
|
||||||
const [employeeName, setEmployeeName] = 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);
|
||||||
// Какая из организаций сотрудника (см. OrganizationField) идёт в печать —
|
// Какая из организаций сотрудника (см. OrganizationField) идёт в печать —
|
||||||
@@ -100,7 +97,6 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
setStatus(loaded.status);
|
setStatus(loaded.status);
|
||||||
setCode(loaded.code);
|
setCode(loaded.code);
|
||||||
setEmployeeName(loaded.employeeName);
|
|
||||||
notifyCode(loaded.code);
|
notifyCode(loaded.code);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -162,15 +158,9 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
itemId={itemId}
|
itemId={itemId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
headerRight={
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
<div className={styles.headerAside}>
|
|
||||||
{status && <StatusBadge status={status} />}
|
|
||||||
{/* Подписывает сотрудник, а не инициатор: заявку могут оформить за него. */}
|
|
||||||
<PrintSignature name={employeeName} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
<LeaveNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
@@ -178,11 +168,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
{/* Исполнение — внутренняя работа кадров после согласования, к заявлению
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
сотрудника не относится: на печати только маршрут согласования. */}
|
|
||||||
<div className="rp-no-print">
|
|
||||||
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О.
|
|||||||
*/
|
*/
|
||||||
export const MIN_NOTICE_DAYS = 14;
|
export const MIN_NOTICE_DAYS = 14;
|
||||||
|
|
||||||
/** Памятка вверху формы и просмотра (см. shared/ui/FormNotice). Текст — по просьбе HR. */
|
/** Памятка вверху формы и просмотра (см. shared/ui/LeaveNotice). Текст — по просьбе HR. */
|
||||||
export const NOTICE_TITLE = 'Памятка';
|
export const NOTICE_TITLE = 'Памятка';
|
||||||
export const NOTICE_ITEMS: string[] = [
|
export const NOTICE_ITEMS: string[] = [
|
||||||
'После согласования заявления его необходимо распечатать с портала (кнопка «Печать» на странице заявки), ' +
|
'После согласования заявления его необходимо распечатать с портала (кнопка «Печать» на странице заявки), ' +
|
||||||
|
|||||||
@@ -31,11 +31,6 @@
|
|||||||
- **Комментарий** (с 10-28, `VacationComment`, Note) — необязательный, заполняет инициатор,
|
- **Комментарий** (с 10-28, `VacationComment`, Note) — необязательный, заполняет инициатор,
|
||||||
последним полем секции; в просмотре пустой не показывается. В DOCX для ЭДО (`vacationDocument.ts`) не идёт; в печать
|
последним полем секции; в просмотре пустой не показывается. В DOCX для ЭДО (`vacationDocument.ts`) не идёт; в печать
|
||||||
тоже не попадает (обёртка `rp-no-print` с `display: contents`, чтобы на экране поле оставалось ячейкой сетки).
|
тоже не попадает (обёртка `rp-no-print` с `display: contents`, чтобы на экране поле оставалось ячейкой сетки).
|
||||||
- **Место для подписи на печати** (с 10-29, `PrintSignature` из `shared/ui`) — столбиком «ФИО /
|
|
||||||
линия / (подпись)» в правом верхнем углу шапки под статусом (`headerAside`), только при печати. ФИО — **сотрудника** (`Employee/Title`), а не
|
|
||||||
инициатора: заявку могут оформить за сотрудника, а заявление подписывает он сам.
|
|
||||||
- **Таблица «Исполнение» на печать не идёт** (с 10-29, обёртка `rp-no-print`) — это внутренняя
|
|
||||||
работа кадров после согласования; на печати только маршрут согласования. На экране видна.
|
|
||||||
|
|
||||||
## Подписание в ЭДО
|
## Подписание в ЭДО
|
||||||
> **Сейчас выключено** (`VACATION_EDO_SIGNING_ENABLED = false` в `shared/config.ts`): решение последнего
|
> **Сейчас выключено** (`VACATION_EDO_SIGNING_ENABLED = false` в `shared/config.ts`): решение последнего
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
SubmitActions,
|
SubmitActions,
|
||||||
getDraftDeleteHandler,
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit,
|
useRequestSubmit,
|
||||||
FormNotice
|
LeaveNotice
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
@@ -354,7 +354,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
<LeaveNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
|
|||||||
@@ -15,12 +15,3 @@
|
|||||||
gap: $sp-3;
|
gap: $sp-3;
|
||||||
margin-bottom: $sp-4;
|
margin-bottom: $sp-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Правый угол шапки: статус, под ним (только в печати) подпись сотрудника.
|
|
||||||
// headerRight у FormCard раскладывает содержимое в строку — здесь нужен столбик.
|
|
||||||
.headerAside {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: $sp-3;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
FormNotice,
|
LeaveNotice
|
||||||
PrintSignature
|
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
@@ -221,26 +220,16 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
itemId={itemId}
|
itemId={itemId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
headerRight={
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
<div className={styles.headerAside}>
|
|
||||||
{status && <StatusBadge status={status} />}
|
|
||||||
{/* Подписывает сотрудник, а не инициатор: заявку могут оформить за него. */}
|
|
||||||
<PrintSignature name={employeeName} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
<LeaveNotice title={NOTICE_TITLE} items={NOTICE_ITEMS} />
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
{/* Исполнение — внутренняя работа кадров после согласования, к заявлению
|
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
||||||
сотрудника не относится: на печати только маршрут согласования. */}
|
|
||||||
<div className="rp-no-print">
|
|
||||||
<ExecutorsMatrix editor={executors} search={(q) => approval.searchUsers(q)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const MIN_NOTICE_DAYS = 7;
|
|||||||
export const MAX_DAYS = 24;
|
export const MAX_DAYS = 24;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Примечание вверху формы и просмотра (см. shared/ui/FormNotice). Текст — от HR,
|
* Примечание вверху формы и просмотра (см. shared/ui/LeaveNotice). Текст — от HR,
|
||||||
* дословно. «Одну календарную неделю» — это MIN_NOTICE_DAYS: поменяли срок — поправьте и текст.
|
* дословно. «Одну календарную неделю» — это MIN_NOTICE_DAYS: поменяли срок — поправьте и текст.
|
||||||
* Правило про часть не менее 14 дней только показываем, а не проверяем: форма не знает,
|
* Правило про часть не менее 14 дней только показываем, а не проверяем: форма не знает,
|
||||||
* брал ли сотрудник уже часть отпуска.
|
* брал ли сотрудник уже часть отпуска.
|
||||||
|
|||||||
@@ -1,36 +1,25 @@
|
|||||||
@import '../../../../../styles/design-tokens';
|
@import '../../../../../styles/design-tokens';
|
||||||
|
|
||||||
// Кружок события на линии ленты.
|
// Ширина колонки со временем — под «29.09.2026 11:02»; уже — текст переносится.
|
||||||
$marker-size: 24px;
|
$time-col-width: 124px;
|
||||||
// Толщина линии, соединяющей события.
|
// Ниже этой ширины время встаёт над текстом события, а не слева от него.
|
||||||
$line-width: 2px;
|
$feed-stack-breakpoint: 640px;
|
||||||
// Зазор между событиями — линия обязана его перекрыть, иначе рвётся.
|
|
||||||
$event-gap: $sp-4;
|
|
||||||
|
|
||||||
.feed {
|
.feed {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $sp-5;
|
gap: $sp-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.round {
|
.round {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $sp-3;
|
gap: $sp-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Номер отправки + плашка итога (см. RoundTitle). Капслок — только у номера:
|
|
||||||
// повесь его на весь заголовок, и плашка унаследовала бы его вместе с разрядкой.
|
|
||||||
.roundTitle {
|
.roundTitle {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: $sp-1 $sp-2;
|
|
||||||
padding-bottom: $sp-1;
|
padding-bottom: $sp-1;
|
||||||
border-bottom: 1px solid $gray-200;
|
border-bottom: 1px solid $gray-200;
|
||||||
}
|
|
||||||
|
|
||||||
.roundNumber {
|
|
||||||
font-size: $fs-overline;
|
font-size: $fs-overline;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 1.4px;
|
letter-spacing: 1.4px;
|
||||||
@@ -41,122 +30,62 @@ $event-gap: $sp-4;
|
|||||||
.events {
|
.events {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $event-gap;
|
gap: $sp-3;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: $sp-3;
|
gap: $sp-3;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-sm;
|
||||||
color: $gray-900;
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
// Линия от кружка к следующему событию — лента читается как хронология, а не
|
.current {
|
||||||
// как стопка одинаковых строк. У последнего события отправки линии нет:
|
padding: $sp-2 $sp-3;
|
||||||
// отправки разделены заголовками, а не одной сплошной нитью.
|
border-radius: $r-sm;
|
||||||
&:not(:last-child)::before {
|
background: $gold-50;
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: $marker-size;
|
|
||||||
bottom: -$event-gap;
|
|
||||||
left: calc((#{$marker-size} - #{$line-width}) / 2);
|
|
||||||
width: $line-width;
|
|
||||||
background: $gray-200;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker {
|
.marker {
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
flex: 0 0 $marker-size;
|
width: 20px;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: $marker-size;
|
|
||||||
height: $marker-size;
|
|
||||||
border: 1.5px solid;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Цвета — те же смысловые, что у плашки события и полосы комментария
|
.markerNeutral { color: $navy-700; }
|
||||||
// (см. historyKinds.ts): кружок, плашка и комментарий одного события — одного цвета.
|
.markerPositive { color: $green-600; }
|
||||||
.markerNeutral {
|
.markerWarning { color: $amber-700; }
|
||||||
border-color: $navy-600;
|
.markerNegative { color: $red-500; }
|
||||||
background: $navy-50;
|
.markerCurrent { color: $gold-600; }
|
||||||
color: $navy-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markerPositive {
|
|
||||||
border-color: $green-600;
|
|
||||||
background: $green-50;
|
|
||||||
color: $green-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markerWarning {
|
|
||||||
border-color: $gold-500;
|
|
||||||
background: $gold-100;
|
|
||||||
color: $amber-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markerNegative {
|
|
||||||
border-color: $red-500;
|
|
||||||
background: $red-50;
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markerCurrent {
|
|
||||||
border-color: $gold-500;
|
|
||||||
background: $gold-50;
|
|
||||||
color: $gold-600;
|
|
||||||
animation: current-pulse 2s ease-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Расходящееся кольцо вокруг кружка «сейчас».
|
|
||||||
@keyframes current-pulse {
|
|
||||||
0% { box-shadow: 0 0 0 0 rgba($gold-500, .45); }
|
|
||||||
70% { box-shadow: 0 0 0 8px rgba($gold-500, 0); }
|
|
||||||
100% { box-shadow: 0 0 0 0 rgba($gold-500, 0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.markerCurrent {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventBody {
|
.eventBody {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $sp-1;
|
gap: 2px;
|
||||||
min-width: 0; // иначе длинный комментарий распирает ленту вбок
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Плашка + время + длительность. На узкой ширине переносятся сами (flex-wrap),
|
.eventLine {
|
||||||
// отдельный медиазапрос не нужен. min-height — чтобы плашка стояла вровень
|
|
||||||
// с центром кружка.
|
|
||||||
.eventHead {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
gap: $sp-1 $sp-2;
|
gap: $sp-1 $sp-3;
|
||||||
min-height: $marker-size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: $fs-label;
|
flex: 0 0 $time-col-width;
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
|
||||||
|
|
||||||
.actor {
|
@media (max-width: $feed-stack-breakpoint) {
|
||||||
overflow-wrap: anywhere;
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
@@ -164,6 +93,12 @@ $event-gap: $sp-4;
|
|||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
color: $gray-700;
|
||||||
|
font-style: italic;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.note {
|
.note {
|
||||||
font-size: $fs-label;
|
font-size: $fs-label;
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ApprovalListService } from '../approvalList.service';
|
import { ApprovalListService } from '../approvalList.service';
|
||||||
import { IApprovalHistory, roundOutcome } from './historyModel';
|
import { IApprovalHistory } from './historyModel';
|
||||||
import { loadApprovalHistory } from './approvalHistory.service';
|
import { loadApprovalHistory } from './approvalHistory.service';
|
||||||
import { CurrentHolderRow } from './CurrentHolderRow';
|
import { CurrentHolderRow } from './CurrentHolderRow';
|
||||||
import { HistoryEventRow } from './HistoryEventRow';
|
import { HistoryEventRow } from './HistoryEventRow';
|
||||||
import { RoundTitle } from './RoundTitle';
|
|
||||||
import { HISTORY_TEXT } from './historyTexts';
|
import { HISTORY_TEXT } from './historyTexts';
|
||||||
import styles from './ApprovalHistory.module.scss';
|
import styles from './ApprovalHistory.module.scss';
|
||||||
|
|
||||||
@@ -50,22 +49,18 @@ export const ApprovalHistoryFeed: React.FC<Props> = ({ approval, dataListRef, re
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.feed}>
|
<div className={styles.feed}>
|
||||||
{rounds.map((round, i) => {
|
{rounds.map((round, i) => (
|
||||||
// «Сейчас» — только у последней (верхней) отправки: прошлые уже закончились.
|
<section key={round.number} className={styles.round}>
|
||||||
const isLatest = i === 0;
|
<div className={styles.roundTitle}>{HISTORY_TEXT.round(round.number)}</div>
|
||||||
const isActive = isLatest && current.length > 0;
|
<ul className={styles.events}>
|
||||||
return (
|
{/* «Сейчас» — только у последней (верхней) отправки. */}
|
||||||
<section key={round.number} className={styles.round}>
|
{i === 0 && current.map((holder, j) => <CurrentHolderRow key={`current-${j}`} holder={holder} />)}
|
||||||
<RoundTitle number={round.number} outcome={roundOutcome(round, isActive)} />
|
{round.events.map((event, j) => (
|
||||||
<ul className={styles.events}>
|
<HistoryEventRow key={`${event.time}-${event.kind}-${j}`} event={event} />
|
||||||
{isLatest && current.map((holder, j) => <CurrentHolderRow key={`current-${j}`} holder={holder} />)}
|
))}
|
||||||
{round.events.map((event, j) => (
|
</ul>
|
||||||
<HistoryEventRow key={`${event.time}-${event.kind}-${j}`} event={event} />
|
</section>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className={styles.note}>{HISTORY_TEXT.partialNote}</div>
|
<div className={styles.note}>{HISTORY_TEXT.partialNote}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge } from '@tnsu/ui-kit';
|
|
||||||
import { formatDuration, ICurrentHolder } from './historyModel';
|
import { formatDuration, ICurrentHolder } from './historyModel';
|
||||||
import { HISTORY_TEXT } from './historyTexts';
|
import { HISTORY_TEXT } from './historyTexts';
|
||||||
import styles from './ApprovalHistory.module.scss';
|
import styles from './ApprovalHistory.module.scss';
|
||||||
@@ -12,21 +11,15 @@ interface Props {
|
|||||||
export const CurrentHolderRow: React.FC<Props> = ({ holder }) => {
|
export const CurrentHolderRow: React.FC<Props> = ({ holder }) => {
|
||||||
const waitedMs = holder.since ? Date.now() - new Date(holder.since).getTime() : NaN;
|
const waitedMs = holder.since ? Date.now() - new Date(holder.since).getTime() : NaN;
|
||||||
return (
|
return (
|
||||||
<li className={styles.event}>
|
<li className={`${styles.event} ${styles.current}`}>
|
||||||
{/* Пульсирующий кружок — единственное «живое» место ленты: всё остальное уже
|
|
||||||
случилось, а это ещё ждёт. */}
|
|
||||||
<span className={`${styles.marker} ${styles.markerCurrent}`} aria-hidden="true">
|
<span className={`${styles.marker} ${styles.markerCurrent}`} aria-hidden="true">
|
||||||
●
|
●
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.eventBody}>
|
<div className={styles.eventBody}>
|
||||||
<div className={styles.eventHead}>
|
<div className={styles.eventLine}>
|
||||||
<Badge variant="info" size="sm">
|
<span className={styles.time}>{HISTORY_TEXT.now}</span>
|
||||||
{HISTORY_TEXT.awaiting}
|
|
||||||
</Badge>
|
|
||||||
{waitedMs > 0 && <span className={styles.meta}>{HISTORY_TEXT.waiting(formatDuration(waitedMs))}</span>}
|
|
||||||
</div>
|
|
||||||
<div className={styles.actor}>
|
|
||||||
<strong>{holder.person || '—'}</strong>
|
<strong>{holder.person || '—'}</strong>
|
||||||
|
{waitedMs > 0 && <span className={styles.meta}>{HISTORY_TEXT.waiting(formatDuration(waitedMs))}</span>}
|
||||||
</div>
|
</div>
|
||||||
{holder.stepTitle && <div className={styles.meta}>{holder.stepTitle}</div>}
|
{holder.stepTitle && <div className={styles.meta}>{holder.stepTitle}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge } from '@tnsu/ui-kit';
|
import { formatDuration, HistoryEventKind, IHistoryEvent } from './historyModel';
|
||||||
import { ApprovalComment, CommentTone } from '../ui/ApprovalComment';
|
import { eventAction, HISTORY_TEXT } from './historyTexts';
|
||||||
import { formatDuration, IHistoryEvent } from './historyModel';
|
|
||||||
import { BADGE_BY_TONE, KIND_VIEW } from './historyKinds';
|
|
||||||
import { eventDetails, eventLabel, HISTORY_TEXT } from './historyTexts';
|
|
||||||
import styles from './ApprovalHistory.module.scss';
|
import styles from './ApprovalHistory.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: IHistoryEvent;
|
event: IHistoryEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MARKER_CLASS: Record<CommentTone, string> = {
|
// Значок и цвет события — по смыслу решения, как у статусов в StatusBadge.
|
||||||
positive: styles.markerPositive,
|
const MARKER: Record<HistoryEventKind, { symbol: string; className: string }> = {
|
||||||
negative: styles.markerNegative,
|
submitted: { symbol: '↗', className: styles.markerNeutral },
|
||||||
warning: styles.markerWarning,
|
resubmitted: { symbol: '↗', className: styles.markerNeutral },
|
||||||
neutral: styles.markerNeutral,
|
approved: { symbol: '✓', className: styles.markerPositive },
|
||||||
|
executed: { symbol: '✓', className: styles.markerPositive },
|
||||||
|
returned: { symbol: '↩', className: styles.markerWarning },
|
||||||
|
rejected: { symbol: '✕', className: styles.markerNegative },
|
||||||
|
delegated: { symbol: '→', className: styles.markerNeutral },
|
||||||
|
replaced: { symbol: '⇄', className: styles.markerNeutral },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatDateTime(iso: string): string {
|
export function formatDateTime(iso: string): string {
|
||||||
@@ -24,35 +26,26 @@ export function formatDateTime(iso: string): string {
|
|||||||
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Одно событие ленты: время · кто — что сделал · за сколько + комментарий. */
|
||||||
* Одно событие ленты. Сверху — ЧТО произошло (цветная плашка) и когда: это то,
|
|
||||||
* за чем пользователь открывает историю. Ниже — кто, на каком шаге и почему.
|
|
||||||
*/
|
|
||||||
export const HistoryEventRow: React.FC<Props> = ({ event }) => {
|
export const HistoryEventRow: React.FC<Props> = ({ event }) => {
|
||||||
const view = KIND_VIEW[event.kind];
|
const marker = MARKER[event.kind];
|
||||||
const details = eventDetails(event);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={styles.event}>
|
<li className={styles.event}>
|
||||||
<span className={`${styles.marker} ${MARKER_CLASS[view.tone]}`} aria-hidden="true">
|
<span className={`${styles.marker} ${marker.className}`} aria-hidden="true">
|
||||||
{view.symbol}
|
{marker.symbol}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.eventBody}>
|
<div className={styles.eventBody}>
|
||||||
<div className={styles.eventHead}>
|
<div className={styles.eventLine}>
|
||||||
<Badge variant={BADGE_BY_TONE[view.tone]} size="sm">
|
|
||||||
{eventLabel(event.kind)}
|
|
||||||
</Badge>
|
|
||||||
<span className={styles.time}>{formatDateTime(event.time)}</span>
|
<span className={styles.time}>{formatDateTime(event.time)}</span>
|
||||||
|
<span>
|
||||||
|
<strong>{event.actor || '—'}</strong> — {eventAction(event)}
|
||||||
|
</span>
|
||||||
{event.durationMs !== undefined && (
|
{event.durationMs !== undefined && (
|
||||||
<span className={styles.meta}>{HISTORY_TEXT.took(formatDuration(event.durationMs))}</span>
|
<span className={styles.meta}>{HISTORY_TEXT.took(formatDuration(event.durationMs))}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actor}>
|
|
||||||
<strong>{event.actor || '—'}</strong>
|
|
||||||
{details && <span className={styles.meta}> · {details}</span>}
|
|
||||||
</div>
|
|
||||||
{event.stepTitle && <div className={styles.meta}>{event.stepTitle}</div>}
|
{event.stepTitle && <div className={styles.meta}>{event.stepTitle}</div>}
|
||||||
{event.comment && <ApprovalComment text={event.comment} tone={view.tone} />}
|
{event.comment && <div className={styles.comment}>«{event.comment}»</div>}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Badge } from '@tnsu/ui-kit';
|
|
||||||
import { RoundOutcome } from './historyModel';
|
|
||||||
import { outcomeBadge } from './historyKinds';
|
|
||||||
import { HISTORY_TEXT, outcomeLabel } from './historyTexts';
|
|
||||||
import styles from './ApprovalHistory.module.scss';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
number: number;
|
|
||||||
/** Чем закончилась отправка (см. roundOutcome); нет — только номер. */
|
|
||||||
outcome?: RoundOutcome;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Заголовок отправки: номер + плашка итога. По одним заголовкам видна вся
|
|
||||||
* судьба заявки («вернули, вернули, в работе») без чтения событий.
|
|
||||||
*/
|
|
||||||
export const RoundTitle: React.FC<Props> = ({ number, outcome }) => (
|
|
||||||
<div className={styles.roundTitle}>
|
|
||||||
<span className={styles.roundNumber}>{HISTORY_TEXT.round(number)}</span>
|
|
||||||
{outcome && (
|
|
||||||
<Badge variant={outcomeBadge(outcome)} size="sm">
|
|
||||||
{outcomeLabel(outcome)}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { BadgeVariant } from '@tnsu/ui-kit';
|
|
||||||
import { CommentTone } from '../ui/ApprovalComment';
|
|
||||||
import { HistoryEventKind, RoundOutcome } from './historyModel';
|
|
||||||
|
|
||||||
/** Как событие выглядит в ленте: значок в кружке и цвет (кружок, плашка, комментарий). */
|
|
||||||
export interface IKindView {
|
|
||||||
symbol: string;
|
|
||||||
tone: CommentTone;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Цвет — по смыслу решения, как у статусов в StatusBadge: зелёное — пройдено,
|
|
||||||
// золотое — вернули, красное — отказ, синее — служебное движение заявки.
|
|
||||||
export const KIND_VIEW: Record<HistoryEventKind, IKindView> = {
|
|
||||||
submitted: { symbol: '↗', tone: 'neutral' },
|
|
||||||
resubmitted: { symbol: '↗', tone: 'neutral' },
|
|
||||||
approved: { symbol: '✓', tone: 'positive' },
|
|
||||||
executed: { symbol: '✓', tone: 'positive' },
|
|
||||||
returned: { symbol: '↩', tone: 'warning' },
|
|
||||||
rejected: { symbol: '✕', tone: 'negative' },
|
|
||||||
delegated: { symbol: '→', tone: 'neutral' },
|
|
||||||
replaced: { symbol: '⇄', tone: 'neutral' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BADGE_BY_TONE: Record<CommentTone, BadgeVariant> = {
|
|
||||||
positive: 'success',
|
|
||||||
negative: 'danger',
|
|
||||||
warning: 'warning',
|
|
||||||
neutral: 'neutral',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Цвет плашки итога отправки: у решения — тот же, что у его события в ленте;
|
|
||||||
* «в работе» — синий, как плашка «Ожидает решения» у текущего шага.
|
|
||||||
*/
|
|
||||||
export function outcomeBadge(outcome: RoundOutcome): BadgeVariant {
|
|
||||||
return outcome === 'inProgress' ? 'info' : BADGE_BY_TONE[KIND_VIEW[outcome].tone];
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
buildApprovalHistory,
|
buildApprovalHistory,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
HistoryEventKind,
|
|
||||||
IHistoryRound,
|
|
||||||
IRequestVersion,
|
IRequestVersion,
|
||||||
IRouteRowVersion,
|
IRouteRowVersion,
|
||||||
roundOutcome,
|
|
||||||
rowEvents,
|
rowEvents,
|
||||||
submitEvents,
|
submitEvents,
|
||||||
} from './historyModel';
|
} from './historyModel';
|
||||||
@@ -134,44 +131,6 @@ describe('buildApprovalHistory', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Отправка из событий заданных видов — в том же порядке, что в ленте: НОВЫЕ СВЕРХУ. */
|
|
||||||
function round(...kinds: HistoryEventKind[]): IHistoryRound {
|
|
||||||
return { number: 1, events: kinds.map((kind) => ({ kind, time: '2026-09-24T10:00:00Z', actor: 'Анна Ким' })) };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('roundOutcome', () => {
|
|
||||||
it('возврат — итог отправки', () => {
|
|
||||||
expect(roundOutcome(round('returned', 'approved', 'submitted'), false)).toBe('returned');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('отказ — итог отправки', () => {
|
|
||||||
expect(roundOutcome(round('rejected', 'submitted'), false)).toBe('rejected');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('возврат важнее «в работе»: вернули, а заново ещё не отправили', () => {
|
|
||||||
expect(roundOutcome(round('returned', 'submitted'), true)).toBe('returned');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('заявка сейчас у кого-то на шаге — «в работе», даже если часть шагов уже согласована', () => {
|
|
||||||
expect(roundOutcome(round('resubmitted'), true)).toBe('inProgress');
|
|
||||||
expect(roundOutcome(round('approved', 'resubmitted'), true)).toBe('inProgress');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('без возврата и не в работе — самое свежее решение', () => {
|
|
||||||
expect(roundOutcome(round('approved', 'approved', 'resubmitted'), false)).toBe('approved');
|
|
||||||
expect(roundOutcome(round('executed', 'approved', 'submitted'), false)).toBe('executed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('делегирование и замена решением не считаются', () => {
|
|
||||||
expect(roundOutcome(round('delegated', 'approved', 'submitted'), false)).toBe('approved');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('решений нет — итога нет', () => {
|
|
||||||
expect(roundOutcome(round('delegated', 'submitted'), false)).toBeUndefined();
|
|
||||||
expect(roundOutcome(round(), false)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatDuration', () => {
|
describe('formatDuration', () => {
|
||||||
it('минуты, часы, дни', () => {
|
it('минуты, часы, дни', () => {
|
||||||
expect(formatDuration(30 * 1000)).toBe('1 мин');
|
expect(formatDuration(30 * 1000)).toBe('1 мин');
|
||||||
|
|||||||
@@ -181,42 +181,6 @@ export function buildApprovalHistory(rows: IRouteRowVersion[][], requestVersions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Виды событий-решений — только ими может закончиться отправка. */
|
|
||||||
export type DecisionKind = 'approved' | 'executed' | 'returned' | 'rejected';
|
|
||||||
|
|
||||||
/** Итог отправки для плашки в её заголовке. 'inProgress' — заявка сейчас у кого-то на шаге. */
|
|
||||||
export type RoundOutcome = DecisionKind | 'inProgress';
|
|
||||||
|
|
||||||
const DECISION_EVENT_KINDS: HistoryEventKind[] = ['approved', 'executed', 'returned', 'rejected'];
|
|
||||||
// Решения, после которых отправка заканчивается: дальше автор либо правит
|
|
||||||
// и отправляет заново (новая отправка), либо заявка закрыта отказом.
|
|
||||||
const CLOSING_KINDS: DecisionKind[] = ['returned', 'rejected'];
|
|
||||||
|
|
||||||
function isDecisionKind(kind: HistoryEventKind): kind is DecisionKind {
|
|
||||||
return DECISION_EVENT_KINDS.indexOf(kind) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Чем закончилась отправка. Правила по порядку:
|
|
||||||
* 1. был возврат или отказ — это итог (отправка на нём оборвалась);
|
|
||||||
* 2. заявка сейчас у кого-то на шаге — «в работе»;
|
|
||||||
* 3. иначе — последнее решение («согласовано» / «выполнено»);
|
|
||||||
* 4. решений нет (история сохранилась не полностью) — итога нет.
|
|
||||||
* @param round отправка; события в ней НОВЫЕ СВЕРХУ (см. buildApprovalHistory)
|
|
||||||
* @param isActive у кого-то на шаге сейчас именно эта отправка — имеет смысл
|
|
||||||
* только для последней (верхней)
|
|
||||||
*/
|
|
||||||
export function roundOutcome(round: IHistoryRound, isActive: boolean): RoundOutcome | undefined {
|
|
||||||
for (const event of round.events) {
|
|
||||||
if (isDecisionKind(event.kind) && CLOSING_KINDS.indexOf(event.kind) >= 0) return event.kind;
|
|
||||||
}
|
|
||||||
if (isActive) return 'inProgress';
|
|
||||||
for (const event of round.events) {
|
|
||||||
if (isDecisionKind(event.kind)) return event.kind;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MINUTE_MS = 60 * 1000;
|
const MINUTE_MS = 60 * 1000;
|
||||||
const HOUR_MS = 60 * MINUTE_MS;
|
const HOUR_MS = 60 * MINUTE_MS;
|
||||||
const DAY_MS = 24 * HOUR_MS;
|
const DAY_MS = 24 * HOUR_MS;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HistoryEventKind, IHistoryEvent, RoundOutcome } from './historyModel';
|
import { HistoryEventKind, IHistoryEvent } from './historyModel';
|
||||||
|
|
||||||
// Тексты «Истории согласования» — в одном месте (задел под локализацию).
|
// Тексты «Истории согласования» — в одном месте (задел под локализацию).
|
||||||
// Формулировки безличные («согласовано», а не «согласовала»): пол по имени не
|
// Формулировки безличные («согласовано», а не «согласовала»): пол по имени не
|
||||||
@@ -9,43 +9,28 @@ export const HISTORY_TEXT = {
|
|||||||
empty: 'Пока нет событий: заявка ещё не отправлялась.',
|
empty: 'Пока нет событий: заявка ещё не отправлялась.',
|
||||||
errorPrefix: 'Не удалось загрузить историю:',
|
errorPrefix: 'Не удалось загрузить историю:',
|
||||||
round: (n: number): string => `Отправка ${n}`,
|
round: (n: number): string => `Отправка ${n}`,
|
||||||
inProgress: 'В работе',
|
now: 'сейчас',
|
||||||
awaiting: 'Ожидает решения',
|
waiting: (duration: string): string => `ждёт ${duration}`,
|
||||||
waiting: (duration: string): string => `уже ${duration}`,
|
|
||||||
took: (duration: string): string => `за ${duration}`,
|
took: (duration: string): string => `за ${duration}`,
|
||||||
delegatedTo: (name: string): string => `кому: ${name}`,
|
|
||||||
partialNote:
|
partialNote:
|
||||||
'Показаны события, сохранённые в SharePoint. Решения из отправок до включения истории могли не сохраниться.',
|
'Показаны события, сохранённые в SharePoint. Решения из отправок до включения истории могли не сохраниться.',
|
||||||
};
|
};
|
||||||
|
|
||||||
// С заглавной: текст стоит первым словом в плашке события.
|
const ACTION: Record<HistoryEventKind, string> = {
|
||||||
const ACTION_LABEL: Record<HistoryEventKind, string> = {
|
submitted: 'отправлено на согласование',
|
||||||
submitted: 'Отправлено на согласование',
|
resubmitted: 'отправлено повторно',
|
||||||
resubmitted: 'Отправлено повторно',
|
approved: 'согласовано',
|
||||||
approved: 'Согласовано',
|
executed: 'выполнено',
|
||||||
executed: 'Выполнено',
|
returned: 'возвращено на доработку',
|
||||||
returned: 'Возвращено на доработку',
|
rejected: 'отклонено',
|
||||||
rejected: 'Отклонено',
|
delegated: 'передано',
|
||||||
delegated: 'Передано',
|
replaced: 'заменён исполнитель',
|
||||||
replaced: 'Заменён исполнитель',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Что произошло — текст плашки: «Согласовано», «Возвращено на доработку». */
|
/** Что произошло — «согласовано», «передано: Асель Самаева», «заменён исполнитель: А → Б». */
|
||||||
export function eventLabel(kind: HistoryEventKind): string {
|
export function eventAction(event: IHistoryEvent): string {
|
||||||
return ACTION_LABEL[kind];
|
const action = ACTION[event.kind];
|
||||||
}
|
if (event.kind === 'delegated' && event.target) return `${action}: ${event.target}`;
|
||||||
|
if (event.kind === 'replaced') return `${action}: ${event.previous || '—'} → ${event.target || '—'}`;
|
||||||
/** Итог отправки — текст плашки в её заголовке. Решения — теми же словами, что в ленте. */
|
return action;
|
||||||
export function outcomeLabel(outcome: RoundOutcome): string {
|
|
||||||
return outcome === 'inProgress' ? HISTORY_TEXT.inProgress : ACTION_LABEL[outcome];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Над кем действие — дописывается к имени автора: «кому: Асель Самаева» для
|
|
||||||
* передачи, «Иванов → Петров» для замены. У прочих событий пусто.
|
|
||||||
*/
|
|
||||||
export function eventDetails(event: IHistoryEvent): string {
|
|
||||||
if (event.kind === 'delegated' && event.target) return HISTORY_TEXT.delegatedTo(event.target);
|
|
||||||
if (event.kind === 'replaced') return `${event.previous || '—'} → ${event.target || '—'}`;
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ $quote-line-height: 19px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Цвет — у полосы и кавычки, фон лишь подтон: текст должен читаться так же
|
// Цвет — у полосы и кавычки, фон лишь подтон: текст должен читаться так же
|
||||||
// спокойно, как остальная таблица, а не кричать. Белую карточку пробовали —
|
// спокойно, как остальная таблица, а не кричать.
|
||||||
// внутри формы она похожа на поле ввода.
|
|
||||||
.neutral {
|
.neutral {
|
||||||
border-left-color: $navy-600;
|
border-left-color: $navy-600;
|
||||||
background: $navy-50;
|
background: $navy-50;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import styles from './ApprovalComment.module.scss';
|
|||||||
import { REQUEST_STATUS } from '../models/statuses';
|
import { REQUEST_STATUS } from '../models/statuses';
|
||||||
import { useTextOverflow } from './useTextOverflow';
|
import { useTextOverflow } from './useTextOverflow';
|
||||||
|
|
||||||
export type CommentTone = 'positive' | 'negative' | 'warning' | 'neutral';
|
type CommentTone = 'positive' | 'negative' | 'warning' | 'neutral';
|
||||||
|
|
||||||
// Цвет полосы — по решению на шаге: причину отказа или возврата видно с первого
|
// Цвет полосы — по решению на шаге: причину отказа или возврата видно с первого
|
||||||
// взгляда, ещё до того, как глаз дойдёт до колонки «Статус».
|
// взгляда, ещё до того, как глаз дойдёт до колонки «Статус».
|
||||||
@@ -31,23 +31,18 @@ export interface IApprovalCommentProps {
|
|||||||
text: string;
|
text: string;
|
||||||
/** Статус строки маршрута (см. REQUEST_STATUS) — от него цвет полосы. */
|
/** Статус строки маршрута (см. REQUEST_STATUS) — от него цвет полосы. */
|
||||||
status?: string;
|
status?: string;
|
||||||
/**
|
|
||||||
* Цвет полосы напрямую — для мест, где статуса строки нет (напр. лента
|
|
||||||
* «Истории согласования» знает вид события, а не статус). Важнее status.
|
|
||||||
*/
|
|
||||||
tone?: CommentTone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Комментарий участника маршрута к своему решению. Длинный текст свёрнут до
|
* Комментарий участника маршрута к своему решению. Длинный текст свёрнут до
|
||||||
* нескольких строк (см. $comment-visible-lines), остальное — по кнопке.
|
* нескольких строк (см. $comment-visible-lines), остальное — по кнопке.
|
||||||
*/
|
*/
|
||||||
export const ApprovalComment: React.FC<IApprovalCommentProps> = ({ text, status, tone: toneProp }) => {
|
export const ApprovalComment: React.FC<IApprovalCommentProps> = ({ text, status }) => {
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
const textRef = React.useRef<HTMLDivElement>(null);
|
const textRef = React.useRef<HTMLDivElement>(null);
|
||||||
const isClamped = useTextOverflow(textRef, text);
|
const isClamped = useTextOverflow(textRef, text);
|
||||||
|
|
||||||
const tone = toneProp || (status && TONE_BY_STATUS[status]) || 'neutral';
|
const tone = (status && TONE_BY_STATUS[status]) || 'neutral';
|
||||||
// В развёрнутом виде обрезки нет, и замер честно вернёт false, но кнопка
|
// В развёрнутом виде обрезки нет, и замер честно вернёт false, но кнопка
|
||||||
// «Свернуть» при этом всё равно нужна.
|
// «Свернуть» при этом всё равно нужна.
|
||||||
const showToggle = isExpanded || isClamped;
|
const showToggle = isExpanded || isClamped;
|
||||||
|
|||||||
@@ -211,8 +211,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
|
|
||||||
// Комментарий занимает всю ширину между объединёнными колонками «Шаг» и
|
// Комментарий занимает всю ширину между объединёнными колонками «Шаг» и
|
||||||
// стрелок: «Согласующий» + «Должность» + те необязательные колонки, что
|
// стрелок: «Согласующий» + «Должность» + те необязательные колонки, что
|
||||||
// сейчас показаны. Колонку действий не захватывает — её ячейка из строки
|
// сейчас показаны. Колонку действий не захватывает — в ней своя пустая ячейка.
|
||||||
// участника объединена и на строку комментария (rowSpan у .matrixActions).
|
|
||||||
const optionalColumns = [hasDepartment, hasOrganization, hasStatuses, hasDates, hasSla];
|
const optionalColumns = [hasDepartment, hasOrganization, hasStatuses, hasDates, hasSla];
|
||||||
const commentColSpan = 2 + optionalColumns.filter(Boolean).length;
|
const commentColSpan = 2 + optionalColumns.filter(Boolean).length;
|
||||||
const hasActionsCol = editable || hasRemovable;
|
const hasActionsCol = editable || hasRemovable;
|
||||||
@@ -338,11 +337,8 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{/* Кнопки — на участника вместе с его комментарием, как номер
|
|
||||||
шага и стрелки: иначе стрелки стоят по центру пары строк,
|
|
||||||
а кнопки — по центру одной, и они расходятся по высоте. */}
|
|
||||||
{hasActionsCol && (
|
{hasActionsCol && (
|
||||||
<td className={styles.matrixActions} rowSpan={r.comment ? 2 : 1}>
|
<td className={styles.matrixActions}>
|
||||||
{editable && (
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -383,13 +379,14 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
{/* Комментарий — отдельной строкой во всю ширину под участником:
|
{/* Комментарий — отдельной строкой во всю ширину под участником:
|
||||||
в узкой колонке «Согласующий» текст ломался по слову на строку.
|
в узкой колонке «Согласующий» текст ломался по слову на строку.
|
||||||
Ячейки «Шаг», стрелок и действий её не повторяют — их накрывает
|
Ячейки «Шаг» и стрелок её не повторяют — их накрывает rowSpan
|
||||||
rowSpan из строки участника. */}
|
(см. block.span); колонка действий — пустая ячейка. */}
|
||||||
{r.comment && (
|
{r.comment && (
|
||||||
<tr ref={registerRow(commentRowKey(rowKeys[i]))} className={commentClassNames.join(' ')}>
|
<tr ref={registerRow(commentRowKey(rowKeys[i]))} className={commentClassNames.join(' ')}>
|
||||||
<td colSpan={commentColSpan} className={styles.commentCell}>
|
<td colSpan={commentColSpan} className={styles.commentCell}>
|
||||||
<ApprovalComment text={r.comment} status={r.status} />
|
<ApprovalComment text={r.comment} status={r.status} />
|
||||||
</td>
|
</td>
|
||||||
|
{hasActionsCol && <td />}
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -159,10 +159,9 @@
|
|||||||
|
|
||||||
// Строка участника, под которой идёт его комментарий (см. ApprovalComment):
|
// Строка участника, под которой идёт его комментарий (см. ApprovalComment):
|
||||||
// разделитель между ними убираем, иначе комментарий читается как отдельная
|
// разделитель между ними убираем, иначе комментарий читается как отдельная
|
||||||
// запись маршрута. Объединённые ячейки (rowSpan) не трогаем: «Шаг» и стрелки
|
// запись маршрута. Ячейки «Шаг» и стрелок не трогаем — они объединены на весь
|
||||||
// накрывают весь шаг, действия — участника с комментарием, и их нижняя
|
// шаг (rowSpan), и их нижняя граница — это граница всего шага.
|
||||||
// граница — это граница всей записи.
|
.rowWithComment > td:not(.matrixStepCol):not(.matrixMoveCol) {
|
||||||
.rowWithComment > td:not(.matrixStepCol):not(.matrixMoveCol):not(.matrixActions) {
|
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +388,11 @@
|
|||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пустая ячейка под колонкой действий на телефоне дала бы лишний отступ.
|
||||||
|
.commentRow > td:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.matrixActions,
|
.matrixActions,
|
||||||
.matrixMoveCol {
|
.matrixMoveCol {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Alert } from '@tnsu/ui-kit';
|
import { Alert } from '@tnsu/ui-kit';
|
||||||
import styles from './FormNotice.module.scss';
|
import styles from './LeaveNotice.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Заголовок плашки, например «Примечание» или «Памятка». */
|
/** Заголовок плашки, например «Примечание» или «Памятка». */
|
||||||
@@ -10,13 +10,12 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Плашка с правилами вверху формы и её просмотра — «Примечание», «Памятка»
|
* Плашка с правилами для заявлений на отпуск (трудовой и без содержания).
|
||||||
* (отпуск, отпуск без содержания, увольнение). Тексты у форм разные — от
|
* Тексты у форм разные, поэтому приходят из model.ts каждой формы.
|
||||||
* кадров, дословно, — поэтому приходят из model.ts каждой формы.
|
|
||||||
* Вариант warn, а не info: нейтральная плашка терялась на фоне формы.
|
* Вариант warn, а не info: нейтральная плашка терялась на фоне формы.
|
||||||
* На печать не выводится — это подсказка для сотрудника, а не часть заявления.
|
* На печать не выводится — это подсказка для сотрудника, а не часть заявления.
|
||||||
*/
|
*/
|
||||||
export const FormNotice: React.FC<Props> = ({ title, items }) => {
|
export const LeaveNotice: React.FC<Props> = ({ title, items }) => {
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@import '../../../../../styles/design-tokens';
|
|
||||||
|
|
||||||
// Длина линии ≈ 5 см на листе A4 — хватает на роспись от руки.
|
|
||||||
$signature-line-width: 200px;
|
|
||||||
// Место под роспись между ФИО и линией.
|
|
||||||
$signature-height: $sp-8;
|
|
||||||
|
|
||||||
// На экране блока нет: расписываться негде.
|
|
||||||
.signature {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
// Столбик, а не строка «ФИО ____»: в шапке рядом с заголовком места мало,
|
|
||||||
// строка в ширину сжала бы заголовок до трёх строк.
|
|
||||||
.signature {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
// min-width, а не width: длинное ФИО растягивает столбик (и линию), а не
|
|
||||||
// вылезает за край листа.
|
|
||||||
min-width: $signature-line-width;
|
|
||||||
font-size: $fs-sm;
|
|
||||||
color: #000;
|
|
||||||
break-inside: avoid;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
white-space: nowrap; // перенос фамилии на бланке выглядит как опечатка
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
align-self: stretch;
|
|
||||||
height: $signature-height;
|
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.caption {
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: $fs-caption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import styles from './PrintSignature.module.scss';
|
|
||||||
|
|
||||||
const CAPTION = '(подпись)';
|
|
||||||
|
|
||||||
interface IPrintSignatureProps {
|
|
||||||
/** ФИО того, кто подписывает. Пусто — печатается только линия. */
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Место для подписи на печатном заявлении — столбиком: ФИО, под ним место под
|
|
||||||
* роспись и линия, под линией «(подпись)». Узкий столбик рассчитан на правый
|
|
||||||
* верхний угол шапки (FormCard headerRight), как угол бумажного заявления.
|
|
||||||
* Видно только при печати — на экране расписываться негде.
|
|
||||||
*/
|
|
||||||
export const PrintSignature: React.FC<IPrintSignatureProps> = ({ name }) => (
|
|
||||||
<div className={styles.signature}>
|
|
||||||
{name && <span className={styles.name}>{name}</span>}
|
|
||||||
<span className={styles.line} />
|
|
||||||
<span className={styles.caption}>{CAPTION}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -50,7 +50,6 @@ export { OrganizationField } from './OrganizationField/OrganizationField';
|
|||||||
export { OrganizationTable } from './OrganizationField/OrganizationTable';
|
export { OrganizationTable } from './OrganizationField/OrganizationTable';
|
||||||
export { PrintSplitButton } from './PrintSplitButton/PrintSplitButton';
|
export { PrintSplitButton } from './PrintSplitButton/PrintSplitButton';
|
||||||
export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
|
export { OrgLetterhead } from './OrgLetterhead/OrgLetterhead';
|
||||||
export { PrintSignature } from './PrintSignature/PrintSignature';
|
|
||||||
export { LinkField } from './LinkField/LinkField';
|
export { LinkField } from './LinkField/LinkField';
|
||||||
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
export { ErrorOverlay } from './ErrorOverlay/ErrorOverlay';
|
||||||
export { SubmitActions } from './SubmitActions/SubmitActions';
|
export { SubmitActions } from './SubmitActions/SubmitActions';
|
||||||
@@ -60,6 +59,6 @@ export { issueLabels } from './FieldIssues/fieldIssues';
|
|||||||
export { getDraftDeleteHandler } from './SubmitActions/getDraftDeleteHandler';
|
export { getDraftDeleteHandler } from './SubmitActions/getDraftDeleteHandler';
|
||||||
export { InitiatorLine } from './InitiatorLine/InitiatorLine';
|
export { InitiatorLine } from './InitiatorLine/InitiatorLine';
|
||||||
export type { IInitiatorLineProps } from './InitiatorLine/InitiatorLine';
|
export type { IInitiatorLineProps } from './InitiatorLine/InitiatorLine';
|
||||||
export { FormNotice } from './FormNotice/FormNotice';
|
export { LeaveNotice } from './LeaveNotice/LeaveNotice';
|
||||||
export { useRequestSubmit } from './useRequestSubmit';
|
export { useRequestSubmit } from './useRequestSubmit';
|
||||||
export type { IRequestSubmit, SubmitKind } from './useRequestSubmit';
|
export type { IRequestSubmit, SubmitKind } from './useRequestSubmit';
|
||||||
|
|||||||
Reference in New Issue
Block a user