feat(stages): чистые функции фильтрации и сортировки этапов, now в getStageStatus (AURORA-1881)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5e364ac807
commit
63c368902b
@ -1,4 +1,4 @@
|
|||||||
export const getStageStatus = (stage) => {
|
export const getStageStatus = (stage, nowDate = new Date()) => {
|
||||||
const start = stage.opens_at;
|
const start = stage.opens_at;
|
||||||
const end = stage.closes_at;
|
const end = stage.closes_at;
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ export const getStageStatus = (stage) => {
|
|||||||
return 'Не определен';
|
return 'Не определен';
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date(nowDate);
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const startDate = new Date(start);
|
const startDate = new Date(start);
|
||||||
|
|||||||
42
web/src/components/Stages/stageFilters.js
Normal file
42
web/src/components/Stages/stageFilters.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { getStageStatus } from './StagesTable/utils.js';
|
||||||
|
|
||||||
|
export const EMPTY_STAGE_FILTERS = { search: '', sheetKey: '', role: '', status: '' };
|
||||||
|
|
||||||
|
export const DIRECTION_ORDER = ['Support', 'Development'];
|
||||||
|
|
||||||
|
export const getSheetKey = ({ sheet, direction }) => `${sheet}|${direction ?? ''}`;
|
||||||
|
|
||||||
|
const normalize = (value) => String(value ?? '').trim().toLowerCase().replaceAll('ё', 'е');
|
||||||
|
|
||||||
|
export const filterStages = (stages, filters = EMPTY_STAGE_FILTERS, now = new Date()) => {
|
||||||
|
const { search = '', sheetKey = '', role = '', status = '' } = filters ?? {};
|
||||||
|
const query = normalize(search);
|
||||||
|
return (stages ?? []).filter((stage) => {
|
||||||
|
if (sheetKey && getSheetKey(stage) !== sheetKey) return false;
|
||||||
|
if (role && stage.role !== role) return false;
|
||||||
|
if (status && getStageStatus(stage, now) !== status) return false;
|
||||||
|
if (query && !normalize(stage.phase_code).includes(query)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rank = (list, value) => {
|
||||||
|
const index = list.indexOf(value);
|
||||||
|
return index === -1 ? list.length : index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const time = (value) => (value ? new Date(value).getTime() : Number.POSITIVE_INFINITY);
|
||||||
|
|
||||||
|
export const sortStages = (stages, sheetOrder = []) =>
|
||||||
|
[...(stages ?? [])].sort((a, b) => {
|
||||||
|
const bySheet = rank(sheetOrder, a.sheet) - rank(sheetOrder, b.sheet);
|
||||||
|
if (bySheet !== 0) return bySheet;
|
||||||
|
// направления: Support, Development, затем без направления
|
||||||
|
const dirRank = (s) => (s.direction ? rank(DIRECTION_ORDER, s.direction) : DIRECTION_ORDER.length + 1);
|
||||||
|
const byDirection = dirRank(a) - dirRank(b);
|
||||||
|
if (byDirection !== 0) return byDirection;
|
||||||
|
const ta = time(a.opens_at);
|
||||||
|
const tb = time(b.opens_at);
|
||||||
|
if (ta !== tb) return ta < tb ? -1 : 1;
|
||||||
|
return String(a.phase_code ?? '').localeCompare(String(b.phase_code ?? ''), 'ru');
|
||||||
|
});
|
||||||
83
web/src/components/Stages/stageFilters.test.js
Normal file
83
web/src/components/Stages/stageFilters.test.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { EMPTY_STAGE_FILTERS, filterStages, sortStages } from './stageFilters.js';
|
||||||
|
|
||||||
|
const NOW = new Date(2026, 10, 11);
|
||||||
|
const mk = (sheet, direction, phase_code, extra = {}) => ({
|
||||||
|
budget_form_id: 1,
|
||||||
|
sheet,
|
||||||
|
direction,
|
||||||
|
phase_code,
|
||||||
|
role: 'executor',
|
||||||
|
opens_at: '2026-11-01',
|
||||||
|
closes_at: '2026-11-30',
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stages = [
|
||||||
|
mk('AHR', 'Support', 'Сбор'),
|
||||||
|
mk('AHR', 'Development', 'Сбор'),
|
||||||
|
mk('OPER', null, 'Сбор'),
|
||||||
|
mk('OPER', null, 'Ёлка', { role: 'approver', opens_at: '2026-01-01', closes_at: '2026-02-01' }),
|
||||||
|
mk('AHR', 'Support', 'Будущее', { opens_at: '2027-01-01', closes_at: '2027-02-01' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const f = (o) => ({ ...EMPTY_STAGE_FILTERS, ...o });
|
||||||
|
|
||||||
|
test('пустые фильтры возвращают всё', () => {
|
||||||
|
assert.equal(filterStages(stages, EMPTY_STAGE_FILTERS, NOW).length, stages.length);
|
||||||
|
assert.equal(filterStages(stages, undefined, NOW).length, stages.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AHR|Development не пропускает AHR|Support с тем же названием', () => {
|
||||||
|
const res = filterStages(stages, f({ sheetKey: 'AHR|Development' }), NOW);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].direction, 'Development');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('OPER| пропускает только этапы без направления', () => {
|
||||||
|
const res = filterStages(stages, f({ sheetKey: 'OPER|' }), NOW);
|
||||||
|
assert.equal(res.length, 2);
|
||||||
|
assert.ok(res.every((s) => s.sheet === 'OPER' && !s.direction));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('фильтр по статусу при фиксированном now', () => {
|
||||||
|
assert.equal(filterStages(stages, f({ status: 'Активен' }), NOW).length, 3);
|
||||||
|
assert.equal(filterStages(stages, f({ status: 'Завершен' }), NOW)[0].phase_code, 'Ёлка');
|
||||||
|
assert.equal(filterStages(stages, f({ status: 'Запланирован' }), NOW)[0].phase_code, 'Будущее');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('поиск без учёта регистра, кириллица и Ё', () => {
|
||||||
|
assert.equal(filterStages(stages, f({ search: 'сбОР' }), NOW).length, 3);
|
||||||
|
assert.equal(filterStages(stages, f({ search: 'ёлка' }), NOW).length, 1);
|
||||||
|
assert.equal(filterStages(stages, f({ search: 'ЁЛКА' }), NOW).length, 1);
|
||||||
|
assert.equal(filterStages(stages, f({ search: ' сбор ' }), NOW).length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('фильтр роли', () => {
|
||||||
|
const res = filterStages(stages, f({ role: 'approver' }), NOW);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].phase_code, 'Ёлка');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('фильтры комбинируются', () => {
|
||||||
|
const res = filterStages(stages, f({ sheetKey: 'AHR|Support', status: 'Активен' }), NOW);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sortStages: лист по порядку sheets, затем направление, дата, название', () => {
|
||||||
|
const sheetOrder = ['OPER', 'AHR'];
|
||||||
|
const shuffled = [stages[4], stages[1], stages[3], stages[0], stages[2]];
|
||||||
|
const res = sortStages(shuffled, sheetOrder);
|
||||||
|
assert.deepEqual(
|
||||||
|
res.map((s) => `${s.sheet}|${s.direction ?? ''}|${s.phase_code}`),
|
||||||
|
['OPER||Ёлка', 'OPER||Сбор', 'AHR|Support|Сбор', 'AHR|Support|Будущее', 'AHR|Development|Сбор'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sortStages не мутирует вход, неизвестный лист в конце', () => {
|
||||||
|
const input = [mk('ZZZ', null, 'A'), mk('AHR', null, 'B')];
|
||||||
|
const res = sortStages(input, ['AHR']);
|
||||||
|
assert.equal(res[0].sheet, 'AHR');
|
||||||
|
assert.equal(input[0].sheet, 'ZZZ');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user