diff --git a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.module.scss b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.module.scss index 82bbc32..c964cc5 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.module.scss +++ b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.module.scss @@ -19,6 +19,15 @@ color: $navy-900; } +// Только для окна печати (PrintButton печатает с media print — там блок виден). +.printOnly { + margin-top: $sp-6; + + @media screen { + display: none; + } +} + .toolbar { display: flex; flex-wrap: wrap; diff --git a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx index 6c9c87a..d00702b 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx +++ b/src/webparts/requestPortal/forms/SkudExplanation/SkudExplanationView.tsx @@ -7,11 +7,11 @@ import { ApproverService, DecisionPanel, StatusBadge, - ApprovalMatrix, ApprovalHistory, notifyActiveStep, useRequestTypeId } from '../../shared/approval'; +import type { ISiteUser } from '../../shared/approval'; import { friendlyErrorMessage } from '../../shared/diagnostics'; import { approvalListTitle, @@ -28,6 +28,7 @@ import { EmployeeSection } from './sections/EmployeeSection'; import { DaysSection } from './sections/DaysSection'; import { ExplanationSection } from './sections/ExplanationSection'; import { NoteDocument } from './components/NoteDocument'; +import { SkudRouteTables } from './components/SkudRouteTables'; interface ISkudExplanationViewProps { context: WebPartContext; @@ -44,7 +45,8 @@ const NOOP_SET: SetField = () => undefined; /** * Объяснительная для согласующего: данные заявки (только чтение) + документ - * записки. На печать идёт только документ (rp-print-root) — как бумажный бланк. + * записки. На печать идёт только rp-print-root: документ как бумажный бланк и + * под ним маршрут согласования/исполнения вместо подписи. */ const SkudExplanationView: React.FC = ({ context, itemId, formKey, listId, onCodeLoaded }) => { const notifyCode = useStableCallback(onCodeLoaded); @@ -119,6 +121,7 @@ const SkudExplanationView: React.FC = ({ context, ite }; const sectionProps = { data, set: NOOP_SET, context, printOrgIndex }; + const searchUsers = (query: string): Promise => approval.searchUsers(query); return (
@@ -150,21 +153,15 @@ const SkudExplanationView: React.FC = ({ context, ite {/* Created — запасной вариант для заявок, сохранённых до появления «Даты подачи». */} + {/* На экране маршрут уже есть под карточкой — копия нужна только в окне печати. */} +
+ +
- approval.searchUsers(q)} /> - - {!executors.loading && executors.rows.length > 0 && ( - approval.searchUsers(q)} - title="Исполнение" - roleLabel="Исполнитель" - emptyText="Исполнители не назначены." - /> - )} +
diff --git a/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.module.scss b/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.module.scss index dd7b7aa..952494c 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.module.scss +++ b/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.module.scss @@ -4,9 +4,10 @@ // красная строка — как в образце объяснительной отдела кадров. $doc-font: 'Times New Roman', Times, serif; $paragraph-indent: 2.5em; -$sign-line-width: 120px; .document { + // border-box: иначе padding и рамка прибавляются к 100% и документ вылезает за карточку. + box-sizing: border-box; width: 100%; padding: $sp-4; border: 1px solid $gray-200; @@ -57,15 +58,3 @@ $sign-line-width: 120px; margin-top: $sp-8; font-weight: 600; } - -.signature { - display: inline-flex; - align-items: flex-end; - gap: $sp-2; -} - -.signLine { - display: inline-block; - width: $sign-line-width; - border-bottom: 1px solid $gray-900; -} diff --git a/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.tsx b/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.tsx index 86f8a5f..a07d96e 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.tsx +++ b/src/webparts/requestPortal/forms/SkudExplanation/components/NoteDocument.tsx @@ -19,7 +19,7 @@ function companyOf(organization: string, index: number): string { /** * Объяснительная записка в виде бумажного документа (как бланк отдела кадров): - * шапка справа, заголовок, текст, дата и подпись. Предпросмотр в форме и + * шапка справа, заголовок, текст, дата и ФИО. Предпросмотр в форме и * печатная форма в просмотре — один и тот же компонент. */ export const NoteDocument: React.FC = ({ data, noteDate, orgIndex = 0 }) => { @@ -42,12 +42,10 @@ export const NoteDocument: React.FC = ({ data, noteDate, org

))} + {/* Без линии для подписи: подписью служит маршрут согласования, который печатается под запиской. */}
{formatShortDate(noteDate)} г. - - - {name} - + {name}
); diff --git a/src/webparts/requestPortal/forms/SkudExplanation/components/SkudRouteTables.tsx b/src/webparts/requestPortal/forms/SkudExplanation/components/SkudRouteTables.tsx new file mode 100644 index 0000000..19782e9 --- /dev/null +++ b/src/webparts/requestPortal/forms/SkudExplanation/components/SkudRouteTables.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { ApprovalMatrix } from '../../../shared/approval'; +import type { ISiteUser, IUseMatrixEditor } from '../../../shared/approval'; + +interface ISkudRouteTablesProps { + approvers: IUseMatrixEditor; + executors: IUseMatrixEditor; + search: (query: string) => Promise; +} + +/** + * Маршрут согласования + исполнение (если исполнители есть). Рисуется дважды: + * на экране под карточкой и внутри печатного блока под запиской — на печати + * таблица маршрута заменяет подпись сотрудника. + */ +export const SkudRouteTables: React.FC = ({ approvers, executors, search }) => { + const hasExecutors = !executors.loading && executors.rows.length > 0; + + return ( + <> + + {hasExecutors && ( + + )} + + ); +}; diff --git a/src/webparts/requestPortal/forms/SkudExplanation/noteText.test.ts b/src/webparts/requestPortal/forms/SkudExplanation/noteText.test.ts index 24113fd..1b17566 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/noteText.test.ts +++ b/src/webparts/requestPortal/forms/SkudExplanation/noteText.test.ts @@ -43,7 +43,21 @@ describe('noteText', () => { ReasonDetails: 'был на объекте', PresenceConfirmed: false }); - expect(second).toBe('Отсутствие отметки в системе СКУД в указанную дату произошло по следующей причине: был на объекте'); + expect(second).toBe('Отсутствие отметки в системе СКУД в указанную дату произошло по следующей причине: был на объекте.'); + }); + + it('«Другое»: свою точку сотрудника не дублирует, фраза о подтверждении не прилипает', () => { + const [, second] = buildNoteParagraphs({ + ...DATA, + MissedDays: [DATA.MissedDays[0]], + Reason: OTHER_REASON, + ReasonDetails: 'был на объекте.', + PresenceConfirmed: true + }); + expect(second).toBe( + 'Отсутствие отметки в системе СКУД в указанную дату произошло по следующей причине: был на объекте. ' + + 'Факт моего нахождения на рабочем месте и исполнения трудовых обязанностей в течение рабочего дня подтверждаю.' + ); }); it('сводка дней для колонки SP', () => { diff --git a/src/webparts/requestPortal/forms/SkudExplanation/noteText.ts b/src/webparts/requestPortal/forms/SkudExplanation/noteText.ts index 41dc964..44ed0e6 100644 --- a/src/webparts/requestPortal/forms/SkudExplanation/noteText.ts +++ b/src/webparts/requestPortal/forms/SkudExplanation/noteText.ts @@ -62,9 +62,14 @@ export function formatMissedDaysSummary(days: IMissedDay[]): string { return days.map((d) => `${formatShortDate(d.date)} (${formatMissedDetails(d)})`).join('; '); } +/** Текст сотрудника — законченное предложение: без точки следующая фраза записки к нему прилипает. */ +function withFinalPunctuation(text: string): string { + return /[.!?…]$/.test(text) ? text : `${text}.`; +} + function reasonSentence(data: IFormData, dateWord: string): string { if (data.Reason === OTHER_REASON) { - const details = data.ReasonDetails.trim() || BLANK; + const details = withFinalPunctuation(data.ReasonDetails.trim() || BLANK); return `Отсутствие отметки в системе СКУД в ${dateWord} произошло по следующей причине: ${details}`; } const reason = SKUD_REASONS.find((r) => r.value === data.Reason);