feat: раздел «Мебель» в организации рабочего места
Мультивыбор (стол, кресло, тумбочка, канцелярия, другое) с полем своего значения при «Другое». Необязательный; руководитель на своём шаге правит его наравне с ПО и доступами. Хранится в WorkplaceFurnitureJson/WorkplaceFurnitureOther и уходит в маршрут (fieldValues), чтобы шаг исполнения «по мебели» мог назначаться условием в админке. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||||
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
||||||
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
||||||
| Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) |
|
| Заполнять «Необходимое ПО», «Доступы» и «Мебель» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) |
|
||||||
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `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` |
|
||||||
|
|||||||
@@ -23,16 +23,21 @@
|
|||||||
- **Полное право правки** (`shared/approval/fullEditRights.ts`, сейчас Ербол
|
- **Полное право правки** (`shared/approval/fullEditRights.ts`, сейчас Ербол
|
||||||
Шаймерденов): все поля и вложения, если он есть в маршруте и заявка «На
|
Шаймерденов): все поля и вложения, если он есть в маршруте и заявка «На
|
||||||
согласовании»/«Выполнение» (`useApproverRights.ts`).
|
согласовании»/«Выполнение» (`useApproverRights.ts`).
|
||||||
- **Поля «Необходимое ПО» и «Доступы» заполняет согласующий-руководитель** на своём
|
- **Поля «Необходимое ПО», «Доступы» и «Мебель» заполняет согласующий-руководитель**
|
||||||
этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до
|
на своём этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до
|
||||||
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
|
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
|
||||||
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
|
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
|
||||||
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
|
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
|
||||||
|
- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле
|
||||||
|
своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и
|
||||||
|
`WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как
|
||||||
|
`WorkplaceFurnitureJson` (пункты через запятую, `routeFields.ts`) — на условие
|
||||||
|
шага исполнения «исполнитель по мебели».
|
||||||
- **Маска телефона** (PhoneInput).
|
- **Маска телефона** (PhoneInput).
|
||||||
- Печать — общий механизм (без «Вложений»).
|
- Печать — общий механизм (без «Вложений»).
|
||||||
|
|
||||||
## Секции формы
|
## Секции формы
|
||||||
HireRequestPicker (только в редактируемой форме) · EmployeeSection · EquipmentSection
|
HireRequestPicker (только в редактируемой форме) · EmployeeSection · EquipmentSection · FurnitureSection
|
||||||
|
|
||||||
## Точки входа
|
## Точки входа
|
||||||
- rptype: `it-workplace-setup`
|
- rptype: `it-workplace-setup`
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import { loadWorkplaceRequest } from './load';
|
|||||||
import { hasRequiredFields, REQUIRED_FIELDS_MESSAGE } from './validation';
|
import { hasRequiredFields, REQUIRED_FIELDS_MESSAGE } from './validation';
|
||||||
import { EmployeeSection } from './sections/EmployeeSection';
|
import { EmployeeSection } from './sections/EmployeeSection';
|
||||||
import { EquipmentSection } from './sections/EquipmentSection';
|
import { EquipmentSection } from './sections/EquipmentSection';
|
||||||
|
import { FurnitureSection } from './sections/FurnitureSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
|
|
||||||
const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
||||||
@@ -392,6 +393,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
<FormModeProvider readOnly={readOnly}>
|
<FormModeProvider readOnly={readOnly}>
|
||||||
<EmployeeSection {...sectionProps} />
|
<EmployeeSection {...sectionProps} />
|
||||||
<EquipmentSection {...sectionProps} />
|
<EquipmentSection {...sectionProps} />
|
||||||
|
<FurnitureSection {...sectionProps} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
</FieldChangesProvider>
|
</FieldChangesProvider>
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { hasRequiredFields, REQUIRED_FIELDS_MESSAGE } from './validation';
|
|||||||
import { useApproverRights } from './useApproverRights';
|
import { useApproverRights } from './useApproverRights';
|
||||||
import { EmployeeSection } from './sections/EmployeeSection';
|
import { EmployeeSection } from './sections/EmployeeSection';
|
||||||
import { EquipmentSection } from './sections/EquipmentSection';
|
import { EquipmentSection } from './sections/EquipmentSection';
|
||||||
|
import { FurnitureSection } from './sections/FurnitureSection';
|
||||||
import { useStableCallback } from '../../shared/useStableCallback';
|
import { useStableCallback } from '../../shared/useStableCallback';
|
||||||
|
|
||||||
interface IWorkplaceSetupViewProps {
|
interface IWorkplaceSetupViewProps {
|
||||||
@@ -64,7 +65,7 @@ interface IWorkplaceSetupViewProps {
|
|||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО» и «Доступы».';
|
const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО», «Доступы» и «Мебель».';
|
||||||
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.';
|
||||||
|
|
||||||
// Заявка «орг. рабочего места» только для просмотра (режим согласующего).
|
// Заявка «орг. рабочего места» только для просмотра (режим согласующего).
|
||||||
@@ -183,8 +184,8 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
await attachments.uploadPending();
|
await attachments.uploadPending();
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveReviewFields = (): Promise<void> =>
|
// updateReviewFields пишет только поля руководителя (ReviewFields), остальное из data игнорирует.
|
||||||
updateReviewFields(itemId, { Software: data.Software, AccessRights: data.AccessRights }, listId);
|
const saveReviewFields = (): Promise<void> => updateReviewFields(itemId, data, listId);
|
||||||
|
|
||||||
// Полное право шире: оно включает и «ПО»/«Доступы».
|
// Полное право шире: оно включает и «ПО»/«Доступы».
|
||||||
const saveFields = (): Promise<void> => (canEditAll ? saveAllFields() : saveReviewFields());
|
const saveFields = (): Promise<void> => (canEditAll ? saveAllFields() : saveReviewFields());
|
||||||
@@ -283,6 +284,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
<FormModeProvider readOnly={!canEditAll}>
|
<FormModeProvider readOnly={!canEditAll}>
|
||||||
<EmployeeSection {...sectionProps} />
|
<EmployeeSection {...sectionProps} />
|
||||||
<EquipmentSection {...sectionProps} approverEditable={canEditReviewFields} />
|
<EquipmentSection {...sectionProps} approverEditable={canEditReviewFields} />
|
||||||
|
<FurnitureSection {...sectionProps} approverEditable={canEditReviewFields} />
|
||||||
</FormModeProvider>
|
</FormModeProvider>
|
||||||
</FieldChangesProvider>
|
</FieldChangesProvider>
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,25 @@ import { dataListTitle } from '../../shared/listTitles';
|
|||||||
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
import { spDateOnlyToIso } from '../../shared/dateUtils';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, EMPTY_DATA } from './model';
|
import {
|
||||||
|
COMPANIES_JSON_FIELD,
|
||||||
|
FURNITURE_JSON_FIELD,
|
||||||
|
FURNITURE_OTHER_FIELD,
|
||||||
|
FORM_ID,
|
||||||
|
IFormData,
|
||||||
|
EMPTY_DATA
|
||||||
|
} from './model';
|
||||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON выбранных компаний → массив. Пусто — заявка до мультивыбора (тогда
|
* JSON-колонка с массивом → массив. Пусто — колонка не заполнена (заявка создана
|
||||||
* организации есть только текстом в Organization) или битый JSON.
|
* до появления поля: компании до мультивыбора, мебель до 2026-10-29) или битый JSON.
|
||||||
*/
|
*/
|
||||||
function parseCompanies(raw: unknown): ICompany[] {
|
function parseJsonArray<T>(raw: unknown): T[] {
|
||||||
if (typeof raw !== 'string' || raw.trim() === '') return [];
|
if (typeof raw !== 'string' || raw.trim() === '') return [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
return Array.isArray(parsed) ? (parsed as ICompany[]) : [];
|
return Array.isArray(parsed) ? (parsed as T[]) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -50,6 +57,8 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro
|
|||||||
...SCALAR_FIELDS,
|
...SCALAR_FIELDS,
|
||||||
'NeedsEquipment',
|
'NeedsEquipment',
|
||||||
COMPANIES_JSON_FIELD,
|
COMPANIES_JSON_FIELD,
|
||||||
|
FURNITURE_JSON_FIELD,
|
||||||
|
FURNITURE_OTHER_FIELD,
|
||||||
APPROVER_CHANGES_FIELD,
|
APPROVER_CHANGES_FIELD,
|
||||||
'RequestStatus',
|
'RequestStatus',
|
||||||
'RequestUniqueCode'
|
'RequestUniqueCode'
|
||||||
@@ -71,7 +80,9 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro
|
|||||||
});
|
});
|
||||||
|
|
||||||
data.NeedsEquipment = item.NeedsEquipment === true;
|
data.NeedsEquipment = item.NeedsEquipment === true;
|
||||||
data.Companies = parseCompanies(item[COMPANIES_JSON_FIELD]);
|
data.Companies = parseJsonArray<ICompany>(item[COMPANIES_JSON_FIELD]);
|
||||||
|
data.Furniture = parseJsonArray<string>(item[FURNITURE_JSON_FIELD]);
|
||||||
|
data.FurnitureOther = (item[FURNITURE_OTHER_FIELD] as string) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { RequestTypeId } from '../../models/types';
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
import { ISelectOption } from '@tnsu/ui-kit';
|
||||||
import { ICompany } from '../../shared/dictionary';
|
import { ICompany } from '../../shared/dictionary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +15,22 @@ export const FORM_ID: RequestTypeId = 'it-workplace-setup';
|
|||||||
*/
|
*/
|
||||||
export const COMPANIES_JSON_FIELD = 'WorkplaceCompaniesJson';
|
export const COMPANIES_JSON_FIELD = 'WorkplaceCompaniesJson';
|
||||||
|
|
||||||
|
/** Note-колонка с IFormData.Furniture в JSON. Префикс Workplace — по той же причине, что у компаний. */
|
||||||
|
export const FURNITURE_JSON_FIELD = 'WorkplaceFurnitureJson';
|
||||||
|
/** Text-колонка с IFormData.FurnitureOther. */
|
||||||
|
export const FURNITURE_OTHER_FIELD = 'WorkplaceFurnitureOther';
|
||||||
|
|
||||||
|
/** Значение-триггер «Другое» в «Мебели»: при нём показываем поле ручного ввода. */
|
||||||
|
export const FURNITURE_OTHER_VALUE = 'Другое';
|
||||||
|
|
||||||
|
export const FURNITURE_OPTIONS: ISelectOption[] = [
|
||||||
|
{ value: 'Стол', label: 'Стол' },
|
||||||
|
{ value: 'Кресло', label: 'Кресло' },
|
||||||
|
{ value: 'Тумбочка', label: 'Тумбочка' },
|
||||||
|
{ value: 'Канцелярские принадлежности', label: 'Канцелярские принадлежности' },
|
||||||
|
{ value: FURNITURE_OTHER_VALUE, label: FURNITURE_OTHER_VALUE }
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка
|
* Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка
|
||||||
* «Организация рабочего места — данные».
|
* «Организация рабочего места — данные».
|
||||||
@@ -57,6 +74,11 @@ export interface IFormData {
|
|||||||
NeedsEquipment: boolean;
|
NeedsEquipment: boolean;
|
||||||
Software: string;
|
Software: string;
|
||||||
AccessRights: string;
|
AccessRights: string;
|
||||||
|
// Мебель (необязательно; заполняется вручную)
|
||||||
|
/** Выбранные пункты из FURNITURE_OPTIONS. В SP — JSON в колонке WorkplaceFurnitureJson. */
|
||||||
|
Furniture: string[];
|
||||||
|
/** Своё значение — только если выбрано «Другое», иначе ''. В SP — колонка WorkplaceFurnitureOther. */
|
||||||
|
FurnitureOther: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,5 +118,7 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
Organization: '',
|
Organization: '',
|
||||||
NeedsEquipment: false,
|
NeedsEquipment: false,
|
||||||
Software: '',
|
Software: '',
|
||||||
AccessRights: ''
|
AccessRights: '',
|
||||||
|
Furniture: [],
|
||||||
|
FurnitureOther: ''
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
{ "internalName": "NeedsEquipment", "title": "Нужна ли техника", "type": "Boolean" },
|
{ "internalName": "NeedsEquipment", "title": "Нужна ли техника", "type": "Boolean" },
|
||||||
{ "internalName": "Software", "title": "Необходимое ПО", "type": "Note" },
|
{ "internalName": "Software", "title": "Необходимое ПО", "type": "Note" },
|
||||||
{ "internalName": "AccessRights", "title": "Доступы", "type": "Note" },
|
{ "internalName": "AccessRights", "title": "Доступы", "type": "Note" },
|
||||||
|
{ "internalName": "WorkplaceFurnitureJson", "title": "Мебель (JSON)", "type": "Note" },
|
||||||
|
{ "internalName": "WorkplaceFurnitureOther", "title": "Мебель (другое)", "type": "Text" },
|
||||||
{ "internalName": "WorkplaceApproverChangesJson", "title": "Изменения согласующего — рабочее место (JSON)", "type": "Note" },
|
{ "internalName": "WorkplaceApproverChangesJson", "title": "Изменения согласующего — рабочее место (JSON)", "type": "Note" },
|
||||||
|
|
||||||
{ "internalName": "SourceHire", "title": "Источник (заявка на прием)", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" }
|
{ "internalName": "SourceHire", "title": "Источник (заявка на прием)", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IResolveFieldValue } from '../../shared/approval';
|
import { IResolveFieldValue } from '../../shared/approval';
|
||||||
import { COMPANIES_JSON_FIELD, IFormData } from './model';
|
import { COMPANIES_JSON_FIELD, FURNITURE_JSON_FIELD, IFormData } from './model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Значения полей заявки для условных шагов маршрута (fieldValues при resolve).
|
* Значения полей заявки для условных шагов маршрута (fieldValues при resolve).
|
||||||
@@ -11,12 +11,18 @@ import { COMPANIES_JSON_FIELD, IFormData } from './model';
|
|||||||
* строку (массив → 400), а «содержит» ищет подстроку. Тот же формат, что в
|
* строку (массив → 400), а «содержит» ищет подстроку. Тот же формат, что в
|
||||||
* заявках на приём и подбор (см. RecruitingHireForm).
|
* заявках на приём и подбор (см. RecruitingHireForm).
|
||||||
*
|
*
|
||||||
* Маршрут планируется при подаче, поэтому условие по Software сработает,
|
* Мебель — «WorkplaceFurnitureJson содержит Стол» и т.п. (исполнитель по мебели):
|
||||||
* только если «Необходимое ПО» указал сам инициатор (руководитель заполняет
|
* пункты одной строкой через запятую — «содержит» ищет подстроку, разделитель
|
||||||
* его позже, на своём шаге).
|
* ему безразличен, а в диагностике админки так читается легче. Сортируем, чтобы
|
||||||
|
* порядок кликов не менял строку и не перепланировал матрицу впустую.
|
||||||
|
*
|
||||||
|
* Маршрут планируется при подаче, поэтому условия по Software и мебели при
|
||||||
|
* подаче сработают, только если их указал сам инициатор. Если их потом
|
||||||
|
* заполнит руководитель, недостающих исполнителей добавляет просмотр заявки
|
||||||
|
* при сохранении правок.
|
||||||
*/
|
*/
|
||||||
export function routeFieldValues(
|
export function routeFieldValues(
|
||||||
data: Pick<IFormData, 'Companies'>,
|
data: Pick<IFormData, 'Companies' | 'Furniture'>,
|
||||||
software: string
|
software: string
|
||||||
): IResolveFieldValue[] {
|
): IResolveFieldValue[] {
|
||||||
const fieldValues: IResolveFieldValue[] = [];
|
const fieldValues: IResolveFieldValue[] = [];
|
||||||
@@ -29,5 +35,9 @@ export function routeFieldValues(
|
|||||||
if (softwareText) {
|
if (softwareText) {
|
||||||
fieldValues.push({ fieldName: 'Software', value: softwareText });
|
fieldValues.push({ fieldName: 'Software', value: softwareText });
|
||||||
}
|
}
|
||||||
|
const furnitureText = data.Furniture.slice().sort().join(', ');
|
||||||
|
if (furnitureText) {
|
||||||
|
fieldValues.push({ fieldName: FURNITURE_JSON_FIELD, value: furnitureText });
|
||||||
|
}
|
||||||
return fieldValues;
|
return fieldValues;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { CollapseSection, FormField, FormModeProvider, MultiChoiceField, TextInput } from '../../../shared/ui';
|
||||||
|
import { FURNITURE_OPTIONS, FURNITURE_OTHER_VALUE, ISectionProps } from '../model';
|
||||||
|
|
||||||
|
interface IFurnitureSectionProps extends ISectionProps {
|
||||||
|
/** Согласующий-руководитель на своём шаге может отметить мебель, даже когда
|
||||||
|
* остальная форма read-only (как ПО и доступы в EquipmentSection). */
|
||||||
|
approverEditable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FurnitureSection: React.FC<IFurnitureSectionProps> = ({ data, set, approverEditable }) => {
|
||||||
|
const hasOther = data.Furniture.indexOf(FURNITURE_OTHER_VALUE) !== -1;
|
||||||
|
|
||||||
|
const handleFurnitureChange = (values: string[]): void => {
|
||||||
|
set('Furniture', values);
|
||||||
|
// Сняли «Другое» — своё значение больше не относится к заявке, не храним его.
|
||||||
|
if (values.indexOf(FURNITURE_OTHER_VALUE) === -1) set('FurnitureOther', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = (
|
||||||
|
<>
|
||||||
|
<FormField label="Мебель" fieldKey="Furniture" full>
|
||||||
|
<MultiChoiceField
|
||||||
|
value={data.Furniture}
|
||||||
|
onChange={handleFurnitureChange}
|
||||||
|
options={FURNITURE_OPTIONS}
|
||||||
|
placeholder="Выберите, что нужно"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{hasOther && (
|
||||||
|
<FormField label="Мебель (другое)" htmlFor="FurnitureOther" full>
|
||||||
|
<TextInput id="FurnitureOther" value={data.FurnitureOther} onChange={(v) => set('FurnitureOther', v)} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapseSection title="Мебель" icon="building">
|
||||||
|
{/* MultiChoiceField берёт режим только из контекста (своего readOnly у него нет),
|
||||||
|
поэтому право руководителя даём вложенным провайдером, перекрывающим внешний. */}
|
||||||
|
{approverEditable ? <FormModeProvider readOnly={false}>{fields}</FormModeProvider> : fields}
|
||||||
|
</CollapseSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval';
|
||||||
import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList';
|
import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList';
|
||||||
import { IFieldChange, serializeFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
import { IFieldChange, serializeFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||||
import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, organizationText } from './model';
|
import {
|
||||||
|
COMPANIES_JSON_FIELD,
|
||||||
|
FURNITURE_JSON_FIELD,
|
||||||
|
FURNITURE_OTHER_FIELD,
|
||||||
|
FORM_ID,
|
||||||
|
IFormData,
|
||||||
|
organizationText
|
||||||
|
} from './model';
|
||||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,22 +20,30 @@ export function workplaceApprovalService(listId?: string): ApprovalListService {
|
|||||||
return approvalServiceFor(FORM_ID, listId);
|
return approvalServiceFor(FORM_ID, listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Поля, которые заполняет согласующий-руководитель на своём шаге (см. updateReviewFields). */
|
||||||
|
export type ReviewFields = Pick<IFormData, 'Software' | 'AccessRights' | 'Furniture' | 'FurnitureOther'>;
|
||||||
|
|
||||||
|
/** Колонки SP «Мебели»: массив — JSON-ом, пустые значения — null. */
|
||||||
|
function furnitureFields(data: Pick<IFormData, 'Furniture' | 'FurnitureOther'>): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
[FURNITURE_JSON_FIELD]: data.Furniture.length > 0 ? JSON.stringify(data.Furniture) : null,
|
||||||
|
[FURNITURE_OTHER_FIELD]: data.FurnitureOther === '' ? null : data.FurnitureOther
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить поля, которые заполняет согласующий-руководитель на этапе согласования
|
* Обновить поля, которые заполняет согласующий-руководитель на этапе согласования
|
||||||
* (инициатор часто не знает нужное ПО и доступы). Пишет только эти два поля.
|
* (инициатор часто не знает нужное ПО, доступы и мебель). Пишет только эти поля.
|
||||||
*
|
*
|
||||||
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest): элемент уже
|
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest): элемент уже
|
||||||
* существует, поэтому читаем/пишем напрямую по url его списка, если он задан.
|
* существует, поэтому читаем/пишем напрямую по url его списка, если он задан.
|
||||||
*/
|
*/
|
||||||
export async function updateReviewFields(
|
export async function updateReviewFields(id: number, fields: ReviewFields, listId?: string): Promise<void> {
|
||||||
id: number,
|
|
||||||
fields: { Software: string; AccessRights: string },
|
|
||||||
listId?: string
|
|
||||||
): Promise<void> {
|
|
||||||
const list = resolveDataList(FORM_ID, listId);
|
const list = resolveDataList(FORM_ID, listId);
|
||||||
await list.items.getById(id).update({
|
await list.items.getById(id).update({
|
||||||
Software: fields.Software === '' ? null : fields.Software,
|
Software: fields.Software === '' ? null : fields.Software,
|
||||||
AccessRights: fields.AccessRights === '' ? null : fields.AccessRights,
|
AccessRights: fields.AccessRights === '' ? null : fields.AccessRights,
|
||||||
|
...furnitureFields(fields)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +78,14 @@ function buildContentFields(data: IFormData): Record<string, unknown> {
|
|||||||
item[COMPANIES_JSON_FIELD] = data.Companies.length > 0 ? JSON.stringify(data.Companies) : null;
|
item[COMPANIES_JSON_FIELD] = data.Companies.length > 0 ? JSON.stringify(data.Companies) : null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (key === 'Furniture') {
|
||||||
|
Object.assign(item, furnitureFields(data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'FurnitureOther') {
|
||||||
|
// Пишется вместе с Furniture (см. выше).
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (key === 'Organization') {
|
if (key === 'Organization') {
|
||||||
// Текстовая копия выбранных компаний — для печати и списка в SP (см. organizationText).
|
// Текстовая копия выбранных компаний — для печати и списка в SP (см. organizationText).
|
||||||
const text = organizationText(data);
|
const text = organizationText(data);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { effectiveUserEmail } from '../../shared/impersonation';
|
|||||||
import { FORM_ID } from './model';
|
import { FORM_ID } from './model';
|
||||||
|
|
||||||
export interface IApproverRights {
|
export interface IApproverRights {
|
||||||
/** Первый шаг согласования (руководитель) может заполнить «Необходимое ПО» и «Доступы». */
|
/** Первый шаг согласования (руководитель) может заполнить «Необходимое ПО», «Доступы» и «Мебель». */
|
||||||
canEditReviewFields: boolean;
|
canEditReviewFields: boolean;
|
||||||
/** Участник маршрута из shared/approval/fullEditRights.ts правит все поля и вложения. */
|
/** Участник маршрута из shared/approval/fullEditRights.ts правит все поля и вложения. */
|
||||||
canEditAll: boolean;
|
canEditAll: boolean;
|
||||||
@@ -28,7 +28,7 @@ const FULL_EDIT_STATUSES: ReadonlyArray<string> = [REQUEST_STATUS.onApproval, RE
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Что текущий пользователь может править в поданной заявке.
|
* Что текущий пользователь может править в поданной заявке.
|
||||||
* - «ПО»/«Доступы» — только на СВОЁМ активном шаге согласования, если это первый шаг.
|
* - «ПО»/«Доступы»/«Мебель» — только на СВОЁМ активном шаге согласования, если это первый шаг.
|
||||||
* - Все поля — если пользователь есть в маршруте (согласование или исполнение)
|
* - Все поля — если пользователь есть в маршруте (согласование или исполнение)
|
||||||
* НЕЗАВИСИМО от того, чья сейчас очередь, пока заявка в работе (решение
|
* НЕЗАВИСИМО от того, чья сейчас очередь, пока заявка в работе (решение
|
||||||
* Тимура 2026-10-07: так в «Организации рабочего места» работает Ербол).
|
* Тимура 2026-10-07: так в «Организации рабочего места» работает Ербол).
|
||||||
|
|||||||
Reference in New Issue
Block a user