diff --git a/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx index de0f2a3..bbd94e5 100644 --- a/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx +++ b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx @@ -5,6 +5,7 @@ import { ExpenseItemApi } from '../../../api/expense-item'; import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons'; import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled'; import { Cancel } from '../../common/icons/icons'; +import { compareExpenseItems, getExpenseItemLabel } from '../utils/expenseItemOptions'; const SelectExpenseItemModalComponent = ({ isOpen, onClose, onSelect, formId, title = '', isSaving = false, sheet }) => { const [selectedValue, setSelectedValue] = useState(''); @@ -14,7 +15,7 @@ const SelectExpenseItemModalComponent = ({ isOpen, onClose, onSelect, formId, ti if (!isOpen || formId == null) return; ExpenseItemApi.getAll({ r_start: true, sheet: sheet }).then((data) => { if (data.success) { - setExpenseItemOption(data.result); + setExpenseItemOption([...data.result].sort(compareExpenseItems)); return; } toast.error('Ошибка получения статья расходов'); @@ -69,7 +70,7 @@ const SelectExpenseItemModalComponent = ({ isOpen, onClose, onSelect, formId, ti size='small' options={expenseItemOptions} noOptionsText='Не найдено' - getOptionLabel={(option) => option.name || ''} + getOptionLabel={getExpenseItemLabel} getOptionKey={(option) => option.id} value={selectedValue} onChange={(_event, newValue) => { diff --git a/web/src/components/RealtimeTable/utils/expenseItemOptions.js b/web/src/components/RealtimeTable/utils/expenseItemOptions.js new file mode 100644 index 0000000..70dbe92 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/expenseItemOptions.js @@ -0,0 +1,22 @@ +const getItemCode = (option) => { + const code = option?.item_id; + if (code == null) return ''; + return String(code).trim(); +}; + +export const getExpenseItemLabel = (option) => { + if (!option || typeof option !== 'object') return ''; + const code = getItemCode(option); + const name = option.name || ''; + if (code && name) return `${code} — ${name}`; + return code || name; +}; + +export const compareExpenseItems = (a, b) => { + const codeA = getItemCode(a); + const codeB = getItemCode(b); + if (!codeA && !codeB) return 0; + if (!codeA) return 1; + if (!codeB) return -1; + return codeA.localeCompare(codeB, 'ru', { numeric: true }); +}; diff --git a/web/src/components/RealtimeTable/utils/expenseItemOptions.test.js b/web/src/components/RealtimeTable/utils/expenseItemOptions.test.js new file mode 100644 index 0000000..08e94be --- /dev/null +++ b/web/src/components/RealtimeTable/utils/expenseItemOptions.test.js @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { compareExpenseItems, getExpenseItemLabel } from './expenseItemOptions.js'; + +const sorted = (ids) => + ids + .map((item_id) => ({ item_id, name: 'x' })) + .sort(compareExpenseItems) + .map((o) => o.item_id); + +test('AURORA-1989: подпись варианта', () => { + assert.equal(getExpenseItemLabel({ item_id: '3.01.', name: 'Связь' }), '3.01. — Связь'); + assert.equal(getExpenseItemLabel({ item_id: null, name: 'Связь' }), 'Связь'); + assert.equal(getExpenseItemLabel({ name: '' }), ''); + assert.equal(getExpenseItemLabel(''), ''); +}); + +test('AURORA-1989: сортировка с учётом числовых частей, без кода — в конце', () => { + assert.deepEqual(sorted(['3.10.', '3.2.', '10', '2']), ['2', '3.2.', '3.10.', '10']); + assert.deepEqual(sorted([null, '1.', '']), ['1.', null, '']); +}); + +test('AURORA-1989: поиск по подписи находит номер и название', () => { + const label = getExpenseItemLabel({ item_id: '3.01.', name: 'Связь' }); + assert.ok(label.includes('3.01')); + assert.ok(label.includes('Связь')); +});