feat: added notifications workflow
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import styles from './RecruitingHireForm.module.scss';
|
||||
import {
|
||||
Alert,
|
||||
FormCard,
|
||||
FormModeProvider,
|
||||
FileDropzone,
|
||||
AttachmentList,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
Icon,
|
||||
@@ -18,24 +19,66 @@ import {
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
getPlannedMatrix,
|
||||
notifyActiveStep,
|
||||
REQUEST_STATUS,
|
||||
isEditableStatus,
|
||||
IApproverToWrite
|
||||
} from '../../shared/approval';
|
||||
import { uploadAttachments } from '../../shared/attachments/attachments.service';
|
||||
import {
|
||||
uploadAttachments,
|
||||
getAttachments,
|
||||
deleteAttachment,
|
||||
IAttachmentInfo
|
||||
} from '../../shared/attachments/attachments.service';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { saveHireRequest, hireApprovalService, HIRE_REQUEST_TYPE_ID, DATA_LIST_TITLE } from './submit';
|
||||
import { loadHireRequest } from './load';
|
||||
import {
|
||||
saveHireRequest,
|
||||
hireApprovalService,
|
||||
HIRE_REQUEST_TYPE_ID,
|
||||
DATA_LIST_TITLE,
|
||||
APPROVAL_LIST_TITLE
|
||||
} from './submit';
|
||||
import { ApplicationSection } from './sections/ApplicationSection';
|
||||
import { CandidateSection } from './sections/CandidateSection';
|
||||
import { PpeSection } from './sections/PpeSection';
|
||||
import { ConditionsSection } from './sections/ConditionsSection';
|
||||
|
||||
const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context, onCancel, onSaved }) => {
|
||||
const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
requestType,
|
||||
context,
|
||||
onCancel,
|
||||
onSaved,
|
||||
editId
|
||||
}) => {
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [existingAttachments, setExistingAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [requestId, setRequestId] = useState<number | undefined>(undefined);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
|
||||
// Режим правки: грузим существующую заявку (id/статус → сохранение как update) и
|
||||
// её уже прикреплённые вложения.
|
||||
useEffect(() => {
|
||||
if (!editId) {
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
Promise.all([loadHireRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
||||
.then(([loaded, atts]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setRequestId(editId);
|
||||
setRequestStatus(loaded.status);
|
||||
setExistingAttachments(atts);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [editId]);
|
||||
const submit = useRequestSubmit();
|
||||
|
||||
const readOnly = !isEditableStatus(requestStatus);
|
||||
@@ -70,6 +113,16 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||
rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
||||
|
||||
// Удаление уже прикреплённого файла (в правке) — сразу из заявки.
|
||||
const handleRemoveAttachment = (fileName: string): void => {
|
||||
if (requestId === undefined) {
|
||||
return;
|
||||
}
|
||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
||||
const id = await saveHireRequest(
|
||||
requestId,
|
||||
@@ -83,10 +136,20 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
if (files.length > 0) {
|
||||
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные
|
||||
}
|
||||
if (onSaved) {
|
||||
onSaved();
|
||||
}
|
||||
// Отправка на согласование — уведомляем первого активного согласующего
|
||||
// (поток оповещения). Черновик не уведомляет. Ошибки внутри проглатываются.
|
||||
if (status === REQUEST_STATUS.onApproval) {
|
||||
await notifyActiveStep(context, approval, id, {
|
||||
requestTypeId: HIRE_REQUEST_TYPE_ID,
|
||||
dataListName: DATA_LIST_TITLE,
|
||||
approvalListName: APPROVAL_LIST_TITLE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveDraft = (): Promise<void> =>
|
||||
@@ -131,7 +194,12 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
|
||||
{!readOnly && (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
<FormField label="Файлы" full>
|
||||
{existingAttachments.length > 0 && (
|
||||
<FormField label="Прикреплённые файлы" full>
|
||||
<AttachmentList items={existingAttachments} onRemove={handleRemoveAttachment} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label={existingAttachments.length > 0 ? 'Добавить файлы' : 'Файлы'} full>
|
||||
<FileDropzone files={files} onChange={setFiles} />
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
StatusBadge,
|
||||
ApprovalMatrix,
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
isEditableStatus,
|
||||
APPROVAL_PHASE
|
||||
} from '../../shared/approval';
|
||||
import { navigate } from '../../shared/routing';
|
||||
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';
|
||||
import { hireApprovalService, HIRE_REQUEST_TYPE_ID, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||
import { ApplicationSection } from './sections/ApplicationSection';
|
||||
import { CandidateSection } from './sections/CandidateSection';
|
||||
import { PpeSection } from './sections/PpeSection';
|
||||
@@ -119,6 +121,17 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
return (
|
||||
<div>
|
||||
<div className={`${styles.toolbar} rp-no-print`}>
|
||||
{isEditableStatus(status) && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
navigate({ mode: 'edit', nav: 'new', formKey: 'recruiting-hire', itemId })
|
||||
}
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
@@ -174,6 +187,12 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
onDecided={(newStatus) => {
|
||||
setStatus(newStatus);
|
||||
setDecisionSeq((n) => n + 1);
|
||||
// Очередь сдвинулась — уведомляем следующий активный шаг.
|
||||
notifyActiveStep(context, approval, itemId, {
|
||||
requestTypeId: HIRE_REQUEST_TYPE_ID,
|
||||
dataListName: DATA_LIST_TITLE,
|
||||
approvalListName: APPROVAL_LIST_TITLE,
|
||||
}).catch(() => undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dataListTitle": "Прием на работу — данные",
|
||||
"columns": [
|
||||
{ "internalName": "RequestUniqueCode", "title": "Уникальный код заявки", "type": "Text" },
|
||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
|
||||
{ "internalName": "Phone", "title": "Телефон", "type": "Text" },
|
||||
|
||||
@@ -61,6 +61,8 @@ export async function saveHireRequest(
|
||||
} 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) });
|
||||
}
|
||||
|
||||
await writeRoute(hireApprovalService(), id, approvers, executors, status);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dataListTitle": "Заявка на подбор — данные",
|
||||
"columns": [
|
||||
{ "internalName": "RequestUniqueCode", "title": "Уникальный код заявки", "type": "Text" },
|
||||
{ "internalName": "Responsible", "title": "Ответственный за подачу заявки", "type": "User", "userSelectionMode": "PeopleOnly", "required": true },
|
||||
{ "internalName": "SubmitDate", "title": "Дата подачи заявки", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "IsRegistered", "title": "Зарегистрирована", "type": "Boolean" },
|
||||
|
||||
@@ -54,5 +54,8 @@ export async function submitRecruitingSelection(data: IFormData): Promise<void>
|
||||
item[`${MULTI_PERSON_FIELDS[i]}Id`] = ids;
|
||||
}
|
||||
|
||||
await sp.web.lists.getByTitle(LIST_TITLE).items.add(item);
|
||||
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) });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FormCard,
|
||||
FormModeProvider,
|
||||
FileDropzone,
|
||||
AttachmentList,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
Icon,
|
||||
@@ -18,26 +19,42 @@ import {
|
||||
StatusBadge,
|
||||
useMatrixEditor,
|
||||
getPlannedMatrix,
|
||||
notifyActiveStep,
|
||||
REQUEST_STATUS,
|
||||
isEditableStatus,
|
||||
IApproverToWrite
|
||||
} from '../../shared/approval';
|
||||
import { uploadAttachments } from '../../shared/attachments/attachments.service';
|
||||
import {
|
||||
uploadAttachments,
|
||||
getAttachments,
|
||||
deleteAttachment,
|
||||
IAttachmentInfo
|
||||
} from '../../shared/attachments/attachments.service';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import {
|
||||
saveWorkplaceRequest,
|
||||
workplaceApprovalService,
|
||||
WORKPLACE_REQUEST_TYPE_ID,
|
||||
DATA_LIST_TITLE
|
||||
DATA_LIST_TITLE,
|
||||
APPROVAL_LIST_TITLE
|
||||
} from './submit';
|
||||
import { loadPrefillFromHire } from './prefill';
|
||||
import { loadWorkplaceRequest } from './load';
|
||||
import { EmployeeSection } from './sections/EmployeeSection';
|
||||
import { EquipmentSection } from './sections/EquipmentSection';
|
||||
|
||||
const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context, onCancel, onSaved, sourceId }) => {
|
||||
const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
||||
requestType,
|
||||
context,
|
||||
onCancel,
|
||||
onSaved,
|
||||
sourceId,
|
||||
editId
|
||||
}) => {
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [existingAttachments, setExistingAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
// Id и статус текущей заявки: заполняются после первого сохранения.
|
||||
const [requestId, setRequestId] = useState<number | undefined>(undefined);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
@@ -48,6 +65,27 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// Режим правки: грузим существующую заявку и её id/статус (сохранение → update).
|
||||
useEffect(() => {
|
||||
if (!editId) {
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
Promise.all([loadWorkplaceRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
||||
.then(([loaded, atts]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setRequestId(editId);
|
||||
setRequestStatus(loaded.status);
|
||||
setExistingAttachments(atts);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [editId]);
|
||||
|
||||
// Предзаполнение из «Заявки на прием», если форма открыта «на основе» (rpsource).
|
||||
useEffect(() => {
|
||||
if (!sourceId) {
|
||||
@@ -94,6 +132,16 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||
rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
||||
|
||||
// Удаление уже прикреплённого файла (в правке) — сразу из заявки.
|
||||
const handleRemoveAttachment = (fileName: string): void => {
|
||||
if (requestId === undefined) {
|
||||
return;
|
||||
}
|
||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
|
||||
const id = await saveWorkplaceRequest(
|
||||
requestId,
|
||||
@@ -108,10 +156,19 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
if (files.length > 0) {
|
||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные
|
||||
}
|
||||
if (onSaved) {
|
||||
onSaved();
|
||||
}
|
||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||
if (status === REQUEST_STATUS.onApproval) {
|
||||
await notifyActiveStep(context, approval, id, {
|
||||
requestTypeId: WORKPLACE_REQUEST_TYPE_ID,
|
||||
dataListName: DATA_LIST_TITLE,
|
||||
approvalListName: APPROVAL_LIST_TITLE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveDraft = (): Promise<void> =>
|
||||
@@ -154,7 +211,12 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
||||
|
||||
{!readOnly && (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
<FormField label="Файлы" full>
|
||||
{existingAttachments.length > 0 && (
|
||||
<FormField label="Прикреплённые файлы" full>
|
||||
<AttachmentList items={existingAttachments} onRemove={handleRemoveAttachment} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label={existingAttachments.length > 0 ? 'Добавить файлы' : 'Файлы'} full>
|
||||
<FileDropzone files={files} onChange={setFiles} />
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
|
||||
@@ -10,20 +10,30 @@ import {
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton
|
||||
PrintButton,
|
||||
Icon
|
||||
} from '../../shared/ui';
|
||||
import { navigate } from '../../shared/routing';
|
||||
import {
|
||||
ApproverService,
|
||||
DecisionPanel,
|
||||
StatusBadge,
|
||||
ApprovalMatrix,
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
isEditableStatus,
|
||||
APPROVAL_PHASE
|
||||
} from '../../shared/approval';
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { loadWorkplaceRequest } from './load';
|
||||
import { workplaceApprovalService, updateReviewFields, DATA_LIST_TITLE } from './submit';
|
||||
import {
|
||||
workplaceApprovalService,
|
||||
updateReviewFields,
|
||||
WORKPLACE_REQUEST_TYPE_ID,
|
||||
DATA_LIST_TITLE,
|
||||
APPROVAL_LIST_TITLE
|
||||
} from './submit';
|
||||
import { EmployeeSection } from './sections/EmployeeSection';
|
||||
import { EquipmentSection } from './sections/EquipmentSection';
|
||||
|
||||
@@ -162,6 +172,17 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
return (
|
||||
<div>
|
||||
<div className={`${styles.toolbar} rp-no-print`}>
|
||||
{isEditableStatus(status) && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
navigate({ mode: 'edit', nav: 'new', formKey: 'it-workplace-setup', itemId })
|
||||
}
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
<PrintButton />
|
||||
</div>
|
||||
|
||||
@@ -219,6 +240,12 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
onDecided={(newStatus) => {
|
||||
setStatus(newStatus);
|
||||
setDecisionSeq((n) => n + 1);
|
||||
// Очередь сдвинулась — уведомляем следующий активный шаг.
|
||||
notifyActiveStep(context, approval, itemId, {
|
||||
requestTypeId: WORKPLACE_REQUEST_TYPE_ID,
|
||||
dataListName: DATA_LIST_TITLE,
|
||||
approvalListName: APPROVAL_LIST_TITLE,
|
||||
}).catch(() => undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dataListTitle": "Организация рабочего места — данные",
|
||||
"columns": [
|
||||
{ "internalName": "RequestUniqueCode", "title": "Уникальный код заявки", "type": "Text" },
|
||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
|
||||
{ "internalName": "FullName", "title": "Ф.И.О.", "type": "Text", "required": true },
|
||||
|
||||
@@ -75,6 +75,8 @@ export async function saveWorkplaceRequest(
|
||||
} 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) });
|
||||
}
|
||||
|
||||
await writeRoute(workplaceApprovalService(), id, approvers, executors, status);
|
||||
|
||||
Reference in New Issue
Block a user