--no-edit
This commit is contained in:
@@ -22,7 +22,9 @@ import {
|
||||
notifyActiveStep,
|
||||
REQUEST_STATUS,
|
||||
isEditableStatus,
|
||||
IApproverToWrite
|
||||
IApproverToWrite,
|
||||
APPROVAL_PHASE,
|
||||
findUnassignedStep
|
||||
} from '../../shared/approval';
|
||||
import {
|
||||
uploadAttachments,
|
||||
@@ -32,7 +34,6 @@ import {
|
||||
} from '../../shared/attachments/attachments.service';
|
||||
import { IRequestFormProps } from '../../models/types';
|
||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { loadHireRequest } from './load';
|
||||
import {
|
||||
saveHireRequest,
|
||||
hireApprovalService,
|
||||
@@ -40,26 +41,28 @@ import {
|
||||
DATA_LIST_TITLE,
|
||||
APPROVAL_LIST_TITLE
|
||||
} from './submit';
|
||||
import { loadHireRequest } from './load';
|
||||
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,
|
||||
editId
|
||||
}) => {
|
||||
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);
|
||||
// При открытии на редактирование (editId) — сразу известен, иначе появится
|
||||
// после первого сохранения.
|
||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
const submit = useRequestSubmit();
|
||||
|
||||
// Режим правки: грузим существующую заявку (id/статус → сохранение как update) и
|
||||
// её уже прикреплённые вложения.
|
||||
const readOnly = !isEditableStatus(requestStatus);
|
||||
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// Открыта СВОЯ заявка на редактирование — подгружаем её содержимое, статус и
|
||||
// уже прикреплённые вложения.
|
||||
useEffect(() => {
|
||||
if (!editId) {
|
||||
return;
|
||||
@@ -69,41 +72,59 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
.then(([loaded, atts]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setRequestId(editId);
|
||||
setRequestStatus(loaded.status);
|
||||
setExistingAttachments(atts);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
.catch((e) => {
|
||||
if (alive) {
|
||||
submit.setError(`Не удалось загрузить заявку №${editId}: ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [editId]);
|
||||
const submit = useRequestSubmit();
|
||||
|
||||
const readOnly = !isEditableStatus(requestStatus);
|
||||
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// Маршрут согласования (планируемый) резолвится по email инициатора — текущего
|
||||
// пользователя (кандидата ещё нет в системе).
|
||||
// пользователя (кандидата ещё нет в системе). У заявки, открытой на
|
||||
// редактирование, маршрут читается из уже записанных строк — планирование
|
||||
// затёрло бы правки, сделанные при первом сохранении.
|
||||
const approval = useMemo(() => hireApprovalService(), []);
|
||||
const userEmail = context.pageContext.user.email;
|
||||
const loadMatrix = useCallback(
|
||||
() => getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Approval'),
|
||||
[context, userEmail, approval]
|
||||
() =>
|
||||
editId
|
||||
? approval
|
||||
.getMatrixForRequest(editId)
|
||||
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
||||
: getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Approval'),
|
||||
[context, userEmail, approval, editId]
|
||||
);
|
||||
const matrix = useMatrixEditor(loadMatrix, userEmail, !readOnly, true, (login) =>
|
||||
approval.getJobTitle(login)
|
||||
const matrix = useMatrixEditor(
|
||||
loadMatrix,
|
||||
`${userEmail}-${editId ?? 'new'}`,
|
||||
!readOnly,
|
||||
true,
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
|
||||
// Фаза исполнения: исполнители из маршрута (status='Execution'), только для чтения.
|
||||
const loadExecutors = useCallback(
|
||||
() => getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Execution'),
|
||||
[context, userEmail, approval]
|
||||
() =>
|
||||
editId
|
||||
? approval
|
||||
.getMatrixForRequest(editId)
|
||||
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
||||
: getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Execution'),
|
||||
[context, userEmail, approval, editId]
|
||||
);
|
||||
const executors = useMatrixEditor(loadExecutors, userEmail, false, true, (login) =>
|
||||
approval.getJobTitle(login)
|
||||
const executors = useMatrixEditor(
|
||||
loadExecutors,
|
||||
`${userEmail}-${editId ?? 'new'}`,
|
||||
false,
|
||||
true,
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
|
||||
const isValid = data.CandidateName.trim().length > 0 && data.Position.trim().length > 0;
|
||||
@@ -111,7 +132,14 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
isValid ? undefined : 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.';
|
||||
|
||||
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||
rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
||||
// order обязателен: параллельные шаги делят один номер, по позиции в
|
||||
// массиве его не восстановить (см. useMatrixEditor/withOrders).
|
||||
rows.map((r) => ({
|
||||
email: r.email,
|
||||
position: r.position,
|
||||
allowReassign: r.allowReassign,
|
||||
order: r.order,
|
||||
}));
|
||||
|
||||
// Удаление уже прикреплённого файла (в правке) — сразу из заявки.
|
||||
const handleRemoveAttachment = (fileName: string): void => {
|
||||
@@ -141,8 +169,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
if (onSaved) {
|
||||
onSaved();
|
||||
}
|
||||
// Отправка на согласование — уведомляем первого активного согласующего
|
||||
// (поток оповещения). Черновик не уведомляет. Ошибки внутри проглатываются.
|
||||
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
|
||||
if (status === REQUEST_STATUS.onApproval) {
|
||||
await notifyActiveStep(context, approval, id, {
|
||||
requestTypeId: HIRE_REQUEST_TYPE_ID,
|
||||
@@ -152,10 +179,28 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// На отправку (в отличие от черновика) маршрут должен быть полностью
|
||||
// укомплектован — иначе шаг без назначенного человека молча пропадёт при
|
||||
// записи в SharePoint (createSteps не может записать Person-поле без
|
||||
// значения), и согласующие в середине маршрута просто не увидят заявку.
|
||||
const validateSubmit = (): string | undefined => {
|
||||
const formError = validate();
|
||||
if (formError) return formError;
|
||||
const unassigned = findUnassignedStep(matrix.rows);
|
||||
if (unassigned) {
|
||||
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`;
|
||||
}
|
||||
const unassignedExecutor = findUnassignedStep(executors.rows);
|
||||
if (unassignedExecutor) {
|
||||
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleSaveDraft = (): Promise<void> =>
|
||||
submit.run('draft', save(REQUEST_STATUS.draft), validate);
|
||||
const handleSubmit = (): Promise<void> =>
|
||||
submit.run('submit', save(REQUEST_STATUS.onApproval), validate);
|
||||
submit.run('submit', save(REQUEST_STATUS.onApproval), validateSubmit);
|
||||
|
||||
const sectionProps = { data, set, context };
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
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 { confirmDiscardFormChanges } from '../../shared/unsavedChanges';
|
||||
import { hireApprovalService, HIRE_REQUEST_TYPE_ID, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||
import { ApplicationSection } from './sections/ApplicationSection';
|
||||
import { CandidateSection } from './sections/CandidateSection';
|
||||
@@ -121,22 +121,15 @@ 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={() =>
|
||||
navigate({ mode: 'create', nav: 'new', formKey: WORKPLACE_FORM_KEY, sourceId: itemId })
|
||||
}
|
||||
onClick={() => {
|
||||
// Раздел «Новая заявка» остаётся смонтированным при переключении
|
||||
// вкладок, но переход «на основе» пересоздаёт форму — если там
|
||||
// остались несохранённые правки, спрашиваем перед их потерей.
|
||||
if (!confirmDiscardFormChanges()) return;
|
||||
navigate({ mode: 'create', nav: 'new', formKey: WORKPLACE_FORM_KEY, sourceId: itemId });
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" size="sm" />
|
||||
Организация рабочего места на основе этой
|
||||
|
||||
Reference in New Issue
Block a user