feat: added app routing

This commit is contained in:
Тимур Городецкий
2026-08-03 17:58:24 +05:00
parent e6203e7093
commit dc60d2dbee
22 changed files with 460 additions and 61 deletions
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useState, useMemo, useCallback } from 'react';
import styles from './RecruitingHireForm.module.scss';
import { Button, Alert, FormCard, FormActions, Icon } from '../../shared/ui';
import { Button, Alert, FormCard, FormActions, FileDropzone, CollapseSection, FormField, Icon } from '../../shared/ui';
import {
ApprovalMatrix,
useMatrixEditor,
@@ -9,9 +9,10 @@ import {
REQUEST_STATUS,
IApproverToWrite
} from '../../shared/approval';
import { uploadAttachments } from '../../shared/attachments/attachments.service';
import { IRequestFormProps } from '../../models/types';
import { IFormData, EMPTY_DATA, SetField } from './model';
import { saveHireRequest, hireApprovalService, HIRE_REQUEST_TYPE_ID } from './submit';
import { saveHireRequest, hireApprovalService, HIRE_REQUEST_TYPE_ID, DATA_LIST_TITLE } from './submit';
import { ApplicationSection } from './sections/ApplicationSection';
import { CandidateSection } from './sections/CandidateSection';
import { PpeSection } from './sections/PpeSection';
@@ -27,6 +28,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
const [actionKind, setActionKind] = useState<ActionKind | undefined>(undefined);
const [status, setStatus] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | null>(null);
const [files, setFiles] = useState<File[]>([]);
const set: SetField = (key, value) => {
setData((prev) => ({ ...prev, [key]: value }));
@@ -60,7 +62,8 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
setActionKind(kind);
setActionStatus('saving');
try {
await saveHireRequest(data, matrixApprovers(), requestStatus);
const id = await saveHireRequest(data, matrixApprovers(), requestStatus);
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
setStatus(requestStatus);
setActionStatus('success');
if (onSaved) {
@@ -106,6 +109,12 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
<ConditionsSection {...sectionProps} />
<ResponsibleSection {...sectionProps} />
<CollapseSection title="Вложения" icon="upload">
<FormField label="Файлы" full>
<FileDropzone files={files} onChange={setFiles} />
</FormField>
</CollapseSection>
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
<FormActions>
@@ -2,8 +2,9 @@ import * as React from 'react';
import { useState, useEffect, useMemo } from 'react';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
import styles from './RecruitingHireView.module.scss';
import { Alert, FormCard, FormModeProvider } from '../../shared/ui';
import { Alert, FormCard, FormModeProvider, CollapseSection, FormField, AttachmentList } from '../../shared/ui';
import { ApproverService, DecisionPanel } from '../../shared/approval';
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
import { IFormData, EMPTY_DATA, SetField } from './model';
import { loadHireRequest } from './load';
import { hireApprovalService, DATA_LIST_TITLE } from './submit';
@@ -26,6 +27,7 @@ const NOOP_SET: SetField = () => undefined;
// через <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId }) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@@ -38,10 +40,11 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
useEffect(() => {
let alive = true;
setLoading(true);
loadHireRequest(itemId)
.then((loaded) => {
Promise.all([loadHireRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
.then(([loaded, files]) => {
if (alive) {
setData(loaded);
setAttachments(files);
setError(null);
setLoading(false);
}
@@ -80,6 +83,12 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
<ConditionsSection {...sectionProps} />
<ResponsibleSection {...sectionProps} />
</FormModeProvider>
<CollapseSection title="Вложения" icon="upload">
<FormField label="Файлы" full>
<AttachmentList items={attachments} />
</FormField>
</CollapseSection>
</FormCard>
{/* Панель решения — ВНЕ FormModeProvider, чтобы комментарий был редактируемым. */}
@@ -62,10 +62,13 @@ export const EMPTY_DATA: IFormData = {
SecuritySpecialist: ''
};
/** Значение «Тип приёма», при котором показывается поле «Взамен кого». */
export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного';
/** Варианты «Тип приёма» — совпадают с choices колонки HireType. */
export const HIRE_TYPE_OPTIONS: ISelectOption[] = [
{ value: 'Новый сотрудник', label: 'Новый сотрудник' },
{ value: 'Вместо уволенного', label: 'Вместо уволенного' }
{ value: HIRE_TYPE_REPLACEMENT, label: HIRE_TYPE_REPLACEMENT }
];
/** Варианты «Вид оформления» — совпадают с choices колонки EmploymentForm. */
@@ -1,27 +1,40 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, Select } from '../../../shared/ui';
import { ISectionProps, HIRE_TYPE_OPTIONS } from '../model';
import { CollapseSection, FormField, TextInput, PhoneInput, Select } from '../../../shared/ui';
import { ISectionProps, HIRE_TYPE_OPTIONS, HIRE_TYPE_REPLACEMENT } from '../model';
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set }) => (
<CollapseSection title="Заявка" icon="clipboard">
<FormField label="Телефон" htmlFor="Phone">
<TextInput id="Phone" value={data.Phone} onChange={(v) => set('Phone', v)} />
</FormField>
<FormField label="Тип приёма" htmlFor="HireType">
<Select
id="HireType"
value={data.HireType}
onChange={(v) => set('HireType', v)}
options={HIRE_TYPE_OPTIONS}
placeholder="— выберите —"
/>
</FormField>
<FormField label="Взамен кого (Ф.И.О. уволенного)" htmlFor="ReplacedEmployee" full>
<TextInput
id="ReplacedEmployee"
value={data.ReplacedEmployee}
onChange={(v) => set('ReplacedEmployee', v)}
/>
</FormField>
</CollapseSection>
);
export const ApplicationSection: React.FC<ISectionProps> = ({ data, set }) => {
const isReplacement = data.HireType === HIRE_TYPE_REPLACEMENT;
const handleHireTypeChange = (value: string): void => {
set('HireType', value);
if (value !== HIRE_TYPE_REPLACEMENT) {
set('ReplacedEmployee', ''); // при не-замене очищаем скрытое поле
}
};
return (
<CollapseSection title="Заявка" icon="clipboard">
<FormField label="Телефон" htmlFor="Phone">
<PhoneInput id="Phone" value={data.Phone} onChange={(v) => set('Phone', v)} />
</FormField>
<FormField label="Тип приёма" htmlFor="HireType">
<Select
id="HireType"
value={data.HireType}
onChange={handleHireTypeChange}
options={HIRE_TYPE_OPTIONS}
placeholder="— выберите —"
/>
</FormField>
{isReplacement && (
<FormField label="Взамен кого (Ф.И.О. уволенного)" htmlFor="ReplacedEmployee" full>
<TextInput
id="ReplacedEmployee"
value={data.ReplacedEmployee}
onChange={(v) => set('ReplacedEmployee', v)}
/>
</FormField>
)}
</CollapseSection>
);
};
@@ -1,21 +1,19 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, Select } from '../../../shared/ui';
import { CollapseSection, FormField, TextInput, MoneyInput, Select } from '../../../shared/ui';
import { ISectionProps, EMPLOYMENT_FORM_OPTIONS } from '../model';
export const ConditionsSection: React.FC<ISectionProps> = ({ data, set }) => (
<CollapseSection title="Условия оплаты и оформления" icon="calculator">
<FormField label="Оклад при испытательном сроке" htmlFor="SalaryProbation">
<TextInput
<MoneyInput
id="SalaryProbation"
type="number"
value={data.SalaryProbation}
onChange={(v) => set('SalaryProbation', v)}
/>
</FormField>
<FormField label="Оклад по завершении ИС" htmlFor="SalaryAfterProbation">
<TextInput
<MoneyInput
id="SalaryAfterProbation"
type="number"
value={data.SalaryAfterProbation}
onChange={(v) => set('SalaryAfterProbation', v)}
/>
@@ -3,7 +3,7 @@ import { CollapseSection, FormField, TextInput } from '../../../shared/ui';
import { ISectionProps } from '../model';
export const PpeSection: React.FC<ISectionProps> = ({ data, set }) => (
<CollapseSection title="СИЗ (спецодежда)" icon="folder">
<CollapseSection title="СИЗ (спецодежда)" icon="helmet">
<FormField label="Рост, см" htmlFor="PpeHeight">
<TextInput id="PpeHeight" type="number" value={data.PpeHeight} onChange={(v) => set('PpeHeight', v)} />
</FormField>
@@ -3,7 +3,7 @@ import { CollapseSection, FormField, PeopleField } from '../../../shared/ui';
import { ISectionProps, PERSON_PLACEHOLDER } from '../model';
export const ResponsibleSection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title="Ответственные лица" icon="check">
<CollapseSection title="Ответственные лица" icon="user-check">
<FormField label="Руководитель участка" full>
<PeopleField
context={context}
@@ -1,16 +1,15 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Toggle, PeopleField, PeopleFieldMulti } from '../../../shared/ui';
import { CollapseSection, FormField, TextInput, TextArea, MoneyInput, Toggle, PeopleField, PeopleFieldMulti } from '../../../shared/ui';
import { ISectionProps, PERSON_PLACEHOLDER } from '../model';
export const ConditionsSection: React.FC<ISectionProps> = ({ data, set, context }) => (
<CollapseSection title="Условия работы" icon="chart">
<FormField label="Ставка на ИС" htmlFor="RateProbation">
<TextInput id="RateProbation" type="number" value={data.RateProbation} onChange={(v) => set('RateProbation', v)} />
<MoneyInput id="RateProbation" value={data.RateProbation} onChange={(v) => set('RateProbation', v)} />
</FormField>
<FormField label="Ставка после ИС" htmlFor="RateAfterProbation">
<TextInput
<MoneyInput
id="RateAfterProbation"
type="number"
value={data.RateAfterProbation}
onChange={(v) => set('RateAfterProbation', v)}
/>