Compare commits
3 Commits
7e9848e83d
...
4f292dde3d
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f292dde3d | |||
| a05a336275 | |||
| ff7d1f26a7 |
@@ -22,6 +22,13 @@
|
|||||||
color: #000 !important;
|
color: #000 !important;
|
||||||
border-color: #ccc !important;
|
border-color: #ccc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// У выбранного человека в PeoplePicker (Fluent) есть кнопка «×». В окне печати
|
||||||
|
// svg-иконки скрыты, а стили Fluent туда не копируются — остаётся пустая
|
||||||
|
// системная кнопка-квадратик под ФИО. На печати удалять некого — скрываем.
|
||||||
|
.rp-print-root .ms-PickerItem-removeButton {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,15 @@
|
|||||||
color: $navy-900;
|
color: $navy-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Только для окна печати (PrintButton печатает с media print — там блок виден).
|
||||||
|
.printOnly {
|
||||||
|
margin-top: $sp-6;
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import {
|
|||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ApprovalMatrix,
|
|
||||||
ApprovalHistory,
|
ApprovalHistory,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
|
import type { ISiteUser } from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
@@ -28,6 +28,7 @@ import { EmployeeSection } from './sections/EmployeeSection';
|
|||||||
import { DaysSection } from './sections/DaysSection';
|
import { DaysSection } from './sections/DaysSection';
|
||||||
import { ExplanationSection } from './sections/ExplanationSection';
|
import { ExplanationSection } from './sections/ExplanationSection';
|
||||||
import { NoteDocument } from './components/NoteDocument';
|
import { NoteDocument } from './components/NoteDocument';
|
||||||
|
import { SkudRouteTables } from './components/SkudRouteTables';
|
||||||
|
|
||||||
interface ISkudExplanationViewProps {
|
interface ISkudExplanationViewProps {
|
||||||
context: WebPartContext;
|
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 SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
const notifyCode = useStableCallback(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 sectionProps = { data, set: NOOP_SET, context, printOrgIndex };
|
||||||
|
const searchUsers = (query: string): Promise<ISiteUser[]> => approval.searchUsers(query);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -150,21 +153,15 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
|
|||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
{/* Created — запасной вариант для заявок, сохранённых до появления «Даты подачи». */}
|
{/* Created — запасной вариант для заявок, сохранённых до появления «Даты подачи». */}
|
||||||
<NoteDocument data={data} noteDate={data.SubmitDate || created} orgIndex={printOrgIndex} />
|
<NoteDocument data={data} noteDate={data.SubmitDate || created} orgIndex={printOrgIndex} />
|
||||||
|
{/* На экране маршрут уже есть под карточкой — копия нужна только в окне печати. */}
|
||||||
|
<div className={styles.printOnly}>
|
||||||
|
<SkudRouteTables approvers={approvers} executors={executors} search={searchUsers} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<SkudRouteTables approvers={approvers} executors={executors} search={searchUsers} />
|
||||||
|
|
||||||
{!executors.loading && executors.rows.length > 0 && (
|
|
||||||
<ApprovalMatrix
|
|
||||||
editor={executors}
|
|
||||||
search={(q) => approval.searchUsers(q)}
|
|
||||||
title="Исполнение"
|
|
||||||
roleLabel="Исполнитель"
|
|
||||||
emptyText="Исполнители не назначены."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rp-no-print">
|
<div className="rp-no-print">
|
||||||
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
|
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
|
||||||
|
|||||||
+2
-13
@@ -4,9 +4,10 @@
|
|||||||
// красная строка — как в образце объяснительной отдела кадров.
|
// красная строка — как в образце объяснительной отдела кадров.
|
||||||
$doc-font: 'Times New Roman', Times, serif;
|
$doc-font: 'Times New Roman', Times, serif;
|
||||||
$paragraph-indent: 2.5em;
|
$paragraph-indent: 2.5em;
|
||||||
$sign-line-width: 120px;
|
|
||||||
|
|
||||||
.document {
|
.document {
|
||||||
|
// border-box: иначе padding и рамка прибавляются к 100% и документ вылезает за карточку.
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: $sp-4;
|
padding: $sp-4;
|
||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
@@ -57,15 +58,3 @@ $sign-line-width: 120px;
|
|||||||
margin-top: $sp-8;
|
margin-top: $sp-8;
|
||||||
font-weight: 600;
|
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 }) => {
|
export const NoteDocument: React.FC<INoteDocumentProps> = ({ data, noteDate, orgIndex = 0 }) => {
|
||||||
@@ -42,12 +42,10 @@ export const NoteDocument: React.FC<INoteDocumentProps> = ({ data, noteDate, org
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Без линии для подписи: подписью служит маршрут согласования, который печатается под запиской. */}
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<span>{formatShortDate(noteDate)} г.</span>
|
<span>{formatShortDate(noteDate)} г.</span>
|
||||||
<span className={styles.signature}>
|
<span>{name}</span>
|
||||||
<span className={styles.signLine} />
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</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="Исполнители не назначены."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -85,8 +85,7 @@ export interface ISkudReason {
|
|||||||
/** Варианты причины. Меняя value — менять и choices колонки SkudReason в provisioning.json. */
|
/** Варианты причины. Меняя value — менять и choices колонки SkudReason в provisioning.json. */
|
||||||
export const SKUD_REASONS: ISkudReason[] = [
|
export const SKUD_REASONS: ISkudReason[] = [
|
||||||
{ value: 'Невнимательность', phrase: 'невнимательности' },
|
{ value: 'Невнимательность', phrase: 'невнимательности' },
|
||||||
{ value: 'Забыл(а) пропуск', phrase: 'того, что я забыл(а) пропуск' },
|
{ value: 'Неисправность СКУД', phrase: 'неисправности СКУД' },
|
||||||
{ value: 'Неисправность турникета или считывателя', phrase: 'неисправности турникета или считывателя' },
|
|
||||||
{ value: OTHER_REASON, phrase: '' }
|
{ value: OTHER_REASON, phrase: '' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,21 @@ describe('noteText', () => {
|
|||||||
ReasonDetails: 'был на объекте',
|
ReasonDetails: 'был на объекте',
|
||||||
PresenceConfirmed: false
|
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', () => {
|
it('сводка дней для колонки SP', () => {
|
||||||
|
|||||||
@@ -62,9 +62,14 @@ export function formatMissedDaysSummary(days: IMissedDay[]): string {
|
|||||||
return days.map((d) => `${formatShortDate(d.date)} (${formatMissedDetails(d)})`).join('; ');
|
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 {
|
function reasonSentence(data: IFormData, dateWord: string): string {
|
||||||
if (data.Reason === OTHER_REASON) {
|
if (data.Reason === OTHER_REASON) {
|
||||||
const details = data.ReasonDetails.trim() || BLANK;
|
const details = withFinalPunctuation(data.ReasonDetails.trim() || BLANK);
|
||||||
return `Отсутствие отметки в системе СКУД в ${dateWord} произошло по следующей причине: ${details}`;
|
return `Отсутствие отметки в системе СКУД в ${dateWord} произошло по следующей причине: ${details}`;
|
||||||
}
|
}
|
||||||
const reason = SKUD_REASONS.find((r) => r.value === data.Reason);
|
const reason = SKUD_REASONS.find((r) => r.value === data.Reason);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
||||||
{ "internalName": "SkudMissedDates", "title": "Дни без отметок", "type": "Note" },
|
{ "internalName": "SkudMissedDates", "title": "Дни без отметок", "type": "Note" },
|
||||||
{ "internalName": "SkudMissedDaysJson", "title": "Дни без отметок (JSON)", "type": "Note" },
|
{ "internalName": "SkudMissedDaysJson", "title": "Дни без отметок (JSON)", "type": "Note" },
|
||||||
{ "internalName": "SkudReason", "title": "Причина", "type": "Choice", "choices": ["Невнимательность", "Забыл(а) пропуск", "Неисправность турникета или считывателя", "Другое"] },
|
{ "internalName": "SkudReason", "title": "Причина", "type": "Choice", "choices": ["Невнимательность", "Неисправность СКУД", "Другое"] },
|
||||||
{ "internalName": "SkudReasonDetails", "title": "Причина (подробно)", "type": "Note" },
|
{ "internalName": "SkudReasonDetails", "title": "Причина (подробно)", "type": "Note" },
|
||||||
{ "internalName": "SkudPresenceConfirmed", "title": "Присутствие подтверждено", "type": "Boolean" }
|
{ "internalName": "SkudPresenceConfirmed", "title": "Присутствие подтверждено", "type": "Boolean" }
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user