feat: печатный вид объяснительной по СКУД с маршрутом согласования
Tests / frontend-test (push) Successful in 1m21s
Tests / frontend-test (push) Successful in 1m21s
Вместо линии для подписи под запиской печатается маршрут согласования и исполнения. Документ больше не вылезает за карточку (box-sizing), а текст причины «Другое» получает точку, чтобы следующая фраза к нему не прилипала. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<ISkudExplanationViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||
const notifyCode = useStableCallback(onCodeLoaded);
|
||||
@@ -119,6 +121,7 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
|
||||
};
|
||||
|
||||
const sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
|
||||
const searchUsers = (query: string): Promise<ISiteUser[]> => approval.searchUsers(query);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -150,21 +153,15 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
|
||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||
{/* Created — запасной вариант для заявок, сохранённых до появления «Даты подачи». */}
|
||||
<NoteDocument data={data} noteDate={data.SubmitDate || created} orgIndex={printOrgIndex} />
|
||||
{/* На экране маршрут уже есть под карточкой — копия нужна только в окне печати. */}
|
||||
<div className={styles.printOnly}>
|
||||
<SkudRouteTables approvers={approvers} executors={executors} search={searchUsers} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCard>
|
||||
|
||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||
|
||||
{!executors.loading && executors.rows.length > 0 && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
/>
|
||||
)}
|
||||
<SkudRouteTables approvers={approvers} executors={executors} search={searchUsers} />
|
||||
|
||||
<div className="rp-no-print">
|
||||
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
|
||||
|
||||
+2
-13
@@ -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;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function companyOf(organization: string, index: number): string {
|
||||
|
||||
/**
|
||||
* Объяснительная записка в виде бумажного документа (как бланк отдела кадров):
|
||||
* шапка справа, заголовок, текст, дата и подпись. Предпросмотр в форме и
|
||||
* шапка справа, заголовок, текст, дата и ФИО. Предпросмотр в форме и
|
||||
* печатная форма в просмотре — один и тот же компонент.
|
||||
*/
|
||||
export const NoteDocument: React.FC<INoteDocumentProps> = ({ data, noteDate, orgIndex = 0 }) => {
|
||||
@@ -42,12 +42,10 @@ export const NoteDocument: React.FC<INoteDocumentProps> = ({ data, noteDate, org
|
||||
</p>
|
||||
))}
|
||||
|
||||
{/* Без линии для подписи: подписью служит маршрут согласования, который печатается под запиской. */}
|
||||
<div className={styles.footer}>
|
||||
<span>{formatShortDate(noteDate)} г.</span>
|
||||
<span className={styles.signature}>
|
||||
<span className={styles.signLine} />
|
||||
{name}
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<ISiteUser[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Маршрут согласования + исполнение (если исполнители есть). Рисуется дважды:
|
||||
* на экране под карточкой и внутри печатного блока под запиской — на печати
|
||||
* таблица маршрута заменяет подпись сотрудника.
|
||||
*/
|
||||
export const SkudRouteTables: React.FC<ISkudRouteTablesProps> = ({ approvers, executors, search }) => {
|
||||
const hasExecutors = !executors.loading && executors.rows.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ApprovalMatrix editor={approvers} search={search} />
|
||||
{hasExecutors && (
|
||||
<ApprovalMatrix
|
||||
editor={executors}
|
||||
search={search}
|
||||
title="Исполнение"
|
||||
roleLabel="Исполнитель"
|
||||
emptyText="Исполнители не назначены."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user