fix: changed script to crete cells from list to site
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# Заявка на прием (recruiting-hire)
|
||||
|
||||
**Назначение:** приём сотрудника на работу.
|
||||
**Категория:** Отдел рекрутинга
|
||||
**Списки SP:** «Прием на работу — данные» / «Прием на работу — согласование»
|
||||
**RequestTypeId (внешний API маршрутов):** 5
|
||||
|
||||
## Маршрут
|
||||
- Фазы: согласование + исполнение.
|
||||
- Исполнители подтягиваются из Execution-маршрута; если их нет — заявка финиширует
|
||||
на «Согласовано» (блок «Исполнение» не показывается).
|
||||
|
||||
## Особое поведение
|
||||
- **Маска телефона** (PhoneInput) на поле «Телефон».
|
||||
- **Разделитель тысяч** (MoneyInput) на окладах (испытательный срок / после).
|
||||
- **Условное поле «Взамен кого» (ReplacedEmployee)** — показывается только когда
|
||||
тип приёма = замена уволенного (см. `HIRE_TYPE_REPLACEMENT` в модели).
|
||||
- Секция **СИЗ** (рост, размеры одежды/обуви).
|
||||
- **Является источником для «Организация рабочего места»** — из просмотра этой
|
||||
заявки кнопка «Организация рабочего места на основе этой» открывает форму
|
||||
it-workplace-setup с префиллом (rpsource = id этой заявки).
|
||||
- Печать — общий механизм (без «Вложений»).
|
||||
|
||||
## Секции формы
|
||||
ApplicationSection · CandidateSection · PpeSection · ConditionsSection
|
||||
|
||||
## Точки входа
|
||||
- rptype: `recruiting-hire`
|
||||
- Режим согласующего (просмотр): есть (`RecruitingHireView`)
|
||||
- «Создать на основе»: эта заявка → источник для `it-workplace-setup`
|
||||
@@ -0,0 +1,30 @@
|
||||
# Заявка на подбор (recruiting-selection)
|
||||
|
||||
**Назначение:** заявка на подбор персонала / открытие вакансии.
|
||||
**Категория:** Отдел рекрутинга
|
||||
**Списки SP:** «Заявка на подбор — данные» (только данные — согласования нет)
|
||||
**RequestTypeId (внешний API маршрутов):** — (маршрут не используется)
|
||||
|
||||
## Маршрут
|
||||
- Фаз нет: отправка = запись в список данных, без согласования и исполнения.
|
||||
|
||||
## Особое поведение
|
||||
- **Самая большая форма (~42 поля).**
|
||||
- **Поля-люди:** `Responsible`, `ProposedMentor`, `DirectManager` — один человек;
|
||||
`HiringDecisionMaker` — несколько (мульти). Пишутся как `<Field>Id`
|
||||
(см. `submit.ts` → `resolveUserId`).
|
||||
- **Нет режима согласующего** (в `viewRegistry` не зарегистрирована) и **нет общего
|
||||
жизненного цикла** (черновик/статус/upsert) — это ранняя форма, простая отправка
|
||||
через `submitRecruitingSelection`.
|
||||
|
||||
## Секции формы
|
||||
См. `sections/` формы.
|
||||
|
||||
## Точки входа
|
||||
- rptype: `recruiting-selection`
|
||||
- Режим согласующего (просмотр): нет
|
||||
- «Создать на основе»: —
|
||||
|
||||
> Замечание: сделана до введения общего жизненного цикла и согласования. При
|
||||
> необходимости приводится к общему шаблону (`useRequestSubmit` + `SubmitActions`
|
||||
> + маршрут согласования), как в остальных формах.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Организация рабочего места (it-workplace-setup)
|
||||
|
||||
**Назначение:** подготовка рабочего места сотрудника (техника, ПО, доступы).
|
||||
**Категория:** IT
|
||||
**Списки SP:** «Организация рабочего места — данные» (+ lookup `SourceHire` →
|
||||
«Прием на работу — данные») / «Организация рабочего места — согласование»
|
||||
**RequestTypeId (внешний API маршрутов):** 7
|
||||
|
||||
## Маршрут
|
||||
- Фазы: согласование + исполнение.
|
||||
- **Шаг 1 согласования — непосредственный руководитель** (роль в маршруте).
|
||||
|
||||
## Особое поведение
|
||||
- **Префилл «на основе» заявки на прием** (открывается по `rpsource`): копируются
|
||||
ФИО, телефон, должность, отдел/участок, вид работ, местоположение, дата приёма
|
||||
(`loadPrefillFromHire`). Это снимок на момент создания; ссылка на источник
|
||||
сохраняется в колонке-lookup `SourceHire`.
|
||||
- **Поля «Необходимое ПО» и «Доступы» заполняет согласующий-руководитель** на своём
|
||||
этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до
|
||||
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
|
||||
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
|
||||
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
|
||||
- **Маска телефона** (PhoneInput).
|
||||
- Печать — общий механизм (без «Вложений»).
|
||||
|
||||
## Секции формы
|
||||
EmployeeSection · EquipmentSection
|
||||
|
||||
## Точки входа
|
||||
- rptype: `it-workplace-setup`
|
||||
- Режим согласующего (просмотр): есть (`WorkplaceSetupView`)
|
||||
- «Создать на основе»: создаётся из просмотра заявки `recruiting-hire`
|
||||
@@ -12,3 +12,33 @@
|
||||
justify-content: flex-end;
|
||||
margin-bottom: $sp-4;
|
||||
}
|
||||
|
||||
.reviewBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-3;
|
||||
margin-bottom: $sp-4;
|
||||
padding: $sp-3 $sp-4;
|
||||
border: 1px solid $gold-300;
|
||||
border-radius: $r-md;
|
||||
background: $gold-50;
|
||||
}
|
||||
|
||||
.reviewHint {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: $fs-sm;
|
||||
color: $navy-800;
|
||||
}
|
||||
|
||||
.savedNote {
|
||||
font-size: $fs-sm;
|
||||
color: $green-600;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.saveError {
|
||||
font-size: $fs-sm;
|
||||
color: $red-500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,16 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './WorkplaceSetupView.module.scss';
|
||||
import { Alert, FormCard, FormModeProvider, CollapseSection, FormField, AttachmentList, PrintButton } from '../../shared/ui';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormCard,
|
||||
FormModeProvider,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton
|
||||
} from '../../shared/ui';
|
||||
import {
|
||||
ApproverService,
|
||||
DecisionPanel,
|
||||
@@ -14,7 +23,7 @@ import {
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||
import { loadWorkplaceRequest } from './load';
|
||||
import { workplaceApprovalService, DATA_LIST_TITLE } from './submit';
|
||||
import { workplaceApprovalService, updateReviewFields, DATA_LIST_TITLE } from './submit';
|
||||
import { EmployeeSection } from './sections/EmployeeSection';
|
||||
import { EquipmentSection } from './sections/EquipmentSection';
|
||||
|
||||
@@ -23,15 +32,21 @@ interface IWorkplaceSetupViewProps {
|
||||
itemId: number;
|
||||
}
|
||||
|
||||
const NOOP_SET: SetField = () => undefined;
|
||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
// Заявка «орг. рабочего места» только для просмотра (режим согласующего).
|
||||
// Исключение: первый согласующий шаг (по маршруту — непосредственный руководитель)
|
||||
// на своём этапе может заполнить «Необходимое ПО» и «Доступы» (инициатор их не знает).
|
||||
const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemId }) => {
|
||||
const [data, setData] = useState<IFormData>(EMPTY_DATA);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [canEditFields, setCanEditFields] = useState<boolean>(false);
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
|
||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const approverService = useMemo(
|
||||
() => new ApproverService(workplaceApprovalService(), DATA_LIST_TITLE),
|
||||
@@ -85,6 +100,45 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
};
|
||||
}, [itemId]);
|
||||
|
||||
// Правка «ПО»/«Доступы» доступна, когда сейчас очередь ПЕРВОГО шага согласования
|
||||
// (по маршруту — непосредственного руководителя) и это текущий пользователь.
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
Promise.all([
|
||||
approval.getForRequest(itemId),
|
||||
approverService.getPendingRowForCurrentUser(itemId)
|
||||
])
|
||||
.then(([rows, pending]) => {
|
||||
if (!alive) return;
|
||||
const orders = rows.filter((r) => r.phase !== APPROVAL_PHASE.execution).map((r) => r.order);
|
||||
const firstOrder = orders.length > 0 ? Math.min(...orders) : undefined;
|
||||
setCanEditFields(
|
||||
!!pending && pending.phase !== APPROVAL_PHASE.execution && pending.order === firstOrder
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (alive) setCanEditFields(false);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [itemId, approval, approverService]);
|
||||
|
||||
const saveReviewFields = useCallback(
|
||||
() => updateReviewFields(itemId, { Software: data.Software, AccessRights: data.AccessRights }),
|
||||
[itemId, data.Software, data.AccessRights]
|
||||
);
|
||||
|
||||
const handleSave = async (): Promise<void> => {
|
||||
setSaveState('saving');
|
||||
try {
|
||||
await saveReviewFields();
|
||||
setSaveState('saved');
|
||||
} catch {
|
||||
setSaveState('error');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className={styles.state}>Загрузка заявки…</div>;
|
||||
}
|
||||
@@ -96,7 +150,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
);
|
||||
}
|
||||
|
||||
const sectionProps = { data, set: NOOP_SET, context };
|
||||
const sectionProps = { data, set, context };
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -104,6 +158,19 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
<PrintButton />
|
||||
</div>
|
||||
|
||||
{canEditFields && (
|
||||
<div className={`${styles.reviewBar} rp-no-print`}>
|
||||
<span className={styles.reviewHint}>
|
||||
Как согласующий вы можете заполнить «Необходимое ПО» и «Доступы».
|
||||
</span>
|
||||
<Button variant="secondary" onClick={handleSave} disabled={saveState === 'saving'}>
|
||||
Сохранить
|
||||
</Button>
|
||||
{saveState === 'saved' && <span className={styles.savedNote}>Сохранено</span>}
|
||||
{saveState === 'error' && <span className={styles.saveError}>Ошибка сохранения</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rp-print-root">
|
||||
<FormCard
|
||||
title="Организация рабочего места"
|
||||
@@ -112,7 +179,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
>
|
||||
<FormModeProvider readOnly>
|
||||
<EmployeeSection {...sectionProps} />
|
||||
<EquipmentSection {...sectionProps} />
|
||||
<EquipmentSection {...sectionProps} approverEditable={canEditFields} />
|
||||
</FormModeProvider>
|
||||
|
||||
<div className="rp-no-print">
|
||||
@@ -138,7 +205,11 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
||||
</div>
|
||||
|
||||
<div className="rp-no-print">
|
||||
<DecisionPanel service={approverService} requestId={itemId} />
|
||||
<DecisionPanel
|
||||
service={approverService}
|
||||
requestId={itemId}
|
||||
onBeforeDecide={canEditFields ? saveReviewFields : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,16 +2,28 @@ import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextArea, Toggle } from '../../../shared/ui';
|
||||
import { ISectionProps } from '../model';
|
||||
|
||||
export const EquipmentSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
<CollapseSection title="Оборудование и доступы" icon="monitor">
|
||||
<FormField label="Нужна ли техника">
|
||||
<Toggle checked={data.NeedsEquipment} onChange={(v) => set('NeedsEquipment', v)} onLabel="Да" />
|
||||
</FormField>
|
||||
<FormField label="Необходимое ПО" htmlFor="Software" full>
|
||||
<TextArea id="Software" value={data.Software} onChange={(v) => set('Software', v)} />
|
||||
</FormField>
|
||||
<FormField label="Доступы" htmlFor="AccessRights" full>
|
||||
<TextArea id="AccessRights" value={data.AccessRights} onChange={(v) => set('AccessRights', v)} />
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
interface IEquipmentSectionProps extends ISectionProps {
|
||||
/** Согласующий-руководитель на этапе согласования может править ПО и доступы,
|
||||
* даже когда остальная форма read-only. undefined — обычное поведение (контекст). */
|
||||
approverEditable?: boolean;
|
||||
}
|
||||
|
||||
export const EquipmentSection: React.FC<IEquipmentSectionProps> = ({ data, set, approverEditable }) => {
|
||||
// Явный readOnly=false перебивает read-only контекст просмотра; undefined —
|
||||
// берёт значение из FormMode (в форме подачи редактируемо, в просмотре — нет).
|
||||
const fieldReadOnly = approverEditable ? false : undefined;
|
||||
|
||||
return (
|
||||
<CollapseSection title="Оборудование и доступы" icon="monitor">
|
||||
<FormField label="Нужна ли техника">
|
||||
<Toggle checked={data.NeedsEquipment} onChange={(v) => set('NeedsEquipment', v)} onLabel="Да" />
|
||||
</FormField>
|
||||
<FormField label="Необходимое ПО" htmlFor="Software" full>
|
||||
<TextArea id="Software" value={data.Software} onChange={(v) => set('Software', v)} readOnly={fieldReadOnly} />
|
||||
</FormField>
|
||||
<FormField label="Доступы" htmlFor="AccessRights" full>
|
||||
<TextArea id="AccessRights" value={data.AccessRights} onChange={(v) => set('AccessRights', v)} readOnly={fieldReadOnly} />
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,20 @@ export function workplaceApprovalService(): ApprovalListService {
|
||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить поля, которые заполняет согласующий-руководитель на этапе согласования
|
||||
* (инициатор часто не знает нужное ПО и доступы). Пишет только эти два поля.
|
||||
*/
|
||||
export async function updateReviewFields(
|
||||
id: number,
|
||||
fields: { Software: string; AccessRights: string }
|
||||
): Promise<void> {
|
||||
await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update({
|
||||
Software: fields.Software === '' ? null : fields.Software,
|
||||
AccessRights: fields.AccessRights === '' ? null : fields.AccessRights,
|
||||
});
|
||||
}
|
||||
|
||||
/** Осмысленный заголовок элемента (системная колонка Title). */
|
||||
function buildTitle(data: IFormData): string {
|
||||
return ['Раб. место', data.FullName, data.Position].filter(Boolean).join(' — ');
|
||||
|
||||
@@ -11,11 +11,19 @@ interface IDecisionPanelProps {
|
||||
requestId: number;
|
||||
/** Уведомление после сохранения решения (например, обновить статус на экране). */
|
||||
onDecided?: (status: string) => void;
|
||||
/** Выполнить ПЕРЕД применением решения (напр. сохранить поля, заполненные
|
||||
* согласующим). Если бросит — решение не применяется, показывается ошибка. */
|
||||
onBeforeDecide?: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Панель решения согласующего: проверяет, его ли сейчас очередь, и если да —
|
||||
// показывает комментарий и кнопки Согласовать / Вернуть / Отклонить.
|
||||
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestId, onDecided }) => {
|
||||
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({
|
||||
service,
|
||||
requestId,
|
||||
onDecided,
|
||||
onBeforeDecide,
|
||||
}) => {
|
||||
const [checking, setChecking] = useState<boolean>(true);
|
||||
const [rowId, setRowId] = useState<number | undefined>(undefined);
|
||||
const [phase, setPhase] = useState<string>(APPROVAL_PHASE.approval);
|
||||
@@ -60,6 +68,10 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
try {
|
||||
// Сохраняем правки согласующего (если есть) ДО применения решения.
|
||||
if (onBeforeDecide) {
|
||||
await onBeforeDecide();
|
||||
}
|
||||
const status = await service.decide(requestId, rowId, decision, comment.trim());
|
||||
setResultStatus(status);
|
||||
setSubmitting(false);
|
||||
|
||||
Reference in New Issue
Block a user