feat: имя PDF при печати — название формы, ФИО и номер заявки
Окно печати ui-kit было без заголовка, и при «Сохранить как PDF» браузер называл файл как попало. Теперь имя — «<Форма> — <ФИО> №<код>» во всех 15 просмотрах с печатью (у подбора без ФИО — это заявка на вакансию). Пока временно локальная копия printRoot/PrintButton в shared/ui/PrintButton с параметром заголовка; параметр будет перенесён в ui-kit, после чего копия удаляется. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import styles from './BusinessTripView.module.scss';
|
import styles from './BusinessTripView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton } from '../../shared/ui';
|
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintButton, buildPrintTitle } from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
ApprovalMatrix,
|
ApprovalMatrix,
|
||||||
@@ -55,6 +55,7 @@ const BusinessTripView: React.FC<IBusinessTripViewProps> = ({ context, itemId, f
|
|||||||
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>('');
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ const BusinessTripView: React.FC<IBusinessTripViewProps> = ({ context, itemId, f
|
|||||||
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);
|
||||||
@@ -128,7 +130,9 @@ const BusinessTripView: React.FC<IBusinessTripViewProps> = ({ context, itemId, f
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintButton />
|
<PrintButton
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface ILoadedRequest {
|
|||||||
status: string;
|
status: string;
|
||||||
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
||||||
code: string;
|
code: string;
|
||||||
|
/** ФИО командируемого (Employee/Title) — для имени печатного документа. */
|
||||||
|
employeeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,11 +39,12 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string):
|
|||||||
'TripObligationAccepted',
|
'TripObligationAccepted',
|
||||||
'RequestStatus',
|
'RequestStatus',
|
||||||
'RequestUniqueCode',
|
'RequestUniqueCode',
|
||||||
'Employee/EMail'
|
'Employee/EMail',
|
||||||
|
'Employee/Title'
|
||||||
)
|
)
|
||||||
.expand('Employee')()) as Record<string, unknown>;
|
.expand('Employee')()) as Record<string, unknown>;
|
||||||
|
|
||||||
const employee = item.Employee as { EMail?: string } | undefined;
|
const employee = item.Employee as { EMail?: string; Title?: string } | undefined;
|
||||||
|
|
||||||
const data: IFormData = {
|
const data: IFormData = {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
@@ -61,6 +64,7 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string):
|
|||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status: (item.RequestStatus as string) || '',
|
status: (item.RequestStatus as string) || '',
|
||||||
code: (item.RequestUniqueCode as string) || ''
|
code: (item.RequestUniqueCode as string) || '',
|
||||||
|
employeeName: employee?.Title || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,16 @@ 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 './ClearanceView.module.scss';
|
import styles from './ClearanceView.module.scss';
|
||||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
buildPrintTitle
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -48,6 +57,8 @@ interface IClearanceViewProps {
|
|||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Обходной лист';
|
||||||
|
|
||||||
// Обходной лист в режиме согласующего. Форма read-only, КРОМЕ отметок, которые
|
// Обходной лист в режиме согласующего. Форма read-only, КРОМЕ отметок, которые
|
||||||
// заполняет профильный отдел на своём шаге: КДП — карта СКУД и сумма обучения,
|
// заполняет профильный отдел на своём шаге: КДП — карта СКУД и сумма обучения,
|
||||||
// отдел подбора — парковочная карта (см. approverRights.ts).
|
// отдел подбора — парковочная карта (см. approverRights.ts).
|
||||||
@@ -60,6 +71,8 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
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>('');
|
||||||
|
// ФИО увольняемого — для имени печатного документа.
|
||||||
|
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);
|
||||||
const [hrRecordsEditable, setHrRecordsEditable] = useState<boolean>(false);
|
const [hrRecordsEditable, setHrRecordsEditable] = useState<boolean>(false);
|
||||||
@@ -123,6 +136,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
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);
|
||||||
@@ -274,7 +288,12 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canEditAnything && (
|
{canEditAnything && (
|
||||||
@@ -295,7 +314,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Обходной лист"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ 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 './ConcreteRequestView.module.scss';
|
import styles from './ConcreteRequestView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
buildPrintTitle
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -42,6 +50,8 @@ interface IConcreteRequestViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявка на бетон';
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
||||||
const NOOP_SET: SetField = () => undefined;
|
const NOOP_SET: SetField = () => undefined;
|
||||||
|
|
||||||
@@ -137,13 +147,18 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.SiteChiefName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на бетон"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -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, FormNotice, buildPrintTitle } from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -58,6 +58,8 @@ interface IDismissalViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявление на увольнение';
|
||||||
|
|
||||||
// Заявление на увольнение только для просмотра (режим согласующего).
|
// Заявление на увольнение только для просмотра (режим согласующего).
|
||||||
// Переиспользует ту же секцию, что и форма подачи; редактирование выключено через
|
// Переиспользует ту же секцию, что и форма подачи; редактирование выключено через
|
||||||
// <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
|
// <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
|
||||||
@@ -70,6 +72,8 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
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>('');
|
||||||
|
// ФИО увольняющегося — для имени печатного документа.
|
||||||
|
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);
|
||||||
// Итог автосоздания обходного листа после согласования (см. onDecided ниже).
|
// Итог автосоздания обходного листа после согласования (см. onDecided ниже).
|
||||||
@@ -107,6 +111,7 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
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);
|
||||||
@@ -265,7 +270,9 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
{printGate === 'allowed' ? (
|
{printGate === 'allowed' ? (
|
||||||
<PrintButton />
|
<PrintButton
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.printBlocked} title="Печать откроется после согласования обходного листа по этому увольнению">
|
<span className={styles.printBlocked} title="Печать откроется после согласования обходного листа по этому увольнению">
|
||||||
Печать станет доступна после согласования обходного листа
|
Печать станет доступна после согласования обходного листа
|
||||||
@@ -275,7 +282,7 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявление на увольнение"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ 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 './EmployeeTransferView.module.scss';
|
import styles from './EmployeeTransferView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
buildPrintTitle
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -41,6 +49,8 @@ interface IEmployeeTransferViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявка на перемещение сотрудника';
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
||||||
const NOOP_SET: SetField = () => undefined;
|
const NOOP_SET: SetField = () => undefined;
|
||||||
|
|
||||||
@@ -56,6 +66,8 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
|
|||||||
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>('');
|
||||||
|
// ФИО перемещаемого — для имени печатного документа.
|
||||||
|
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) идёт в печать —
|
||||||
@@ -89,6 +101,7 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
|
|||||||
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);
|
||||||
@@ -136,13 +149,18 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на перемещение сотрудника"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
PrintButton,
|
PrintButton,
|
||||||
FieldChangesProvider,
|
FieldChangesProvider,
|
||||||
FieldChangesBanner,
|
FieldChangesBanner,
|
||||||
issueLabels
|
issueLabels,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { HIRE_CHANGE_TRACKING } from './changeTracking';
|
import { HIRE_CHANGE_TRACKING } from './changeTracking';
|
||||||
@@ -87,6 +88,7 @@ const HIRE_DATE_REQUIRED_MESSAGE = 'укажите дату приёма (п. 2.
|
|||||||
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
||||||
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
const FORM_TITLE = 'Заявка на прием';
|
||||||
|
|
||||||
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
||||||
// те же секции, что и форма подачи; редактирование выключено через
|
// те же секции, что и форма подачи; редактирование выключено через
|
||||||
@@ -382,7 +384,11 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
<Icon name="copy" size="sm" />
|
<Icon name="copy" size="sm" />
|
||||||
Организация рабочего места на основе этой
|
Организация рабочего места на основе этой
|
||||||
</Button>
|
</Button>
|
||||||
{isApprovedStatus(status) && <PrintButton />}
|
{isApprovedStatus(status) && (
|
||||||
|
<PrintButton
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.HireCandidateName, code, itemId })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasEditableFields && (
|
{hasEditableFields && (
|
||||||
@@ -400,7 +406,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на прием"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
PrintButton,
|
PrintButton,
|
||||||
FieldChangesProvider,
|
FieldChangesProvider,
|
||||||
FieldChangesBanner
|
FieldChangesBanner,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { SELECTION_CHANGE_TRACKING } from './changeTracking';
|
import { SELECTION_CHANGE_TRACKING } from './changeTracking';
|
||||||
@@ -80,6 +81,7 @@ interface IRecruitingSelectionViewProps {
|
|||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
const FORM_TITLE = 'Заявка на подбор';
|
||||||
|
|
||||||
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
|
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
|
||||||
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>,
|
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>,
|
||||||
@@ -366,7 +368,10 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
{isApprovedStatus(status) && <PrintButton />}
|
{/* Подбор — на вакансию, конкретного человека ещё нет: имя файла без ФИО. */}
|
||||||
|
{isApprovedStatus(status) && (
|
||||||
|
<PrintButton documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: '', code, itemId })} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{fullEditable && (
|
{fullEditable && (
|
||||||
@@ -384,7 +389,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на подбор"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
FormField,
|
FormField,
|
||||||
AttachmentList,
|
AttachmentList,
|
||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead
|
OrgLetterhead,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
@@ -60,6 +61,8 @@ interface IReferralViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Реферальная рекомендация';
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
||||||
const NOOP_SET: SetField = () => undefined;
|
const NOOP_SET: SetField = () => undefined;
|
||||||
|
|
||||||
@@ -189,14 +192,19 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
|
|||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
{isApprovedStatus(status) && (
|
{isApprovedStatus(status) && (
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.CandidateName, code, itemId })}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Реферальная рекомендация"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
Icon
|
Icon,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import {
|
import {
|
||||||
@@ -57,6 +58,8 @@ interface IServiceRequestViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявка на услуги';
|
||||||
|
|
||||||
// Заявка «Заявка на услуги» только для просмотра (режим согласующего).
|
// Заявка «Заявка на услуги» только для просмотра (режим согласующего).
|
||||||
const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя, его ссылка меняется на каждом рендере; в зависимостях эффекта
|
// onCodeLoaded — колбэк родителя, его ссылка меняется на каждом рендере; в зависимостях эффекта
|
||||||
@@ -166,13 +169,18 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
|
|||||||
Редактировать
|
Редактировать
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.RequesterName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на услуги"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import styles from './SkudExplanationView.module.scss';
|
import styles from './SkudExplanationView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
buildPrintTitle
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -130,7 +138,12 @@ const SkudExplanationView: React.FC<ISkudExplanationViewProps> = ({ context, ite
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.EmployeeFio, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormCard
|
<FormCard
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
FormField,
|
FormField,
|
||||||
AttachmentList,
|
AttachmentList,
|
||||||
PrintButton,
|
PrintButton,
|
||||||
Icon
|
Icon,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import {
|
import {
|
||||||
@@ -56,6 +57,8 @@ interface ITicket1CViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявка в отдел 1С';
|
||||||
|
|
||||||
// Заявка в отдел 1С только для просмотра (режим согласующего).
|
// Заявка в отдел 1С только для просмотра (режим согласующего).
|
||||||
const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||||
@@ -185,12 +188,12 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
|
|||||||
Редактировать
|
Редактировать
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PrintButton />
|
<PrintButton documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.FIO, code, itemId })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка в отдел 1С"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
AttachmentList,
|
AttachmentList,
|
||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
Icon
|
Icon,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +56,8 @@ interface ITmcTransferViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Служебная записка на перемещение ТМЦ';
|
||||||
|
|
||||||
// Заявка «СЗ на перемещение ТМЦ» только для просмотра (режим согласующего).
|
// Заявка «СЗ на перемещение ТМЦ» только для просмотра (режим согласующего).
|
||||||
const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||||
@@ -169,13 +172,18 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
|||||||
Редактировать
|
Редактировать
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.AuthorName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Служебная записка на перемещение ТМЦ"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
FormNotice,
|
FormNotice,
|
||||||
PrintSignature
|
PrintSignature,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
@@ -50,6 +51,8 @@ interface IUnpaidLeaveViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявление на отпуск без сохранения заработной платы';
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
||||||
const NOOP_SET: SetField = () => undefined;
|
const NOOP_SET: SetField = () => undefined;
|
||||||
|
|
||||||
@@ -148,13 +151,18 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявление на отпуск без сохранения заработной платы"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
PrintSplitButton,
|
PrintSplitButton,
|
||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
FormNotice,
|
FormNotice,
|
||||||
PrintSignature
|
PrintSignature,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
@@ -57,6 +58,8 @@ interface IVacationViewProps {
|
|||||||
onCodeLoaded?: (code: string) => void;
|
onCodeLoaded?: (code: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Заявление на оплачиваемый ежегодный трудовой отпуск';
|
||||||
|
|
||||||
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
|
||||||
const NOOP_SET: SetField = () => undefined;
|
const NOOP_SET: SetField = () => undefined;
|
||||||
|
|
||||||
@@ -207,13 +210,18 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.toolbar} rp-no-print`}>
|
<div className={`${styles.toolbar} rp-no-print`}>
|
||||||
<PrintSplitButton value={data.Organization} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={data.Organization}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: employeeName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявление на оплачиваемый ежегодный трудовой отпуск"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
OrgLetterhead,
|
OrgLetterhead,
|
||||||
Icon,
|
Icon,
|
||||||
FieldChangesProvider,
|
FieldChangesProvider,
|
||||||
FieldChangesBanner
|
FieldChangesBanner,
|
||||||
|
buildPrintTitle
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import {
|
import {
|
||||||
@@ -66,6 +67,7 @@ interface IWorkplaceSetupViewProps {
|
|||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
|
const FORM_TITLE = 'Организация рабочего места';
|
||||||
const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО», «Доступы» и «Мебель».';
|
const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО», «Доступы» и «Мебель».';
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
|
||||||
@@ -271,7 +273,12 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
Редактировать
|
Редактировать
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PrintSplitButton value={organizationText(data)} selectedIndex={printOrgIndex} onChange={setPrintOrgIndex} />
|
<PrintSplitButton
|
||||||
|
value={organizationText(data)}
|
||||||
|
selectedIndex={printOrgIndex}
|
||||||
|
onChange={setPrintOrgIndex}
|
||||||
|
documentTitle={buildPrintTitle({ formTitle: FORM_TITLE, personName: data.FullName, code, itemId })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canEditAnything && (
|
{canEditAnything && (
|
||||||
@@ -290,7 +297,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={organizationText(data)} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={organizationText(data)} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Организация рабочего места"
|
title={FORM_TITLE}
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine listTitle={dataListName} itemId={itemId} />
|
<InitiatorLine listTitle={dataListName} itemId={itemId} />
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Button, Icon } from '@tnsu/ui-kit';
|
||||||
|
import { printRoot } from './printRoot';
|
||||||
|
|
||||||
|
interface IPrintButtonProps {
|
||||||
|
label?: string;
|
||||||
|
/** Заголовок документа = имя PDF-файла (см. buildPrintTitle). Не задан — браузер назовёт сам. */
|
||||||
|
documentTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ВРЕМЕННО: локальная кнопка «Печать» вместо PrintButton из @tnsu/ui-kit — ради
|
||||||
|
* documentTitle (см. printRoot.ts). Вид и поведение те же, что у кита.
|
||||||
|
*/
|
||||||
|
export const PrintButton: React.FC<IPrintButtonProps> = ({ label = 'Печать', documentTitle }) => (
|
||||||
|
<Button variant="secondary" onClick={() => printRoot(undefined, documentTitle)}>
|
||||||
|
<Icon name="document" size="sm" />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* ВРЕМЕННО (2026-10-29): локальная копия printRoot из @tnsu/ui-kit с одним
|
||||||
|
* отличием — заголовком окна печати (title). Браузер берёт его как имя файла при
|
||||||
|
* «Сохранить как PDF»; у кита окно без <title>, и файл назывался как попало.
|
||||||
|
* План: перенести параметр title в кит (сигнатура уже совместимая:
|
||||||
|
* printRoot(selector, title)), затем удалить этот файл и вернуть импорт из кита.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Значение поля для печати. Дату (input type=date, ISO yyyy-mm-dd) — в дд.мм.гггг.
|
||||||
|
function formatValue(el: HTMLInputElement): string {
|
||||||
|
if (el.type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(el.value)) {
|
||||||
|
const [y, m, d] = el.value.split('-');
|
||||||
|
return `${d}.${m}.${y}`;
|
||||||
|
}
|
||||||
|
return el.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Печатаем как ДАННЫЕ, а не как форму: каждое поле ввода в клоне заменяем на
|
||||||
|
// обычный текст его значения (значения берём из живого DOM — React держит их как
|
||||||
|
// свойства, в клонированном HTML их нет).
|
||||||
|
function renderValuesAsText(source: Element, clone: Element): void {
|
||||||
|
const put = (cloneEl: Element | undefined, text: string): void => {
|
||||||
|
if (!cloneEl) return;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'rp-print-value';
|
||||||
|
div.textContent = text && text.length > 0 ? text : '—';
|
||||||
|
(cloneEl.closest('label') ?? cloneEl).replaceWith(div);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cInputs = clone.querySelectorAll('input');
|
||||||
|
source.querySelectorAll('input').forEach((el, i) => {
|
||||||
|
if (el.type === 'checkbox' || el.type === 'radio') {
|
||||||
|
put(cInputs[i], el.checked ? 'Да' : 'Нет');
|
||||||
|
} else {
|
||||||
|
put(cInputs[i], formatValue(el));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cAreas = clone.querySelectorAll('textarea');
|
||||||
|
source.querySelectorAll('textarea').forEach((el, i) => put(cAreas[i], el.value));
|
||||||
|
|
||||||
|
const cSelects = clone.querySelectorAll('select');
|
||||||
|
source.querySelectorAll('select').forEach((el, i) => {
|
||||||
|
const opt = el.options[el.selectedIndex];
|
||||||
|
put(cSelects[i], opt && opt.value ? opt.text : '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стили печатного окна — как в ките (см. комментарии там).
|
||||||
|
const PRINT_CSS =
|
||||||
|
'html,body{margin:0;background:#fff;height:auto!important;min-height:0!important}' +
|
||||||
|
'body{padding:24px;font-size:13px}' +
|
||||||
|
'svg{display:none!important}' +
|
||||||
|
'.ms-Persona-imageArea,.ms-Persona-image{display:none!important}' +
|
||||||
|
'.rp-print-root>*{border:0!important;box-shadow:none!important;padding:0!important}' +
|
||||||
|
'.rp-print-value{white-space:pre-wrap;color:#000;padding:1px 0 2px}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запасной путь — печать самой страницы (нет корня печати или всплывающее окно
|
||||||
|
* заблокировано). Заголовок страницы подменяем на время диалога, чтобы и тут
|
||||||
|
* имя файла было осмысленным; window.print() ждёт закрытия диалога.
|
||||||
|
*/
|
||||||
|
function printCurrentPage(title?: string): void {
|
||||||
|
if (!title) {
|
||||||
|
window.print();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pageTitle = document.title;
|
||||||
|
document.title = title;
|
||||||
|
window.print();
|
||||||
|
document.title = pageTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переносит документ (по умолчанию `.rp-print-root`) в отдельное чистое окно,
|
||||||
|
* заменяя поля ввода на текст значений, и печатает его. title — заголовок окна,
|
||||||
|
* он же имя файла при сохранении в PDF (см. buildPrintTitle).
|
||||||
|
*/
|
||||||
|
export function printRoot(selector = '.rp-print-root', title?: string): void {
|
||||||
|
const source = document.querySelector(selector);
|
||||||
|
if (!source) {
|
||||||
|
printCurrentPage(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const win = window.open('', '_blank', 'width=980,height=1200');
|
||||||
|
if (!win) {
|
||||||
|
printCurrentPage(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = source.cloneNode(true) as Element;
|
||||||
|
clone.querySelectorAll('.rp-no-print').forEach((el) => el.remove());
|
||||||
|
renderValuesAsText(source, clone);
|
||||||
|
|
||||||
|
const head = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]'))
|
||||||
|
.map((n) => n.outerHTML)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
win.document.open();
|
||||||
|
win.document.write(
|
||||||
|
`<!doctype html><html><head><meta charset="utf-8">${head}` +
|
||||||
|
`<style>${PRINT_CSS}</style></head><body>${clone.outerHTML}</body></html>`
|
||||||
|
);
|
||||||
|
win.document.close();
|
||||||
|
// Через свойство, а не тегом в разметке выше: так текст не надо экранировать.
|
||||||
|
if (title) win.document.title = title;
|
||||||
|
win.focus();
|
||||||
|
|
||||||
|
const done = (): void => {
|
||||||
|
win.print();
|
||||||
|
win.close();
|
||||||
|
};
|
||||||
|
if (win.document.readyState === 'complete') {
|
||||||
|
window.setTimeout(done, 350);
|
||||||
|
} else {
|
||||||
|
win.onload = (): void => {
|
||||||
|
window.setTimeout(done, 350);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPrintTitleParts {
|
||||||
|
/** Название формы, как в шапке карточки. */
|
||||||
|
formTitle: string;
|
||||||
|
/** ФИО сотрудника (кандидата), о ком заявка; '' — пропускаем. Берётся как есть. */
|
||||||
|
personName: string;
|
||||||
|
/** Уникальный код заявки; '' у старых заявок — тогда номер элемента. */
|
||||||
|
code: string;
|
||||||
|
itemId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Имя печатного документа (и PDF-файла):
|
||||||
|
* «<Название формы> — <ФИО> №<код>», напр. «Заявка на отпуск — Иванов Иван №HR-2610-45».
|
||||||
|
* Номер — как в шапке карточки (код, а у старых заявок — номер элемента).
|
||||||
|
*/
|
||||||
|
export function buildPrintTitle({ formTitle, personName, code, itemId }: IPrintTitleParts): string {
|
||||||
|
const name = personName.trim();
|
||||||
|
const head = name ? `${formTitle} — ${name}` : formTitle;
|
||||||
|
return `${head} №${code || itemId}`;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { DropdownButton, Icon, PrintButton, printRoot } from '@tnsu/ui-kit';
|
import { DropdownButton, Icon } from '@tnsu/ui-kit';
|
||||||
|
import { PrintButton } from '../PrintButton/PrintButton';
|
||||||
|
import { printRoot } from '../PrintButton/printRoot';
|
||||||
|
|
||||||
interface IPrintSplitButtonProps {
|
interface IPrintSplitButtonProps {
|
||||||
/** `\n`-joined «Компания — должность», как в data.Organization. */
|
/** `\n`-joined «Компания — должность», как в data.Organization. */
|
||||||
@@ -7,6 +9,8 @@ interface IPrintSplitButtonProps {
|
|||||||
selectedIndex: number;
|
selectedIndex: number;
|
||||||
onChange: (index: number) => void;
|
onChange: (index: number) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
/** Заголовок документа = имя PDF-файла (см. buildPrintTitle). */
|
||||||
|
documentTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +23,8 @@ export const PrintSplitButton: React.FC<IPrintSplitButtonProps> = ({
|
|||||||
value,
|
value,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
onChange,
|
onChange,
|
||||||
label = 'Печать'
|
label = 'Печать',
|
||||||
|
documentTitle
|
||||||
}) => {
|
}) => {
|
||||||
const [pendingPrint, setPendingPrint] = React.useState(false);
|
const [pendingPrint, setPendingPrint] = React.useState(false);
|
||||||
const lines = value ? value.split('\n') : [];
|
const lines = value ? value.split('\n') : [];
|
||||||
@@ -31,11 +36,11 @@ export const PrintSplitButton: React.FC<IPrintSplitButtonProps> = ({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!pendingPrint) return;
|
if (!pendingPrint) return;
|
||||||
setPendingPrint(false);
|
setPendingPrint(false);
|
||||||
printRoot();
|
printRoot(undefined, documentTitle);
|
||||||
}, [pendingPrint]);
|
}, [pendingPrint, documentTitle]);
|
||||||
|
|
||||||
if (lines.length <= 1) {
|
if (lines.length <= 1) {
|
||||||
return <PrintButton label={label} />;
|
return <PrintButton label={label} documentTitle={documentTitle} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = lines.map((line, i) => ({ value: String(i), label: line }));
|
const options = lines.map((line, i) => ({ value: String(i), label: line }));
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export {
|
|||||||
Icon,
|
Icon,
|
||||||
ConfirmHost,
|
ConfirmHost,
|
||||||
confirmModal,
|
confirmModal,
|
||||||
RequestNumber,
|
RequestNumber
|
||||||
PrintButton
|
|
||||||
} from '@tnsu/ui-kit';
|
} from '@tnsu/ui-kit';
|
||||||
export type { ISelectOption, IConfirmOptions } from '@tnsu/ui-kit';
|
export type { ISelectOption, IConfirmOptions } from '@tnsu/ui-kit';
|
||||||
|
|
||||||
@@ -48,6 +47,9 @@ export { PeopleFieldMulti } from './PeopleField/PeopleFieldMulti';
|
|||||||
export { PeopleTextField } from './PeopleField/PeopleTextField';
|
export { PeopleTextField } from './PeopleField/PeopleTextField';
|
||||||
export { OrganizationField } from './OrganizationField/OrganizationField';
|
export { OrganizationField } from './OrganizationField/OrganizationField';
|
||||||
export { OrganizationTable } from './OrganizationField/OrganizationTable';
|
export { OrganizationTable } from './OrganizationField/OrganizationTable';
|
||||||
|
// PrintButton — ВРЕМЕННО локальный (заголовок = имя PDF), пока параметр title не перенесён в ui-kit.
|
||||||
|
export { PrintButton } from './PrintButton/PrintButton';
|
||||||
|
export { buildPrintTitle } from './PrintButton/printRoot';
|
||||||
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 { PrintSignature } from './PrintSignature/PrintSignature';
|
||||||
|
|||||||
Reference in New Issue
Block a user