feat: согласующий с полным правом правит вложения в подборе и приёме
Алтынай на своём шаге согласования теперь может добавлять и удалять файлы, а не только поля. Удаление — сразу, новые файлы загружаются вместе с «Сохранить» или решением. Блоки вложений вынесены из просмотров в отдельные компоненты на общем хуке useAttachmentEditor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@
|
||||
|---|---|---|---|---|
|
||||
| Удалять согласующих, которые ещё ждут очереди | Алтынай Бултурукова | email в коде | Подбор, Приём, Реферальная рекомендация: и при составлении, и в поданной заявке | `shared/approval/routeEditRights.ts` → `canEditRoute` |
|
||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
||||
| Править **все поля** заявки на своём активном шаге согласования (без вложений) | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
||||
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
|
||||
| Править карту СКУД и сумму за обучение | Начальник отдела кадрового делопроизводства | должность шага: «начальник/руководитель» + «кадров» + «делопроизводств» | Обходной лист | `forms/Clearance/approverRights.ts` → `canEditHrRecordsFields` |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@import '../../../../styles/design-tokens';
|
||||
|
||||
.empty {
|
||||
font-family: $font-sans;
|
||||
font-size: 13.5px;
|
||||
color: $gray-500;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { AttachmentList, FileDropzone, FormField } from '../../shared/ui';
|
||||
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
import { attachmentsOfCategory, categoryPrefix, uncategorizedAttachments } from './attachmentCategories';
|
||||
import styles from './HireAttachmentField.module.scss';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
editor: IAttachmentEditor;
|
||||
editable: boolean;
|
||||
/** Группа новых файлов в editor.pending. */
|
||||
groupKey: string;
|
||||
/** Категория в имени файла; не задана — «прочие» вложения без префикса. */
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/** Поле одной категории вложений: прикреплённые файлы + (в правке) выбор новых. */
|
||||
export const HireAttachmentField: React.FC<Props> = ({ label, editor, editable, groupKey, category }) => {
|
||||
const items = category ? attachmentsOfCategory(editor.items, category) : uncategorizedAttachments(editor.items);
|
||||
// В списке имена показаны без префикса категории — для удаления возвращаем его.
|
||||
const removeWithPrefix = (name: string): void => editor.remove(category ? categoryPrefix(category) + name : name);
|
||||
|
||||
return (
|
||||
<FormField label={label} full>
|
||||
{items.length > 0 && <AttachmentList items={items} onRemove={editable ? removeWithPrefix : undefined} />}
|
||||
{items.length === 0 && !editable && <span className={styles.empty}>—</span>}
|
||||
{editable && (
|
||||
<FileDropzone files={editor.pending[groupKey] || []} onChange={(files) => editor.setPending(groupKey, files)} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, CollapseSection } from '../../shared/ui';
|
||||
import { IAttachmentEditor, PrepareFiles } from '../../shared/attachments/useAttachmentEditor';
|
||||
import { ATT_CATEGORY_ID, ATT_CATEGORY_RESUME, prefixFiles, uncategorizedAttachments } from './attachmentCategories';
|
||||
import { HireAttachmentField } from './HireAttachmentField';
|
||||
|
||||
/**
|
||||
* Группы новых файлов → категория в имени файла (см. attachmentCategories).
|
||||
* У «прочих» категории нет — имя остаётся как есть.
|
||||
*/
|
||||
const GROUP_ID = 'id';
|
||||
const GROUP_RESUME = 'resume';
|
||||
const GROUP_OTHER = 'other';
|
||||
const GROUP_CATEGORY: Record<string, string | undefined> = {
|
||||
[GROUP_ID]: ATT_CATEGORY_ID,
|
||||
[GROUP_RESUME]: ATT_CATEGORY_RESUME
|
||||
};
|
||||
|
||||
/** Префикс категории для новых файлов группы — передаётся в editor.uploadPending. */
|
||||
export const prepareHireFiles: PrepareFiles = (groupKey, files) => {
|
||||
const category = GROUP_CATEGORY[groupKey];
|
||||
return category ? prefixFiles(files, category) : files;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
editor: IAttachmentEditor;
|
||||
/** Согласующий с полным правом на своём шаге — может добавлять и удалять файлы. */
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
/** Блок «Вложения» в просмотре заявки на прием: удостоверение, резюме, прочие. */
|
||||
export const HireAttachments: React.FC<Props> = ({ editor, editable }) => {
|
||||
// «Прочие» без правки показываем, только если они есть (старые заявки).
|
||||
const showOther = editable || uncategorizedAttachments(editor.items).length > 0;
|
||||
const fieldProps = { editor, editable };
|
||||
|
||||
return (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
{editor.error && <Alert variant="err">{editor.error}</Alert>}
|
||||
<HireAttachmentField label="Удостоверение" groupKey={GROUP_ID} category={ATT_CATEGORY_ID} {...fieldProps} />
|
||||
<HireAttachmentField label="Резюме" groupKey={GROUP_RESUME} category={ATT_CATEGORY_RESUME} {...fieldProps} />
|
||||
{showOther && <HireAttachmentField label="Прочие вложения" groupKey={GROUP_OTHER} {...fieldProps} />}
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
@@ -13,12 +13,6 @@
|
||||
margin-bottom: $sp-4;
|
||||
}
|
||||
|
||||
.emptyAtt {
|
||||
font-family: $font-sans;
|
||||
font-size: 13.5px;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
// Полоса «вы можете заполнить оклады» для начальника расчётной группы —
|
||||
// см. approverRights.ts (тот же приём, что и в Clearance/ClearanceView).
|
||||
.reviewBar {
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
Icon,
|
||||
PrintButton,
|
||||
FieldChangesProvider,
|
||||
@@ -39,7 +36,8 @@ import {
|
||||
} from '../../shared/approval';
|
||||
import { navigate } from '../../shared/routing';
|
||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
import { getAttachments } from '../../shared/attachments/attachments.service';
|
||||
import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import {
|
||||
approvalListTitle,
|
||||
@@ -49,7 +47,7 @@ import {
|
||||
} from '../../shared/listTitles';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { loadHireRequest } from './load';
|
||||
import { ATT_CATEGORY_ID, ATT_CATEGORY_RESUME, attachmentsOfCategory, uncategorizedAttachments } from './attachmentCategories';
|
||||
import { HireAttachments, prepareHireFiles } from './HireAttachments';
|
||||
import { confirmDiscardFormChanges } from '../../shared/unsavedChanges';
|
||||
import { hireApprovalService, updateApproverFields, updateAllHireFields, IApproverEditableFields } from './submit';
|
||||
import { submitIssues } from './validation';
|
||||
@@ -81,14 +79,14 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
const HIRE_DATE_REQUIRED_MESSAGE = 'укажите дату приёма (п. 2.2) — без неё заявку нельзя отметить выполненной.';
|
||||
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
||||
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля заявки (кроме вложений).';
|
||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||
|
||||
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
||||
// те же секции, что и форма подачи; редактирование выключено через
|
||||
// <FormModeProvider readOnly>, КРОМЕ окладов (4.2/4.3) и отдела/участка (2.4) —
|
||||
// их на своём шаге правит начальник расчётной группы (см. approverRights.ts), —
|
||||
// и даты приёма (2.2), которую проставляет активный исполнитель. Согласующий
|
||||
// из fullEditRights.ts на своём шаге правит все поля (вложения — нет).
|
||||
// из fullEditRights.ts на своём шаге правит все поля и вложения.
|
||||
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
||||
@@ -98,7 +96,6 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
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);
|
||||
const [payrollEditable, setPayrollEditable] = useState<boolean>(false);
|
||||
@@ -137,6 +134,9 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
() => new ApproverService(hireApprovalService(listId), dataListRef),
|
||||
[listId, dataListRef]
|
||||
);
|
||||
const attachments = useAttachmentEditor(dataListRef, itemId);
|
||||
// Сеттер стабилен (useState) — безопасно класть в зависимости эффекта загрузки.
|
||||
const { setItems: setAttachmentItems } = attachments;
|
||||
|
||||
// Матрица маршрута из фактических строк заявки (read-only), разбитая по фазам.
|
||||
//
|
||||
@@ -207,6 +207,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
await updateAllHireFields(itemId, data, nextChanges, listId);
|
||||
setApproverChanges(nextChanges);
|
||||
setSavedData(data);
|
||||
await attachments.uploadPending(prepareHireFiles);
|
||||
return;
|
||||
}
|
||||
const fields: Partial<IApproverEditableFields> = {};
|
||||
@@ -278,7 +279,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
notifyCode(loaded.code);
|
||||
setAttachments(files);
|
||||
setAttachmentItems(files);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -292,7 +293,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [itemId, listId, notifyCode]);
|
||||
}, [itemId, listId, notifyCode, setAttachmentItems]);
|
||||
|
||||
if (loading) {
|
||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||
@@ -363,7 +364,6 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
@@ -375,27 +375,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
</FieldChangesProvider>
|
||||
|
||||
<div className="rp-no-print">
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
<FormField label="Удостоверение" full>
|
||||
{attachmentsOfCategory(attachments, ATT_CATEGORY_ID).length > 0 ? (
|
||||
<AttachmentList items={attachmentsOfCategory(attachments, ATT_CATEGORY_ID)} />
|
||||
) : (
|
||||
<span className={styles.emptyAtt}>—</span>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField label="Резюме" full>
|
||||
{attachmentsOfCategory(attachments, ATT_CATEGORY_RESUME).length > 0 ? (
|
||||
<AttachmentList items={attachmentsOfCategory(attachments, ATT_CATEGORY_RESUME)} />
|
||||
) : (
|
||||
<span className={styles.emptyAtt}>—</span>
|
||||
)}
|
||||
</FormField>
|
||||
{uncategorizedAttachments(attachments).length > 0 && (
|
||||
<FormField label="Прочие вложения" full>
|
||||
<AttachmentList items={uncategorizedAttachments(attachments)} />
|
||||
</FormField>
|
||||
)}
|
||||
</CollapseSection>
|
||||
<HireAttachments editor={attachments} editable={fullEditable} />
|
||||
</div>
|
||||
</FormCard>
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton,
|
||||
FieldChangesProvider,
|
||||
FieldChangesBanner
|
||||
@@ -36,7 +33,9 @@ import {
|
||||
APPROVAL_PHASE,
|
||||
ApproverDecision
|
||||
} from '../../shared/approval';
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
import { getAttachments } from '../../shared/attachments/attachments.service';
|
||||
import { useAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
import { SelectionAttachments } from './SelectionAttachments';
|
||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import {
|
||||
@@ -71,12 +70,12 @@ interface IRecruitingSelectionViewProps {
|
||||
|
||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля заявки (кроме вложений).';
|
||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||
|
||||
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
|
||||
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>,
|
||||
// кроме согласующего из fullEditRights.ts на его активном шаге — он правит все поля
|
||||
// (вложения — нет).
|
||||
// и вложения.
|
||||
const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
||||
@@ -86,7 +85,6 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
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);
|
||||
// Право удалять согласующих из уже поданного маршрута — см.
|
||||
@@ -115,6 +113,9 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
// архивной партиции, Title текущей — иначе (см. ApproverService.dataListRef).
|
||||
const dataListRef = listId || dataListTitle(FORM_ID);
|
||||
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
||||
const attachments = useAttachmentEditor(dataListRef, itemId);
|
||||
// Сеттер стабилен (useState) — безопасно класть в зависимости эффекта загрузки.
|
||||
const { setItems: setAttachmentItems } = attachments;
|
||||
|
||||
// Матрица маршрута из фактических строк заявки (read-only), разбитая по
|
||||
// фазам. decisionSeq в reloadKey: после решения строки на сервере меняются —
|
||||
@@ -169,6 +170,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
await updateAllSelectionFields(itemId, data, nextChanges, listId);
|
||||
setApproverChanges(nextChanges);
|
||||
setSavedData(data);
|
||||
await attachments.uploadPending();
|
||||
};
|
||||
|
||||
const handleSaveFields = async (): Promise<void> => {
|
||||
@@ -266,7 +268,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
notifyCode(loaded.code);
|
||||
setAttachments(files);
|
||||
setAttachmentItems(files);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -280,7 +282,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [itemId, listId, notifyCode]);
|
||||
}, [itemId, listId, notifyCode, setAttachmentItems]);
|
||||
|
||||
if (loading) {
|
||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||
@@ -327,7 +329,6 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
@@ -347,11 +348,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
</FieldChangesProvider>
|
||||
|
||||
<div className="rp-no-print">
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
<FormField label="Файлы" full>
|
||||
<AttachmentList items={attachments} />
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
<SelectionAttachments editor={attachments} editable={fullEditable} />
|
||||
</div>
|
||||
</FormCard>
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, AttachmentList, CollapseSection, FileDropzone, FormField } from '../../shared/ui';
|
||||
import { IAttachmentEditor } from '../../shared/attachments/useAttachmentEditor';
|
||||
|
||||
/** Единственная группа новых файлов в этой форме (категорий нет). */
|
||||
export const SELECTION_FILES_GROUP = 'files';
|
||||
|
||||
interface Props {
|
||||
editor: IAttachmentEditor;
|
||||
/** Согласующий с полным правом на своём шаге — может добавлять и удалять файлы. */
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
/** Блок «Вложения» в просмотре заявки на подбор. */
|
||||
export const SelectionAttachments: React.FC<Props> = ({ editor, editable }) => {
|
||||
const newFiles = editor.pending[SELECTION_FILES_GROUP] || [];
|
||||
|
||||
return (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
{editor.error && <Alert variant="err">{editor.error}</Alert>}
|
||||
<FormField label="Файлы" full>
|
||||
<AttachmentList items={editor.items} onRemove={editable ? editor.remove : undefined} />
|
||||
</FormField>
|
||||
{editable && (
|
||||
<FormField label="Добавить файлы" full>
|
||||
<FileDropzone files={newFiles} onChange={(files) => editor.setPending(SELECTION_FILES_GROUP, files)} />
|
||||
</FormField>
|
||||
)}
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
@@ -7,8 +7,9 @@
|
||||
* нет, как и в routeEditRights.ts. ВРЕМЕННО.
|
||||
*
|
||||
* Подключено только в «Заявке на подбор» и «Заявке на прием» (Реферальная
|
||||
* рекомендация — нет, по решению Тимура 2026-09-24). Вложения этим правом не
|
||||
* открываются — только поля.
|
||||
* рекомендация — нет, по решению Тимура 2026-09-24). С 2026-10-06 право
|
||||
* открывает и вложения: добавлять и удалять файлы (см. shared/attachments/
|
||||
* useAttachmentEditor).
|
||||
*/
|
||||
const FULL_FIELD_EDITOR_EMAILS: ReadonlySet<string> = new Set([
|
||||
'a.bulturukova@tnsu.kz' // Алтынай Бултурукова
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState } from 'react';
|
||||
import { deleteAttachment, getAttachments, IAttachmentInfo, uploadAttachments } from './attachments.service';
|
||||
import { friendlyErrorMessage } from '../diagnostics';
|
||||
|
||||
/**
|
||||
* Подготовка новых файлов группы к загрузке — напр. префикс категории в имени
|
||||
* (см. forms/RecruitingHire/attachmentCategories). По умолчанию — как есть.
|
||||
*/
|
||||
export type PrepareFiles = (groupKey: string, files: File[]) => File[];
|
||||
|
||||
export interface IAttachmentEditor {
|
||||
/** Вложения, уже прикреплённые к заявке. */
|
||||
items: IAttachmentInfo[];
|
||||
setItems: (items: IAttachmentInfo[]) => void;
|
||||
/** Выбранные, но ещё не загруженные файлы — по группам (категориям) формы. */
|
||||
pending: Record<string, File[]>;
|
||||
setPending: (groupKey: string, files: File[]) => void;
|
||||
/** Удалить прикреплённый файл сразу из заявки (по полному имени файла в SP). */
|
||||
remove: (fileName: string) => void;
|
||||
/** Загрузить все выбранные файлы и перечитать список вложений. */
|
||||
uploadPending: (prepare?: PrepareFiles) => Promise<void>;
|
||||
/** Ошибка последнего удаления — показать рядом со списком. */
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
const keepAsIs: PrepareFiles = (_groupKey, files) => files;
|
||||
|
||||
/**
|
||||
* Правка вложений уже поданной заявки — для согласующего с полным правом
|
||||
* (см. shared/approval/fullEditRights.ts). Удаление — сразу, как в форме
|
||||
* автора; новые файлы копятся и загружаются вместе с сохранением полей
|
||||
* (кнопка «Сохранить» или решение), чтобы не было второй кнопки сохранения.
|
||||
*
|
||||
* listRef — Title текущей партиции или url архивной (см. attachments.service).
|
||||
*/
|
||||
export function useAttachmentEditor(listRef: string, itemId: number): IAttachmentEditor {
|
||||
const [items, setItems] = useState<IAttachmentInfo[]>([]);
|
||||
const [pending, setPendingState] = useState<Record<string, File[]>>({});
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const setPending = (groupKey: string, files: File[]): void =>
|
||||
setPendingState((prev) => ({ ...prev, [groupKey]: files }));
|
||||
|
||||
const remove = (fileName: string): void => {
|
||||
setError(undefined);
|
||||
deleteAttachment(listRef, itemId, fileName)
|
||||
.then(() => setItems((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||
.catch((e) => setError(`Не удалось удалить файл «${fileName}»: ${friendlyErrorMessage(e)}`));
|
||||
};
|
||||
|
||||
const uploadPending = async (prepare: PrepareFiles = keepAsIs): Promise<void> => {
|
||||
const files = Object.keys(pending).reduce<File[]>(
|
||||
(acc, groupKey) => acc.concat(prepare(groupKey, pending[groupKey])),
|
||||
[]
|
||||
);
|
||||
if (files.length === 0) return;
|
||||
await uploadAttachments(listRef, itemId, files);
|
||||
// Загруженные убираем из выбора, иначе повторное сохранение продублирует их.
|
||||
setPendingState({});
|
||||
setItems(await getAttachments(listRef, itemId));
|
||||
};
|
||||
|
||||
return { items, setItems, pending, setPending, remove, uploadPending, error };
|
||||
}
|
||||
Reference in New Issue
Block a user