Compare commits
3 Commits
637e51d7f9
...
da8c2c3e54
| Author | SHA1 | Date | |
|---|---|---|---|
| da8c2c3e54 | |||
| 0b4fbc4487 | |||
| 271758527e |
+1
-1
@@ -20,7 +20,7 @@
|
|||||||
| Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
|
| Удалять любых согласующих **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все остальные формы с маршрутом согласования (отпуск, увольнение, 1С и т. д.); в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `composeRemoveRowRule` |
|
||||||
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
| Добавлять и удалять исполнителей **при составлении** заявки | те же (`canEditRoute`) | email в коде | Все формы, где в маршруте из админки есть фаза исполнения; в поданной заявке — нет | `shared/approval/routeEditRights.ts` → `canEditExecutors` |
|
||||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
||||||
| Править **все поля** заявки на своём активном шаге согласования (без вложений) | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||||
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
||||||
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
|
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
|
||||||
| Править карту СКУД и сумму за обучение | Начальник отдела кадрового делопроизводства | должность шага: «начальник/руководитель» + «кадров» + «делопроизводств» | Обходной лист | `forms/Clearance/approverRights.ts` → `canEditHrRecordsFields` |
|
| Править карту СКУД и сумму за обучение | Начальник отдела кадрового делопроизводства | должность шага: «начальник/руководитель» + «кадров» + «делопроизводств» | Обходной лист | `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;
|
margin-bottom: $sp-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyAtt {
|
|
||||||
font-family: $font-sans;
|
|
||||||
font-size: 13.5px;
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Полоса «вы можете заполнить оклады» для начальника расчётной группы —
|
// Полоса «вы можете заполнить оклады» для начальника расчётной группы —
|
||||||
// см. approverRights.ts (тот же приём, что и в Clearance/ClearanceView).
|
// см. approverRights.ts (тот же приём, что и в Clearance/ClearanceView).
|
||||||
.reviewBar {
|
.reviewBar {
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import {
|
|||||||
FormCard,
|
FormCard,
|
||||||
InitiatorLine,
|
InitiatorLine,
|
||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
CollapseSection,
|
|
||||||
FormField,
|
|
||||||
AttachmentList,
|
|
||||||
Icon,
|
Icon,
|
||||||
PrintButton,
|
PrintButton,
|
||||||
FieldChangesProvider,
|
FieldChangesProvider,
|
||||||
@@ -39,7 +36,8 @@ import {
|
|||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
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 { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
@@ -49,7 +47,7 @@ import {
|
|||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadHireRequest } from './load';
|
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 { confirmDiscardFormChanges } from '../../shared/unsavedChanges';
|
||||||
import { hireApprovalService, updateApproverFields, updateAllHireFields, IApproverEditableFields } from './submit';
|
import { hireApprovalService, updateApproverFields, updateAllHireFields, IApproverEditableFields } from './submit';
|
||||||
import { submitIssues } from './validation';
|
import { submitIssues } from './validation';
|
||||||
@@ -81,14 +79,14 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
|||||||
const HIRE_DATE_REQUIRED_MESSAGE = 'укажите дату приёма (п. 2.2) — без неё заявку нельзя отметить выполненной.';
|
const HIRE_DATE_REQUIRED_MESSAGE = 'укажите дату приёма (п. 2.2) — без неё заявку нельзя отметить выполненной.';
|
||||||
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
||||||
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля заявки (кроме вложений).';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
|
||||||
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
||||||
// те же секции, что и форма подачи; редактирование выключено через
|
// те же секции, что и форма подачи; редактирование выключено через
|
||||||
// <FormModeProvider readOnly>, КРОМЕ окладов (4.2/4.3) и отдела/участка (2.4) —
|
// <FormModeProvider readOnly>, КРОМЕ окладов (4.2/4.3) и отдела/участка (2.4) —
|
||||||
// их на своём шаге правит начальник расчётной группы (см. approverRights.ts), —
|
// их на своём шаге правит начальник расчётной группы (см. approverRights.ts), —
|
||||||
// и даты приёма (2.2), которую проставляет активный исполнитель. Согласующий
|
// и даты приёма (2.2), которую проставляет активный исполнитель. Согласующий
|
||||||
// из fullEditRights.ts на своём шаге правит все поля (вложения — нет).
|
// из fullEditRights.ts на своём шаге правит все поля и вложения.
|
||||||
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||||
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
||||||
@@ -98,7 +96,6 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [payrollEditable, setPayrollEditable] = useState<boolean>(false);
|
const [payrollEditable, setPayrollEditable] = useState<boolean>(false);
|
||||||
@@ -137,6 +134,9 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
() => new ApproverService(hireApprovalService(listId), dataListRef),
|
() => new ApproverService(hireApprovalService(listId), dataListRef),
|
||||||
[listId, dataListRef]
|
[listId, dataListRef]
|
||||||
);
|
);
|
||||||
|
const attachments = useAttachmentEditor(dataListRef, itemId);
|
||||||
|
// Сеттер стабилен (useState) — безопасно класть в зависимости эффекта загрузки.
|
||||||
|
const { setItems: setAttachmentItems } = attachments;
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only), разбитая по фазам.
|
// Матрица маршрута из фактических строк заявки (read-only), разбитая по фазам.
|
||||||
//
|
//
|
||||||
@@ -207,6 +207,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
await updateAllHireFields(itemId, data, nextChanges, listId);
|
await updateAllHireFields(itemId, data, nextChanges, listId);
|
||||||
setApproverChanges(nextChanges);
|
setApproverChanges(nextChanges);
|
||||||
setSavedData(data);
|
setSavedData(data);
|
||||||
|
await attachments.uploadPending(prepareHireFiles);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fields: Partial<IApproverEditableFields> = {};
|
const fields: Partial<IApproverEditableFields> = {};
|
||||||
@@ -278,7 +279,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
setStatus(loaded.status);
|
setStatus(loaded.status);
|
||||||
setCode(loaded.code);
|
setCode(loaded.code);
|
||||||
notifyCode(loaded.code);
|
notifyCode(loaded.code);
|
||||||
setAttachments(files);
|
setAttachmentItems(files);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -292,7 +293,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [itemId, listId, notifyCode]);
|
}, [itemId, listId, notifyCode, setAttachmentItems]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||||
@@ -363,7 +364,6 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FieldChangesBanner changes={approverChanges} />
|
<FieldChangesBanner changes={approverChanges} />
|
||||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
|
||||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||||
<FormModeProvider readOnly={!fullEditable}>
|
<FormModeProvider readOnly={!fullEditable}>
|
||||||
<ApplicationSection {...sectionProps} />
|
<ApplicationSection {...sectionProps} />
|
||||||
@@ -375,27 +375,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
</FieldChangesProvider>
|
</FieldChangesProvider>
|
||||||
|
|
||||||
<div className="rp-no-print">
|
<div className="rp-no-print">
|
||||||
<CollapseSection title="Вложения" icon="upload">
|
<HireAttachments editor={attachments} editable={fullEditable} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import {
|
|||||||
FormCard,
|
FormCard,
|
||||||
InitiatorLine,
|
InitiatorLine,
|
||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
CollapseSection,
|
|
||||||
FormField,
|
|
||||||
AttachmentList,
|
|
||||||
PrintButton,
|
PrintButton,
|
||||||
FieldChangesProvider,
|
FieldChangesProvider,
|
||||||
FieldChangesBanner
|
FieldChangesBanner
|
||||||
@@ -36,7 +33,9 @@ import {
|
|||||||
APPROVAL_PHASE,
|
APPROVAL_PHASE,
|
||||||
ApproverDecision
|
ApproverDecision
|
||||||
} from '../../shared/approval';
|
} 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 { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import {
|
import {
|
||||||
@@ -71,12 +70,12 @@ interface IRecruitingSelectionViewProps {
|
|||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля заявки (кроме вложений).';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
|
||||||
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
|
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
|
||||||
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>,
|
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>,
|
||||||
// кроме согласующего из fullEditRights.ts на его активном шаге — он правит все поля
|
// кроме согласующего из fullEditRights.ts на его активном шаге — он правит все поля
|
||||||
// (вложения — нет).
|
// и вложения.
|
||||||
const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||||
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
||||||
@@ -86,7 +85,6 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Право удалять согласующих из уже поданного маршрута — см.
|
// Право удалять согласующих из уже поданного маршрута — см.
|
||||||
@@ -115,6 +113,9 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
// архивной партиции, Title текущей — иначе (см. ApproverService.dataListRef).
|
// архивной партиции, Title текущей — иначе (см. ApproverService.dataListRef).
|
||||||
const dataListRef = listId || dataListTitle(FORM_ID);
|
const dataListRef = listId || dataListTitle(FORM_ID);
|
||||||
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
const approverService = useMemo(() => new ApproverService(approval, dataListRef), [approval, dataListRef]);
|
||||||
|
const attachments = useAttachmentEditor(dataListRef, itemId);
|
||||||
|
// Сеттер стабилен (useState) — безопасно класть в зависимости эффекта загрузки.
|
||||||
|
const { setItems: setAttachmentItems } = attachments;
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only), разбитая по
|
// Матрица маршрута из фактических строк заявки (read-only), разбитая по
|
||||||
// фазам. decisionSeq в reloadKey: после решения строки на сервере меняются —
|
// фазам. decisionSeq в reloadKey: после решения строки на сервере меняются —
|
||||||
@@ -169,6 +170,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
await updateAllSelectionFields(itemId, data, nextChanges, listId);
|
await updateAllSelectionFields(itemId, data, nextChanges, listId);
|
||||||
setApproverChanges(nextChanges);
|
setApproverChanges(nextChanges);
|
||||||
setSavedData(data);
|
setSavedData(data);
|
||||||
|
await attachments.uploadPending();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveFields = async (): Promise<void> => {
|
const handleSaveFields = async (): Promise<void> => {
|
||||||
@@ -266,7 +268,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
setStatus(loaded.status);
|
setStatus(loaded.status);
|
||||||
setCode(loaded.code);
|
setCode(loaded.code);
|
||||||
notifyCode(loaded.code);
|
notifyCode(loaded.code);
|
||||||
setAttachments(files);
|
setAttachmentItems(files);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -280,7 +282,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [itemId, listId, notifyCode]);
|
}, [itemId, listId, notifyCode, setAttachmentItems]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||||
@@ -327,7 +329,6 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FieldChangesBanner changes={approverChanges} />
|
<FieldChangesBanner changes={approverChanges} />
|
||||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
|
||||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||||
<FormModeProvider readOnly={!fullEditable}>
|
<FormModeProvider readOnly={!fullEditable}>
|
||||||
<ApplicationSection {...sectionProps} />
|
<ApplicationSection {...sectionProps} />
|
||||||
@@ -347,11 +348,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
</FieldChangesProvider>
|
</FieldChangesProvider>
|
||||||
|
|
||||||
<div className="rp-no-print">
|
<div className="rp-no-print">
|
||||||
<CollapseSection title="Вложения" icon="upload">
|
<SelectionAttachments editor={attachments} editable={fullEditable} />
|
||||||
<FormField label="Файлы" full>
|
|
||||||
<AttachmentList items={attachments} />
|
|
||||||
</FormField>
|
|
||||||
</CollapseSection>
|
|
||||||
</div>
|
</div>
|
||||||
</FormCard>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -49,8 +49,8 @@ import {
|
|||||||
} from '../../shared/dictionary';
|
} from '../../shared/dictionary';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
import { requestLinkUrl } from '../../shared/routing';
|
import { requestLinkUrl } from '../../shared/routing';
|
||||||
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
|
||||||
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
import { resolveLzkProjectId } from '../../shared/dataverse/lzkPosition.service';
|
||||||
|
import { EXTERNAL_SOURCE } from '../../shared/config';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { todayIsoDate } from '../../shared/dateUtils';
|
import { todayIsoDate } from '../../shared/dateUtils';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
@@ -63,6 +63,7 @@ import {
|
|||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit';
|
import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit';
|
||||||
import { loadTmcTransferRequest } from './load';
|
import { loadTmcTransferRequest } from './load';
|
||||||
|
import { writeSourceLink } from './writeSourceLink';
|
||||||
import { HeaderSection } from './sections/HeaderSection';
|
import { HeaderSection } from './sections/HeaderSection';
|
||||||
import { ItemsSection } from './sections/ItemsSection';
|
import { ItemsSection } from './sections/ItemsSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
@@ -197,9 +198,10 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
// Dataverse хранит ID позиции ЛЗК, у которой есть родительская запись LZK
|
||||||
// с проектом (см. shared/dataverse/lzkPosition.service). Необязательное
|
// с проектом (см. shared/dataverse/lzkPosition.service). Необязательное
|
||||||
// удобство — на ошибке просто не предзаполняем поле, пользователь выберет
|
// удобство — на ошибке просто не предзаполняем поле, пользователь выберет
|
||||||
// проект вручную.
|
// проект вручную. Для ссылок из КПС не ищем: там positionId — позиция акта
|
||||||
|
// дефектов, а не строка Dataverse (запрос вернул бы только 404).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editId || !positionId) {
|
if (editId || !positionId || sourceSystem === EXTERNAL_SOURCE.kps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -231,7 +233,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [editId, positionId, context]);
|
}, [editId, positionId, sourceSystem, context]);
|
||||||
|
|
||||||
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
|
// Выбор другого заявителя подтягивает его ФИО (для печати) и организации (как
|
||||||
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
// в Vacation.handleEmployeeChange, но источник тот же Employee/ByEmail — здесь
|
||||||
@@ -411,18 +413,15 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID),
|
||||||
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
|
approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
// Обратная связь во внешнюю систему: пишем ссылку на эту заявку в
|
// Обратная связь во внешнюю систему (КПС или Dataverse — см. writeSourceLink):
|
||||||
// строку-источник Dataverse (только если заявка открыта по внешней ссылке).
|
// отдаём ссылку на эту заявку, если её открыли по внешней ссылке.
|
||||||
// Не критично для самой заявки — она уже сохранена и отправлена, поэтому
|
// Не критично для самой заявки — она уже сохранена и отправлена, поэтому
|
||||||
// сбой записи не роняем, а только логируем (см. флаг ниже для UX-решения).
|
// сбой записи не роняем, а только логируем.
|
||||||
if (data.SourcePositionId) {
|
try {
|
||||||
const link = requestLinkUrl(FORM_ID, id, writeListId);
|
await writeSourceLink(context, data, requestLinkUrl(FORM_ID, id, writeListId));
|
||||||
try {
|
} catch (e) {
|
||||||
await writeDemandLinePortalLink(context, data.SourcePositionId, link);
|
// eslint-disable-next-line no-console
|
||||||
} catch (e) {
|
console.warn(`Не удалось передать ссылку на заявку во внешнюю систему (${data.SourceSystem || 'dataverse'}):`, e);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('Не удалось записать ссылку в строку-источник (Dataverse):', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { EXTERNAL_SOURCE } from '../../shared/config';
|
||||||
|
import { writeDemandLinePortalLink } from '../../shared/dataverse/demandLine.service';
|
||||||
|
import { writeDefectActPartTmcLink } from '../../shared/inventory/defectActPart.service';
|
||||||
|
import { IFormData } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответить внешней системе, из которой открыли форму, ссылкой на заявку.
|
||||||
|
* Куда — по SourceSystem (параметр source внешней ссылки):
|
||||||
|
* - 'kps' → Inventory API, позиция акта дефектов (SourcePositionId = partId);
|
||||||
|
* - иначе → Dataverse, «Строка заявки» (SourcePositionId = id строки).
|
||||||
|
* Заявка без SourcePositionId (заведена вручную) — ничего не делаем.
|
||||||
|
*/
|
||||||
|
export async function writeSourceLink(context: WebPartContext, data: IFormData, link: string): Promise<void> {
|
||||||
|
if (!data.SourcePositionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.SourceSystem === EXTERNAL_SOURCE.kps) {
|
||||||
|
await writeDefectActPartTmcLink(context, data.SourcePositionId, link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await writeDemandLinePortalLink(context, data.SourcePositionId, link);
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@
|
|||||||
* нет, как и в routeEditRights.ts. ВРЕМЕННО.
|
* нет, как и в routeEditRights.ts. ВРЕМЕННО.
|
||||||
*
|
*
|
||||||
* Подключено только в «Заявке на подбор» и «Заявке на прием» (Реферальная
|
* Подключено только в «Заявке на подбор» и «Заявке на прием» (Реферальная
|
||||||
* рекомендация — нет, по решению Тимура 2026-09-24). Вложения этим правом не
|
* рекомендация — нет, по решению Тимура 2026-09-24). С 2026-10-06 право
|
||||||
* открываются — только поля.
|
* открывает и вложения: добавлять и удалять файлы (см. shared/attachments/
|
||||||
|
* useAttachmentEditor).
|
||||||
*/
|
*/
|
||||||
const FULL_FIELD_EDITOR_EMAILS: ReadonlySet<string> = new Set([
|
const FULL_FIELD_EDITOR_EMAILS: ReadonlySet<string> = new Set([
|
||||||
'a.bulturukova@tnsu.kz' // Алтынай Бултурукова
|
'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 };
|
||||||
|
}
|
||||||
@@ -115,6 +115,24 @@ export const DICTIONARY_API_BASE_URL = 'https://api.tnsu.kz';
|
|||||||
*/
|
*/
|
||||||
export const SKUD_EVENTS_PATH = '/Hikvision/Events/CurrentUser';
|
export const SKUD_EVENTS_PATH = '/Hikvision/Events/CurrentUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значения параметра `source` внешней ссылки (см. shared/routing, sourceSystem) —
|
||||||
|
* из какой системы открыли форму и, значит, куда отвечать ссылкой на заявку.
|
||||||
|
* Без source (или с неизвестным значением) — ссылка пришла из Dataverse.
|
||||||
|
*/
|
||||||
|
export const EXTERNAL_SOURCE = {
|
||||||
|
/** КПС: positionId — id позиции акта дефектов, ответ — в Inventory API. */
|
||||||
|
kps: 'kps'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory API (КПС) — приём ссылки на заявку «СЗ на перемещение ТМЦ» для
|
||||||
|
* позиции акта дефектов: POST {DICTIONARY_API_BASE_URL}<путь> с телом { url }.
|
||||||
|
* {partId} = positionId из внешней ссылки с source=kps. Тот же gateway
|
||||||
|
* api.tnsu.kz — авторизация та же, что у справочников (APPROVAL_API_CLIENT_ID).
|
||||||
|
*/
|
||||||
|
export const INVENTORY_TMC_TRANSFER_PATH = '/inventory/api/defect-acts/parts/{partId}/tmc-transfer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Производственный календарь (праздничные выходные) — SP-список на корневом
|
* Производственный календарь (праздничные выходные) — SP-список на корневом
|
||||||
* сайте, где живёт главная страница портала (portal-main-page читает его же).
|
* сайте, где живёт главная страница портала (portal-main-page читает его же).
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL, INVENTORY_TMC_TRANSFER_PATH } from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обратная связь в КПС: передать ссылку на заявку портала в позицию акта
|
||||||
|
* дефектов, из которой открыли форму «СЗ на перемещение ТМЦ» (source=kps).
|
||||||
|
*
|
||||||
|
* partId — positionId из внешней ссылки (id позиции акта в КПС, НЕ строка
|
||||||
|
* Dataverse). Ответ сервиса ({ partId, defectActId, tmcTransferUrl }) нам не
|
||||||
|
* нужен — достаточно успешного статуса.
|
||||||
|
*/
|
||||||
|
export async function writeDefectActPartTmcLink(
|
||||||
|
context: WebPartContext,
|
||||||
|
partId: string,
|
||||||
|
portalRequestUrl: string
|
||||||
|
): Promise<void> {
|
||||||
|
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||||
|
const path = INVENTORY_TMC_TRANSFER_PATH.replace('{partId}', encodeURIComponent(partId));
|
||||||
|
|
||||||
|
const response: HttpClientResponse = await client.post(`${DICTIONARY_API_BASE_URL}${path}`, AadHttpClient.configurations.v1, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url: portalRequestUrl })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errBody = '';
|
||||||
|
try {
|
||||||
|
errBody = await response.text();
|
||||||
|
} catch {
|
||||||
|
// тело ошибки прочитать не удалось — оставляем пустым
|
||||||
|
}
|
||||||
|
throw new Error(`Inventory POST ${path}: ${response.status} — ${errBody}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user