This commit is contained in:
@@ -55,6 +55,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
// после первого сохранения.
|
||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
const [requestCode, setRequestCode] = useState<string | undefined>(undefined);
|
||||
const submit = useRequestSubmit();
|
||||
|
||||
const readOnly = !isEditableStatus(requestStatus);
|
||||
@@ -73,6 +74,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setRequestStatus(loaded.status);
|
||||
setRequestCode(loaded.code || undefined);
|
||||
setExistingAttachments(atts);
|
||||
}
|
||||
})
|
||||
@@ -152,7 +154,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
};
|
||||
|
||||
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
||||
const id = await saveHireRequest(
|
||||
const { id, code } = await saveHireRequest(
|
||||
requestId,
|
||||
data,
|
||||
toApprovers(matrix.rows),
|
||||
@@ -161,6 +163,9 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
);
|
||||
setRequestId(id);
|
||||
setRequestStatus(status);
|
||||
if (code) {
|
||||
setRequestCode(code);
|
||||
}
|
||||
if (files.length > 0) {
|
||||
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||
@@ -218,7 +223,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
requestId || requestStatus ? (
|
||||
<>
|
||||
{requestStatus && <StatusBadge status={requestStatus} />}
|
||||
{requestId && <RequestNumber id={requestId} />}
|
||||
{requestCode && <RequestNumber code={requestCode} />}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const NOOP_SET: SetField = () => undefined;
|
||||
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId }) => {
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [code, setCode] = useState<string>('');
|
||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -89,6 +90,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
setAttachments(files);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
@@ -140,7 +142,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
<div className="rp-print-root">
|
||||
<FormCard
|
||||
title="Заявка на прием"
|
||||
subtitle={`№${itemId}`}
|
||||
subtitle={`№${code || itemId}`}
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FormModeProvider readOnly>
|
||||
|
||||
@@ -13,6 +13,8 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||
export interface ILoadedRequest {
|
||||
data: IFormData;
|
||||
status: string;
|
||||
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** Прочитать заявку «на прием» по id: модель формы + статус (для режима чтения). */
|
||||
@@ -21,7 +23,7 @@ export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
||||
const item = (await sp.web.lists
|
||||
.getByTitle(LIST_TITLE)
|
||||
.items.getById(itemId)
|
||||
.select(...SCALAR_FIELDS, 'Status')()) as Record<string, unknown>;
|
||||
.select(...SCALAR_FIELDS, 'Status', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = { ...EMPTY_DATA };
|
||||
const writable = data as unknown as Record<string, string>;
|
||||
@@ -37,5 +39,5 @@ export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
||||
}
|
||||
});
|
||||
|
||||
return { data, status: (item.Status as string) || '' };
|
||||
return { data, status: (item.Status as string) || '', code: (item.RequestUniqueCode as string) || '' };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||
import { IFormData } from './model';
|
||||
|
||||
export const DATA_LIST_TITLE = 'Прием на работу — данные';
|
||||
@@ -37,12 +38,18 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||
return item;
|
||||
}
|
||||
|
||||
export interface ISaveResult {
|
||||
id: number;
|
||||
/** Задан только при создании новой заявки (существующий код не меняется). */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить заявку «на прием»: контент + маршрут (согласующие + исполнители) со статусом.
|
||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||
* status = «Черновик» (сохранение) или «На согласовании» (отправка).
|
||||
* executors может быть пустым — тогда после согласования заявка станет «Согласовано».
|
||||
* Возвращает Id заявки.
|
||||
* Возвращает Id заявки (+ код при создании).
|
||||
*/
|
||||
export async function saveHireRequest(
|
||||
existingId: number | undefined,
|
||||
@@ -50,21 +57,23 @@ export async function saveHireRequest(
|
||||
approvers: IApproverToWrite[],
|
||||
executors: IApproverToWrite[],
|
||||
status: string
|
||||
): Promise<number> {
|
||||
): Promise<ISaveResult> {
|
||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
||||
const item = buildItem(data, status);
|
||||
|
||||
let id: number;
|
||||
let code: string | undefined;
|
||||
if (existingId) {
|
||||
await list.items.getById(existingId).update(item);
|
||||
id = existingId;
|
||||
} else {
|
||||
const added = await list.items.add(item);
|
||||
id = (added as { Id: number }).Id;
|
||||
// Уникальный код заявки: пока = id (id известен только после создания).
|
||||
await list.items.getById(id).update({ RequestUniqueCode: String(id) });
|
||||
// Код известен только после создания элемента (нужен его Id).
|
||||
code = buildRequestUniqueCode(HIRE_REQUEST_TYPE_ID, id, new Date());
|
||||
await list.items.getById(id).update({ RequestUniqueCode: code });
|
||||
}
|
||||
|
||||
await writeRoute(hireApprovalService(), id, approvers, executors, status);
|
||||
return id;
|
||||
return { id, code };
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [saved, setSaved] = useState<boolean>(false);
|
||||
const [savedCode, setSavedCode] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const set: SetField = (key, value) => {
|
||||
@@ -35,8 +36,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
||||
setError(null);
|
||||
setSaving(true);
|
||||
try {
|
||||
await submitRecruitingSelection(data);
|
||||
const code = await submitRecruitingSelection(data);
|
||||
setSaved(true);
|
||||
setSavedCode(code);
|
||||
if (onSaved) {
|
||||
onSaved();
|
||||
}
|
||||
@@ -57,7 +59,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
||||
</button>
|
||||
|
||||
<FormCard title={requestType.title} subtitle="Категория: Отдел рекрутинга">
|
||||
{saved && <Alert variant="ok">Заявка сохранена в SharePoint.</Alert>}
|
||||
{saved && (
|
||||
<Alert variant="ok">Заявка сохранена в SharePoint. Код заявки: №{savedCode}.</Alert>
|
||||
)}
|
||||
{error && <Alert variant="err">{error}</Alert>}
|
||||
|
||||
<ApplicationSection {...sectionProps} />
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||
import { IFormData } from './model';
|
||||
|
||||
const LIST_TITLE = 'Заявка на подбор — данные';
|
||||
// Тип заявки во внешнем approval-route API (GET /api/request-types → id 4).
|
||||
const RECRUITING_SELECTION_REQUEST_TYPE_ID = 4;
|
||||
|
||||
// Поля-люди (один человек): логин → ensureUser → Id в колонку <Field>Id.
|
||||
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
||||
@@ -16,8 +19,8 @@ async function resolveUserId(login: string): Promise<number> {
|
||||
return ensured.Id;
|
||||
}
|
||||
|
||||
/** Записать заявку «на подбор» в список данных SharePoint. */
|
||||
export async function submitRecruitingSelection(data: IFormData): Promise<void> {
|
||||
/** Записать заявку «на подбор» в список данных SharePoint. Возвращает уникальный код заявки. */
|
||||
export async function submitRecruitingSelection(data: IFormData): Promise<string> {
|
||||
const sp = getSP();
|
||||
const item: Record<string, unknown> = {};
|
||||
|
||||
@@ -56,6 +59,8 @@ export async function submitRecruitingSelection(data: IFormData): Promise<void>
|
||||
|
||||
const added = await sp.web.lists.getByTitle(LIST_TITLE).items.add(item);
|
||||
const id = (added as { Id: number }).Id;
|
||||
// Уникальный код заявки: пока = id (id известен только после создания).
|
||||
await sp.web.lists.getByTitle(LIST_TITLE).items.getById(id).update({ RequestUniqueCode: String(id) });
|
||||
// Код известен только после создания элемента (нужен его Id).
|
||||
const code = buildRequestUniqueCode(RECRUITING_SELECTION_REQUEST_TYPE_ID, id, new Date());
|
||||
await sp.web.lists.getByTitle(LIST_TITLE).items.getById(id).update({ RequestUniqueCode: code });
|
||||
return code;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
// открытии на редактирование (editId) — сразу из загруженной записи.
|
||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
const [requestCode, setRequestCode] = useState<string | undefined>(undefined);
|
||||
const submit = useRequestSubmit();
|
||||
|
||||
// Редактировать можно новую/черновик/возвращённую/отклонённую; иначе только чтение.
|
||||
@@ -74,6 +75,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setRequestStatus(loaded.status);
|
||||
setRequestCode(loaded.code || undefined);
|
||||
setExistingAttachments(atts);
|
||||
}
|
||||
})
|
||||
@@ -175,7 +177,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
};
|
||||
|
||||
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
||||
const id = await saveWorkplaceRequest(
|
||||
const { id, code } = await saveWorkplaceRequest(
|
||||
requestId,
|
||||
data,
|
||||
toApprovers(matrix.rows),
|
||||
@@ -185,6 +187,9 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
);
|
||||
setRequestId(id);
|
||||
setRequestStatus(status);
|
||||
if (code) {
|
||||
setRequestCode(code);
|
||||
}
|
||||
if (files.length > 0) {
|
||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||
@@ -242,7 +247,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
requestId || requestStatus ? (
|
||||
<>
|
||||
{requestStatus && <StatusBadge status={requestStatus} />}
|
||||
{requestId && <RequestNumber id={requestId} />}
|
||||
{requestCode && <RequestNumber code={requestCode} />}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemId }) => {
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [code, setCode] = useState<string>('');
|
||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -101,6 +102,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
setAttachments(files);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
@@ -202,7 +204,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
<div className="rp-print-root">
|
||||
<FormCard
|
||||
title="Организация рабочего места"
|
||||
subtitle={`№${itemId}`}
|
||||
subtitle={`№${code || itemId}`}
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FormModeProvider readOnly>
|
||||
|
||||
@@ -11,6 +11,8 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||
export interface ILoadedRequest {
|
||||
data: IFormData;
|
||||
status: string;
|
||||
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** Прочитать заявку по id: модель формы + статус (для режима чтения). */
|
||||
@@ -18,7 +20,7 @@ export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedReque
|
||||
const item = (await getSP()
|
||||
.web.lists.getByTitle(LIST_TITLE)
|
||||
.items.getById(itemId)
|
||||
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'Status')()) as Record<string, unknown>;
|
||||
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'Status', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = { ...EMPTY_DATA };
|
||||
const writable = data as unknown as Record<string, string>;
|
||||
@@ -36,5 +38,5 @@ export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedReque
|
||||
|
||||
data.NeedsEquipment = item.NeedsEquipment === true;
|
||||
|
||||
return { data, status: (item.Status as string) || '' };
|
||||
return { data, status: (item.Status as string) || '', code: (item.RequestUniqueCode as string) || '' };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||
import { IFormData } from './model';
|
||||
|
||||
export const DATA_LIST_TITLE = 'Организация рабочего места — данные';
|
||||
@@ -51,11 +52,17 @@ function buildItem(data: IFormData, status: string, sourceHireId?: number): Reco
|
||||
return item;
|
||||
}
|
||||
|
||||
export interface ISaveResult {
|
||||
id: number;
|
||||
/** Задан только при создании новой заявки (существующий код не меняется). */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить заявку «орг. рабочего места»: контент + маршрут (согласующие + исполнители).
|
||||
* existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую.
|
||||
* Исполнители пишутся сразу — фаза исполнения стартует автоматически после согласования.
|
||||
* sourceHireId — если заявка создана на основе «прием». Возвращает Id заявки.
|
||||
* sourceHireId — если заявка создана на основе «прием». Возвращает Id заявки (+ код при создании).
|
||||
*/
|
||||
export async function saveWorkplaceRequest(
|
||||
existingId: number | undefined,
|
||||
@@ -64,21 +71,23 @@ export async function saveWorkplaceRequest(
|
||||
executors: IApproverToWrite[],
|
||||
status: string,
|
||||
sourceHireId?: number
|
||||
): Promise<number> {
|
||||
): Promise<ISaveResult> {
|
||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
||||
const item = buildItem(data, status, sourceHireId);
|
||||
|
||||
let id: number;
|
||||
let code: string | undefined;
|
||||
if (existingId) {
|
||||
await list.items.getById(existingId).update(item);
|
||||
id = existingId;
|
||||
} else {
|
||||
const added = await list.items.add(item);
|
||||
id = (added as { Id: number }).Id;
|
||||
// Уникальный код заявки: пока = id (id известен только после создания).
|
||||
await list.items.getById(id).update({ RequestUniqueCode: String(id) });
|
||||
// Код известен только после создания элемента (нужен его Id).
|
||||
code = buildRequestUniqueCode(WORKPLACE_REQUEST_TYPE_ID, id, new Date());
|
||||
await list.items.getById(id).update({ RequestUniqueCode: code });
|
||||
}
|
||||
|
||||
await writeRoute(workplaceApprovalService(), id, approvers, executors, status);
|
||||
return id;
|
||||
return { id, code };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Уникальный код заявки: {requestTypeId}-{YY}{MM}-{itemId}.
|
||||
* requestTypeId — id типа заявки во внешнем approval-route API (GET /api/request-types).
|
||||
* YY/MM — год и месяц подачи, itemId — Id элемента в списке данных. Пример: 1-2608-12.
|
||||
*/
|
||||
export function buildRequestUniqueCode(requestTypeId: number, itemId: number, submittedAt: Date): string {
|
||||
const yy = String(submittedAt.getFullYear()).slice(-2);
|
||||
const month = submittedAt.getMonth() + 1;
|
||||
const mm = month < 10 ? `0${month}` : String(month);
|
||||
return `${requestTypeId}-${yy}${mm}-${itemId}`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import styles from './RequestNumber.module.scss';
|
||||
|
||||
/** Номер заявки (Id элемента списка) — плашка в правом углу шапки формы. */
|
||||
export const RequestNumber: React.FC<{ id: number }> = ({ id }) => (
|
||||
<span className={styles.badge}>№{id}</span>
|
||||
/** Уникальный код заявки ({requestTypeId}-{YY}{MM}-{itemId}) — плашка в правом углу шапки формы. */
|
||||
export const RequestNumber: React.FC<{ code: string }> = ({ code }) => (
|
||||
<span className={styles.badge}>№{code}</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user