feat: «Необходимое ПО» в рабочем месте — мультивыбор из списка
Tests / frontend-test (push) Successful in 1m28s
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:
@@ -28,6 +28,10 @@
|
|||||||
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
|
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
|
||||||
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
|
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
|
||||||
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
|
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
|
||||||
|
- **Необходимое ПО** — мультивыбор из общего списка `shared/dictionary/software.ts`
|
||||||
|
(тот же, что в подборе) + своё значение. В SP и в маршрут — по-прежнему ТЕКСТОМ
|
||||||
|
в колонке `Software` через запятую (`softwareList.ts`): условия «Software содержит
|
||||||
|
AutoCAD» и старые заявки со свободным текстом работают как раньше.
|
||||||
- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле
|
- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле
|
||||||
своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и
|
своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и
|
||||||
`WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как
|
`WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
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 { ISectionProps } from '../model';
|
||||||
|
import { formatSoftware, parseSoftware } from '../softwareList';
|
||||||
|
|
||||||
interface IEquipmentSectionProps extends ISectionProps {
|
interface IEquipmentSectionProps extends ISectionProps {
|
||||||
/** Согласующий-руководитель на этапе согласования может править ПО и доступы,
|
/** Согласующий-руководитель на этапе согласования может править ПО и доступы,
|
||||||
@@ -13,13 +15,25 @@ export const EquipmentSection: React.FC<IEquipmentSectionProps> = ({ data, set,
|
|||||||
// берёт значение из FormMode (в форме подачи редактируемо, в просмотре — нет).
|
// берёт значение из FormMode (в форме подачи редактируемо, в просмотре — нет).
|
||||||
const fieldReadOnly = approverEditable ? false : undefined;
|
const fieldReadOnly = approverEditable ? false : undefined;
|
||||||
|
|
||||||
|
const software = (
|
||||||
|
<MultiChoiceField
|
||||||
|
value={parseSoftware(data.Software)}
|
||||||
|
onChange={(items) => set('Software', formatSoftware(items))}
|
||||||
|
options={SOFTWARE_OPTIONS}
|
||||||
|
allowOther
|
||||||
|
placeholder="Выберите программу или введите свою"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title="Оборудование и доступы" icon="monitor">
|
<CollapseSection title="Оборудование и доступы" icon="monitor">
|
||||||
<FormField label="Нужна ли техника" fieldKey="NeedsEquipment">
|
<FormField label="Нужна ли техника" fieldKey="NeedsEquipment">
|
||||||
<Toggle checked={data.NeedsEquipment} onChange={(v) => set('NeedsEquipment', v)} onLabel="Да" />
|
<Toggle checked={data.NeedsEquipment} onChange={(v) => set('NeedsEquipment', v)} onLabel="Да" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Необходимое ПО" htmlFor="Software" full>
|
<FormField label="Необходимое ПО" fieldKey="Software" full>
|
||||||
<TextArea id="Software" value={data.Software} onChange={(v) => set('Software', v)} readOnly={fieldReadOnly} />
|
{/* У MultiChoiceField нет своего readOnly — право руководителя даём вложенным
|
||||||
|
провайдером режима, как в FurnitureSection. */}
|
||||||
|
{approverEditable ? <FormModeProvider readOnly={false}>{software}</FormModeProvider> : software}
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Доступы" htmlFor="AccessRights" full>
|
<FormField label="Доступы" htmlFor="AccessRights" full>
|
||||||
<TextArea id="AccessRights" value={data.AccessRights} onChange={(v) => set('AccessRights', v)} readOnly={fieldReadOnly} />
|
<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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user