fix: changed resolve endpoint contract form get to post

This commit is contained in:
Тимур Городецкий
2026-08-03 14:37:42 +05:00
parent 788e046ebe
commit e6203e7093
22 changed files with 512 additions and 22 deletions
@@ -0,0 +1,8 @@
@import '../../../../styles/design-tokens';
.state {
padding: $sp-8;
color: $gray-500;
font-size: $fs-sm;
text-align: center;
}
@@ -0,0 +1,91 @@
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 { ApproverService, DecisionPanel } from '../../shared/approval';
import { IFormData, EMPTY_DATA, SetField } from './model';
import { loadHireRequest } from './load';
import { hireApprovalService, DATA_LIST_TITLE } from './submit';
import { ApplicationSection } from './sections/ApplicationSection';
import { CandidateSection } from './sections/CandidateSection';
import { PpeSection } from './sections/PpeSection';
import { ConditionsSection } from './sections/ConditionsSection';
import { ResponsibleSection } from './sections/ResponsibleSection';
interface IRecruitingHireViewProps {
context: WebPartContext;
itemId: number;
}
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
const NOOP_SET: SetField = () => undefined;
// Заявка «на прием» только для просмотра (режим согласующего).
// Переиспользует те же секции, что и форма подачи; редактирование выключено
// через <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId }) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Сервис решений согласующего: список согласования + список данных этой формы.
const approverService = useMemo(
() => new ApproverService(hireApprovalService(), DATA_LIST_TITLE),
[]
);
useEffect(() => {
let alive = true;
setLoading(true);
loadHireRequest(itemId)
.then((loaded) => {
if (alive) {
setData(loaded);
setError(null);
setLoading(false);
}
})
.catch((e) => {
if (alive) {
setError(`Не удалось загрузить заявку №${itemId}: ${(e as Error).message}`);
setLoading(false);
}
});
return () => {
alive = false;
};
}, [itemId]);
if (loading) {
return <div className={styles.state}>Загрузка заявки…</div>;
}
if (error) {
return (
<div className={styles.state}>
<Alert variant="err">{error}</Alert>
</div>
);
}
const sectionProps = { data, set: NOOP_SET, context };
return (
<div>
<FormCard title="Заявка на прием" subtitle={`№${itemId} · режим согласования`}>
<FormModeProvider readOnly>
<ApplicationSection {...sectionProps} />
<CandidateSection {...sectionProps} />
<PpeSection {...sectionProps} />
<ConditionsSection {...sectionProps} />
<ResponsibleSection {...sectionProps} />
</FormModeProvider>
</FormCard>
{/* Панель решения — ВНЕ FormModeProvider, чтобы комментарий был редактируемым. */}
<DecisionPanel service={approverService} requestId={itemId} />
</div>
);
};
export default RecruitingHireView;
@@ -0,0 +1,47 @@
import { getSP } from '../../shared/pnp/pnpjs';
import { IFormData, EMPTY_DATA } from './model';
const LIST_TITLE = 'Прием на работу — данные';
// Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке.
const SCALAR_FIELDS: (keyof IFormData)[] = [
'Phone', 'HireType', 'ReplacedEmployee', 'CandidateName', 'HireDate', 'Position', 'WorkKind',
'SiteName', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'SalaryProbation',
'SalaryAfterProbation', 'WorkSchedule', 'EmploymentForm'
];
/** Прочитать заявку «на прием» по id и разложить в модель формы (для режима чтения). */
export async function loadHireRequest(itemId: number): Promise<IFormData> {
const sp = getSP();
const item = (await sp.web.lists
.getByTitle(LIST_TITLE)
.items.getById(itemId)
.select(
...SCALAR_FIELDS,
'SiteManager/EMail',
'SecuritySpecialist/EMail'
)
.expand('SiteManager', 'SecuritySpecialist')()) as Record<string, unknown>;
const data: IFormData = { ...EMPTY_DATA };
const writable = data as unknown as Record<string, string>;
SCALAR_FIELDS.forEach((key) => {
const value = item[key];
if (value === null || value === undefined) {
writable[key] = '';
} else if (key === 'HireDate' && typeof value === 'string') {
writable[key] = value.slice(0, 10); // ISO → YYYY-MM-DD для input type=date
} else {
writable[key] = String(value);
}
});
// Люди: из expand берём e-mail (по нему picker покажет ФИО в режиме чтения).
const manager = item.SiteManager as { EMail?: string } | undefined;
const security = item.SecuritySpecialist as { EMail?: string } | undefined;
data.SiteManager = manager?.EMail ?? '';
data.SecuritySpecialist = security?.EMail ?? '';
return data;
}
@@ -2,7 +2,7 @@ import { getSP } from '../../shared/pnp/pnpjs';
import { ApprovalListService, IApproverToWrite, writeApprovers } from '../../shared/approval';
import { IFormData } from './model';
const LIST_TITLE = 'Прием на работу — данные';
export const DATA_LIST_TITLE = 'Прием на работу — данные';
export const APPROVAL_LIST_TITLE = 'Прием на работу — согласование';
// Тип заявки «Заявка на прием» во внешнем approval-route API
// (GET /api/request-types → id 5). Захардкожен, как «отпуск = 1» в pto-tracker.
@@ -54,7 +54,7 @@ async function writeContent(data: IFormData, status: string): Promise<number> {
}
}
const added = await getSP().web.lists.getByTitle(LIST_TITLE).items.add(item);
const added = await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.add(item);
return (added as { Id: number }).Id;
}
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { WebPartContext } from '@microsoft/sp-webpart-base';
export interface IApprovalViewProps {
context: WebPartContext;
itemId: number;
}
type ViewComponent = React.ComponentType<IApprovalViewProps>;
/**
* Реестр форм «только для чтения» (режим согласующего): ключ формы → ленивый просмотр.
* Добавить просмотр = создать <Форма>View.tsx + строка здесь.
*/
const viewRegistry: Record<string, React.LazyExoticComponent<ViewComponent>> = {
'recruiting-hire': React.lazy(
() => import(/* webpackChunkName: 'view-recruiting-hire' */ './RecruitingHire/RecruitingHireView')
)
};
export function getViewComponent(
formKey: string
): React.LazyExoticComponent<ViewComponent> | undefined {
return viewRegistry[formKey];
}