2 Commits

Author SHA1 Message Date
Тимур Городецкий b1060433d0 feat: «Необходимое ПО» в рабочем месте — мультивыбор из списка
Tests / frontend-test (push) Successful in 1m28s
Вместо свободного текста — выбор из общего списка ПО (как в подборе) со
своим значением. В SP и в маршрут по-прежнему уходит текст через запятую в
колонке Software: условия «Software содержит …» и старые заявки работают
без изменений.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:58:32 +05:00
Тимур Городецкий cf039f4d08 refactor: общий список ПО в shared/dictionary
Список программ жил в модели «Заявки на подбор»; теперь он нужен и
«Организации рабочего места». Вынесен в shared/dictionary/software.ts, чтобы
обе формы брали его из одного места.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:58:31 +05:00
8 changed files with 106 additions and 27 deletions
@@ -207,28 +207,6 @@ export const LANGUAGE_LEVEL_OPTIONS: ISelectOption[] = [
{ value: 'Свободный', label: 'Свободный' } { value: 'Свободный', label: 'Свободный' }
]; ];
/**
* Программы (ПО) — ВРЕМЕННЫЙ список, пока нет справочника в API. Используется в
* мультивыборе «Необходимые знания в программах». Меняется здесь одним местом.
*/
export const SOFTWARE_OPTIONS: ISelectOption[] = [
{ value: '1С:Предприятие', label: '1С:Предприятие' },
{ value: 'MS Word', label: 'MS Word' },
{ value: 'MS Excel', label: 'MS Excel' },
{ value: 'MS PowerPoint', label: 'MS PowerPoint' },
{ value: 'MS Outlook', label: 'MS Outlook' },
{ value: 'MS Project', label: 'MS Project' },
{ value: 'AutoCAD', label: 'AutoCAD' },
{ value: 'Autodesk Forma', label: 'Autodesk Forma' },
{ value: 'BIM', label: 'BIM' },
{ value: 'Revit', label: 'Revit' },
{ value: 'ArchiCAD', label: 'ArchiCAD' },
{ value: 'Компас-3D', label: 'Компас-3D' },
{ value: 'Primavera', label: 'Primavera' },
{ value: 'SAP', label: 'SAP' },
{ value: 'Adobe Photoshop', label: 'Adobe Photoshop' }
];
/** Категории «Обеспечение рабочего места» (+ «Другое» через allowOther). */ /** Категории «Обеспечение рабочего места» (+ «Другое» через allowOther). */
export const WORKPLACE_PROVISION_OPTIONS: ISelectOption[] = [ export const WORKPLACE_PROVISION_OPTIONS: ISelectOption[] = [
{ value: 'Оргтехника', label: 'Оргтехника' }, { value: 'Оргтехника', label: 'Оргтехника' },
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Select, MultiChoiceField } from '../../../shared/ui'; import { CollapseSection, FormField, TextInput, TextArea, Select, MultiChoiceField } from '../../../shared/ui';
import { PositionPicker } from '../../../shared/dictionary'; import { PositionPicker, SOFTWARE_OPTIONS } from '../../../shared/dictionary';
import { LanguagesField } from './LanguagesField'; import { LanguagesField } from './LanguagesField';
import { import {
ISectionProps, ISectionProps,
@@ -8,7 +8,6 @@ import {
EDUCATION_OPTIONS, EDUCATION_OPTIONS,
WORK_EXPERIENCE_OPTIONS, WORK_EXPERIENCE_OPTIONS,
RESIDENCE_OPTIONS, RESIDENCE_OPTIONS,
SOFTWARE_OPTIONS,
OTHER_VALUE OTHER_VALUE
} from '../model'; } from '../model';
@@ -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);
}
@@ -50,3 +50,4 @@ export {
parseOrganizationLine parseOrganizationLine
} from './organizationLines'; } from './organizationLines';
export type { IOrganizationLine } from './organizationLines'; export type { IOrganizationLine } from './organizationLines';
export { SOFTWARE_OPTIONS } from './software';
@@ -0,0 +1,27 @@
import { ISelectOption } from '@tnsu/ui-kit';
/**
* Программы (ПО) — ВРЕМЕННЫЙ список, пока нет справочника в API. Общий для форм:
* «Заявка на подбор» (3.8 Необходимые знания в программах) и «Организация
* рабочего места» (Необходимое ПО). Пункт добавляется здесь — появляется в обеих.
*
* Значения уходят в условия маршрута как текст («Software содержит AutoCAD»),
* поэтому существующие значения не переименовывать без правки условий в админке.
*/
export const SOFTWARE_OPTIONS: ISelectOption[] = [
{ value: '1С:Предприятие', label: '1С:Предприятие' },
{ value: 'MS Word', label: 'MS Word' },
{ value: 'MS Excel', label: 'MS Excel' },
{ value: 'MS PowerPoint', label: 'MS PowerPoint' },
{ value: 'MS Outlook', label: 'MS Outlook' },
{ value: 'MS Project', label: 'MS Project' },
{ value: 'AutoCAD', label: 'AutoCAD' },
{ value: 'Autodesk Forma', label: 'Autodesk Forma' },
{ value: 'BIM', label: 'BIM' },
{ value: 'Revit', label: 'Revit' },
{ value: 'ArchiCAD', label: 'ArchiCAD' },
{ value: 'Компас-3D', label: 'Компас-3D' },
{ value: 'Primavera', label: 'Primavera' },
{ value: 'SAP', label: 'SAP' },
{ value: 'Adobe Photoshop', label: 'Adobe Photoshop' }
];