feat: «Необходимое ПО» в рабочем месте — мультивыбор из списка
Tests / frontend-test (push) Successful in 1m28s

Вместо свободного текста — выбор из общего списка ПО (как в подборе) со
своим значением. В SP и в маршрут по-прежнему уходит текст через запятую в
колонке Software: условия «Software содержит …» и старые заявки работают
без изменений.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-30 09:58:32 +05:00
parent cf039f4d08
commit b1060433d0
4 changed files with 77 additions and 3 deletions
@@ -28,6 +28,10 @@
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
- **Необходимое ПО** — мультивыбор из общего списка `shared/dictionary/software.ts`
(тот же, что в подборе) + своё значение. В SP и в маршрут — по-прежнему ТЕКСТОМ
в колонке `Software` через запятую (`softwareList.ts`): условия «Software содержит
AutoCAD» и старые заявки со свободным текстом работают как раньше.
- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле
своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и
`WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как
@@ -1,6 +1,8 @@
import * as React from 'react';
import { CollapseSection, FormField, TextArea, Toggle } from '../../../shared/ui';
import { CollapseSection, FormField, FormModeProvider, MultiChoiceField, TextArea, Toggle } from '../../../shared/ui';
import { SOFTWARE_OPTIONS } from '../../../shared/dictionary';
import { ISectionProps } from '../model';
import { formatSoftware, parseSoftware } from '../softwareList';
interface IEquipmentSectionProps extends ISectionProps {
/** Согласующий-руководитель на этапе согласования может править ПО и доступы,
@@ -13,13 +15,25 @@ export const EquipmentSection: React.FC<IEquipmentSectionProps> = ({ data, set,
// берёт значение из FormMode (в форме подачи редактируемо, в просмотре — нет).
const fieldReadOnly = approverEditable ? false : undefined;
const software = (
<MultiChoiceField
value={parseSoftware(data.Software)}
onChange={(items) => set('Software', formatSoftware(items))}
options={SOFTWARE_OPTIONS}
allowOther
placeholder="Выберите программу или введите свою"
/>
);
return (
<CollapseSection title="Оборудование и доступы" icon="monitor">
<FormField label="Нужна ли техника" fieldKey="NeedsEquipment">
<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 label="Необходимое ПО" fieldKey="Software" full>
{/* У MultiChoiceField нет своего readOnly — право руководителя даём вложенным
провайдером режима, как в FurnitureSection. */}
{approverEditable ? <FormModeProvider readOnly={false}>{software}</FormModeProvider> : software}
</FormField>
<FormField label="Доступы" htmlFor="AccessRights" full>
<TextArea id="AccessRights" value={data.AccessRights} onChange={(v) => set('AccessRights', v)} readOnly={fieldReadOnly} />
@@ -0,0 +1,31 @@
import { formatSoftware, parseSoftware } from './softwareList';
describe('parseSoftware', () => {
it('разбирает сохранённый список', () => {
expect(parseSoftware('AutoCAD, MS Excel, Revit')).toEqual(['AutoCAD', 'MS Excel', 'Revit']);
});
it('пустой текст — пустой список', () => {
expect(parseSoftware('')).toEqual([]);
expect(parseSoftware(' , \n ')).toEqual([]);
});
it('старый свободный текст с переносами строк — тоже чипами', () => {
expect(parseSoftware('AutoCAD\nКорпоративная почта')).toEqual(['AutoCAD', 'Корпоративная почта']);
});
it('убирает повторы', () => {
expect(parseSoftware('SAP, SAP, Revit')).toEqual(['SAP', 'Revit']);
});
});
describe('formatSoftware', () => {
it('склеивает через запятую — формат, который ждут условия маршрута', () => {
expect(formatSoftware(['AutoCAD', 'Revit'])).toBe('AutoCAD, Revit');
});
it('туда и обратно — без потерь', () => {
const items = ['1С:Предприятие', 'MS Word', 'Своя программа'];
expect(parseSoftware(formatSoftware(items))).toEqual(items);
});
});
@@ -0,0 +1,25 @@
/**
* «Необходимое ПО» — мультивыбор на экране, но в SP и в маршруте это ТЕКСТ в
* колонке Software (условия вида «Software содержит AutoCAD» заведены по нему).
* Поэтому модель хранит строку, а раздел переводит её в список и обратно.
*/
const SEPARATOR = ', ';
/**
* Текст колонки → список для мультивыбора. Делим по запятым и переносам строк:
* у старых заявок ПО написано свободным текстом — так он тоже ляжет чипами.
* Повторы убираем: чип с тем же значением был бы дублем.
*/
export function parseSoftware(text: string): string[] {
const items = text
.split(/[,\n]/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
return items.filter((s, i) => items.indexOf(s) === i);
}
/** Список из мультивыбора → текст для колонки Software. */
export function formatSoftware(items: string[]): string {
return items.join(SEPARATOR);
}