Compare commits
3 Commits
41cce93b41
...
96f53da020
| Author | SHA1 | Date | |
|---|---|---|---|
| 96f53da020 | |||
| d6bb76fe47 | |||
| 4b05da5469 |
@@ -143,7 +143,7 @@ interface ITableColumns {
|
|||||||
assignee: boolean;
|
assignee: boolean;
|
||||||
/** Колонка «Исполнено» — кто из исполнителей уже выполнил (см. showsExecuted). */
|
/** Колонка «Исполнено» — кто из исполнителей уже выполнил (см. showsExecuted). */
|
||||||
executed: boolean;
|
executed: boolean;
|
||||||
/** Колонка «Дата выхода» — только «Все заявки» по организации рабочего места. */
|
/** Колонка «Дата выхода» — только таблица из заявок на организацию рабочего места (см. showsStartDate). */
|
||||||
startDate?: boolean;
|
startDate?: boolean;
|
||||||
/** Заголовок колонки с комментарием; undefined — колонки нет. */
|
/** Заголовок колонки с комментарием; undefined — колонки нет. */
|
||||||
commentLabel?: string;
|
commentLabel?: string;
|
||||||
@@ -178,9 +178,15 @@ function showsExecuted(view: ListView, statusFilter: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Дата выхода сотрудника есть только у заявки на организацию рабочего места, и показываем её во «Все заявки». */
|
/**
|
||||||
function showsStartDate(view: ListView, type: IRequestType | undefined): boolean {
|
* Дата выхода сотрудника есть только у заявки на организацию рабочего места.
|
||||||
return view === 'all' && type?.pageUrl === WORKPLACE_SETUP_FORM_ID;
|
* Показываем её, когда таблица целиком из таких заявок: «Все заявки» этого типа
|
||||||
|
* или любой раздел с фильтром по этому типу. Без фильтра типы вперемешку —
|
||||||
|
* колонка была бы почти пустой.
|
||||||
|
*/
|
||||||
|
function showsStartDate(view: ListView, allRequestsType: IRequestType | undefined, filteredType: IRequestType | undefined): boolean {
|
||||||
|
const tableType = view === 'all' ? allRequestsType : filteredType;
|
||||||
|
return tableType?.pageUrl === WORKPLACE_SETUP_FORM_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function columnsFor(view: ListView, statusFilter: string, startDate = false): ITableColumns {
|
function columnsFor(view: ListView, statusFilter: string, startDate = false): ITableColumns {
|
||||||
@@ -194,15 +200,15 @@ function columnsFor(view: ListView, statusFilter: string, startDate = false): IT
|
|||||||
? 'Причина возврата'
|
? 'Причина возврата'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (view === 'approvals' || view === 'execution') return { author: true, status: false, assignee, executed };
|
if (view === 'approvals' || view === 'execution') return { author: true, status: false, assignee, executed, startDate };
|
||||||
if (view === 'rework') return { author: false, status: false, assignee, executed, commentLabel };
|
if (view === 'rework') return { author: false, status: false, assignee, executed, startDate, commentLabel };
|
||||||
if (view === 'own:created') return { author: false, status: !statusFilter, assignee, executed, commentLabel };
|
if (view === 'own:created') return { author: false, status: !statusFilter, assignee, executed, startDate, commentLabel };
|
||||||
// 'all' — заявки всех авторов вперемешку, поэтому нужны и автор, и статус.
|
// 'all' — заявки всех авторов вперемешку, поэтому нужны и автор, и статус.
|
||||||
if (view === 'all') return { author: true, status: true, assignee, executed, startDate };
|
if (view === 'all') return { author: true, status: true, assignee, executed, startDate };
|
||||||
// История решений: заявка чужая, поэтому нужен автор. Статус — только если он
|
// История решений: заявка чужая, поэтому нужен автор. Статус — только если он
|
||||||
// не предопределён самой вкладкой («Отклонённые мной» — это всегда «Отклонено»,
|
// не предопределён самой вкладкой («Отклонённые мной» — это всегда «Отклонено»,
|
||||||
// а вот у согласованных/выполненных заявка могла уйти дальше по маршруту).
|
// а вот у согласованных/выполненных заявка могла уйти дальше по маршруту).
|
||||||
return { author: true, status: !decisionStatus, assignee, executed, commentLabel };
|
return { author: true, status: !decisionStatus, assignee, executed, startDate, commentLabel };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRequestsTableViewProps {
|
export interface IRequestsTableViewProps {
|
||||||
@@ -494,6 +500,8 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
});
|
});
|
||||||
const { localFilters, sort, isRefined } = refinement;
|
const { localFilters, sort, isRefined } = refinement;
|
||||||
const refinementSig = refinement.signature;
|
const refinementSig = refinement.signature;
|
||||||
|
const filteredType = types?.find((t) => t.id === localFilters.typeId);
|
||||||
|
const withStartDate = showsStartDate(view, allRequestsType, filteredType);
|
||||||
|
|
||||||
// Полный список раздела для режима с фильтрами — последний загруженный.
|
// Полный список раздела для режима с фильтрами — последний загруженный.
|
||||||
// Листание страниц и смена фильтра его переиспользуют (заново по сети не
|
// Листание страниц и смена фильтра его переиспользуют (заново по сети не
|
||||||
@@ -664,7 +672,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
decisionStatus ? hydrateDecisionComments(context, types, result.rows, decisionStatus) : undefined,
|
decisionStatus ? hydrateDecisionComments(context, types, result.rows, decisionStatus) : undefined,
|
||||||
showsAssignee(view, statusFilter) ? hydrateCurrentAssignees(context, types, result.rows) : undefined,
|
showsAssignee(view, statusFilter) ? hydrateCurrentAssignees(context, types, result.rows) : undefined,
|
||||||
showsExecuted(view, statusFilter) ? hydrateExecutedSteps(context, types, result.rows) : undefined,
|
showsExecuted(view, statusFilter) ? hydrateExecutedSteps(context, types, result.rows) : undefined,
|
||||||
showsStartDate(view, allRequestsType) ? hydrateStartDates(context, result.rows) : undefined,
|
withStartDate ? hydrateStartDates(context, result.rows) : undefined,
|
||||||
]);
|
]);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -850,7 +858,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
rows={visible}
|
rows={visible}
|
||||||
view={view}
|
view={view}
|
||||||
statusFilter={statusFilter}
|
statusFilter={statusFilter}
|
||||||
withStartDate={showsStartDate(view, allRequestsType)}
|
withStartDate={withStartDate}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSort={refinement.onSort}
|
onSort={refinement.onSort}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
getDraftDeleteHandler,
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit,
|
||||||
|
LeavePrintNotice
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
@@ -326,6 +327,8 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
|
<LeavePrintNotice />
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -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 './UnpaidLeaveView.module.scss';
|
import styles from './UnpaidLeaveView.module.scss';
|
||||||
import { Alert, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
LeavePrintNotice
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -148,6 +156,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
}
|
}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
|
<LeavePrintNotice />
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
RequestNumber,
|
RequestNumber,
|
||||||
SubmitActions,
|
SubmitActions,
|
||||||
getDraftDeleteHandler,
|
getDraftDeleteHandler,
|
||||||
useRequestSubmit
|
useRequestSubmit,
|
||||||
|
LeavePrintNotice
|
||||||
} from '../../shared/ui';
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
composeRemoveRowRule,
|
composeRemoveRowRule,
|
||||||
@@ -44,7 +45,7 @@ import {
|
|||||||
dataListTitle,
|
dataListTitle,
|
||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS, MAX_DAYS } from './model';
|
||||||
import { saveVacationRequest, vacationApprovalService } from './submit';
|
import { saveVacationRequest, vacationApprovalService } from './submit';
|
||||||
import { loadVacationRequest } from './load';
|
import { loadVacationRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -202,17 +203,20 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
// выбрать более раннюю дату, но проверяем и здесь на случай обхода UI.
|
// выбрать более раннюю дату, но проверяем и здесь на случай обхода UI.
|
||||||
const minStartDate = isoDatePlusDays(MIN_NOTICE_DAYS);
|
const minStartDate = isoDatePlusDays(MIN_NOTICE_DAYS);
|
||||||
const isStartDateValid = !data.StartDate || data.StartDate >= minStartDate;
|
const isStartDateValid = !data.StartDate || data.StartDate >= minStartDate;
|
||||||
|
const isDaysCountTooLarge = Number.isFinite(daysNum) && daysNum > MAX_DAYS;
|
||||||
const isValid = !!(
|
const isValid = !!(
|
||||||
data.Employee &&
|
data.Employee &&
|
||||||
data.StartDate &&
|
data.StartDate &&
|
||||||
isStartDateValid &&
|
isStartDateValid &&
|
||||||
Number.isFinite(daysNum) &&
|
Number.isFinite(daysNum) &&
|
||||||
daysNum > 0
|
daysNum > 0 &&
|
||||||
|
!isDaysCountTooLarge
|
||||||
);
|
);
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
if (data.StartDate && !isStartDateValid) {
|
if (data.StartDate && !isStartDateValid) {
|
||||||
return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`;
|
return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`;
|
||||||
}
|
}
|
||||||
|
if (isDaysCountTooLarge) return `Отпуск — не более ${MAX_DAYS} календарных дней в одной заявке.`;
|
||||||
if (!isValid) return 'Заполните обязательные поля: сотрудник, дата начала, количество дней (> 0).';
|
if (!isValid) return 'Заполните обязательные поля: сотрудник, дата начала, количество дней (> 0).';
|
||||||
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
||||||
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
||||||
@@ -354,6 +358,8 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||||
|
|
||||||
|
<LeavePrintNotice />
|
||||||
|
|
||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -2,7 +2,16 @@ import * as React from 'react';
|
|||||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import styles from './VacationView.module.scss';
|
import styles from './VacationView.module.scss';
|
||||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, PrintSplitButton, OrgLetterhead } from '../../shared/ui';
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
FormCard,
|
||||||
|
InitiatorLine,
|
||||||
|
FormModeProvider,
|
||||||
|
PrintSplitButton,
|
||||||
|
OrgLetterhead,
|
||||||
|
LeavePrintNotice
|
||||||
|
} from '../../shared/ui';
|
||||||
import {
|
import {
|
||||||
ApproverService,
|
ApproverService,
|
||||||
DecisionPanel,
|
DecisionPanel,
|
||||||
@@ -208,6 +217,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
}
|
}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
|
<LeavePrintNotice />
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
<RequestSection {...sectionProps} />
|
<RequestSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
|
|||||||
@@ -80,5 +80,7 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
||||||
/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */
|
/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */
|
||||||
export const MIN_NOTICE_DAYS = 7;
|
export const MIN_NOTICE_DAYS = 7;
|
||||||
|
/** Ежегодный трудовой отпуск — не более стольких календарных дней в одной заявке (требование HR). */
|
||||||
|
export const MAX_DAYS = 24;
|
||||||
/** Плейсхолдер для поля выбора замещающего. */
|
/** Плейсхолдер для поля выбора замещающего. */
|
||||||
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';
|
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormGrid, FormField, TextInput, PeopleField, OrganizationTable } from '../../../shared/ui';
|
import { FormGrid, FormField, TextInput, PeopleField, OrganizationTable } from '../../../shared/ui';
|
||||||
import { ProjectPicker } from '../../../shared/dictionary';
|
import { ProjectPicker } from '../../../shared/dictionary';
|
||||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS } from '../model';
|
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS, MAX_DAYS } from '../model';
|
||||||
import { isoDatePlusDays } from '../../../shared/dateUtils';
|
import { isoDatePlusDays } from '../../../shared/dateUtils';
|
||||||
|
|
||||||
/** Единственная секция формы «Заявка на отпуск»: сотрудник (должность и подразделение подтягиваются из справочника), даты. */
|
/** Единственная секция формы «Заявка на отпуск»: сотрудник (должность и подразделение подтягиваются из справочника), даты. */
|
||||||
@@ -38,7 +38,14 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Количество дней" htmlFor="DaysCount" required>
|
<FormField label="Количество дней" htmlFor="DaysCount" required>
|
||||||
<TextInput id="DaysCount" type="number" value={data.DaysCount} onChange={(v) => set('DaysCount', v)} />
|
<TextInput
|
||||||
|
id="DaysCount"
|
||||||
|
type="number"
|
||||||
|
value={data.DaysCount}
|
||||||
|
onChange={(v) => set('DaysCount', v)}
|
||||||
|
min={1}
|
||||||
|
max={MAX_DAYS}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full>
|
<FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full>
|
||||||
<PeopleField
|
<PeopleField
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Alert } from '@tnsu/ui-kit';
|
||||||
|
|
||||||
|
/** Пользовательские тексты памятки — в одном месте (задел под локализацию). */
|
||||||
|
const NOTICE_TITLE = 'Памятка';
|
||||||
|
const NOTICE_TEXT =
|
||||||
|
'После согласования заявления его необходимо распечатать с портала (кнопка «Печать» на странице заявки), ' +
|
||||||
|
'собственноручно подписать и передать специалисту отдела кадров.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Памятка для заявлений на отпуск (трудовой и без содержания): что сделать после
|
||||||
|
* согласования. Подписанный бумажный экземпляр нужен отделу кадров, поэтому
|
||||||
|
* напоминаем и при подаче, и при просмотре. На печать не выводится.
|
||||||
|
* Вариант warn, а не info: нейтральная плашка терялась на фоне формы.
|
||||||
|
*/
|
||||||
|
export const LeavePrintNotice: React.FC = () => (
|
||||||
|
<div className="rp-no-print">
|
||||||
|
<Alert variant="warn">
|
||||||
|
<strong>{NOTICE_TITLE}.</strong> {NOTICE_TEXT}
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -59,5 +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 { LeavePrintNotice } from './LeavePrintNotice/LeavePrintNotice';
|
||||||
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