e0ffe7e686
- api/reports.ts: типизированные axios-helpers (listReportJobs/createReportJob/
retryReportJob/cancelReportJob/deleteReportJob) + ApiReportJob/Status/Format/
Counts/Quota interfaces. ensureCsrfCookie на mutating-вызовах.
- composables/reportsMapper.ts: mapApiReportJob (API → UI mock format с конверсией
pending→queued / processing→running). title строится на frontend'е (тип + period
с RU-месяцами «апр 2026» или диапазон «мар 2026 — апр 2026»). sizeText форматирует
bytes (B/KB/MB). timeText зависит от status (в очереди / в работе · Nс / N мин назад).
uiTypeToApi (deals → deals_export и т.д.).
- ReportsView.vue полностью переписан под API:
- onMounted → loadJobs (replace MOCK_JOBS на data из listReportJobs).
- usePolling 30 сек (фоновый авто-refresh).
- Submit → createReportJob → reload + success-alert + error-alert (validation+
общие ошибки извлекаются через extractValidationErrors/extractErrorMessage).
- canSubmit computed: disable если квота заполнена (active >= max).
- Reset-btn возвращает форму к defaults.
- Reload-btn (manual fast-path).
- Retry/Cancel/Download/Delete-кнопки → соответствующие API-вызовы;
Delete через v-dialog persistent confirm.
- fetch-error-alert на listReportJobs reject.
- Empty-state «Нет отчётов» когда jobs.length=0.
- canRetry проверяет retry_count<3 (max attempts CTO-6).
- Vitest +24 (всего 393/393, +24 от 369):
- reportsMapper.spec.ts +14: status mapping (pending/processing/done/failed) /
title (один месяц / диапазон) / format / sizeText (B/KB/MB/null) / attempt /
error / timeText (pending / processing / done «10 мин назад» / «только что») /
uiTypeToApi 4 slug'а / progress=50 для running.
- ReportsView.spec.ts переписан с MOCK_JOBS на vi.mock('api/reports') +12:
mount + listReportJobs called on mount / 4 type cards / default Сделки active /
4 формата / quota-banner из API / empty-state / done с Готов+Скачать /
failed с Ошибка+Повторить / failed retry_count=3 НЕ показывает Повторить /
pending с Отменить / Submit вызывает createReportJob+reload / Submit error →
submit-error-alert / Submit-btn disabled при квоте 3/3 / Reset / Reload-btn /
fetch-error-alert / Retry-btn / Cancel-btn / Delete confirm-dialog +
deleteReportJob.
Этап 4/4 эпика Reports backend ЗАКРЫТ. Эпик закрыт целиком.
Backend: 1 type (deals_export) × 4 формата (CSV/XLSX/JSON/PDF-stub).
Этап 2b (3 оставшихся типа: managers_summary/sources_summary/billing_summary)
— расширение через добавление 3 новых Provider-классов без изменений в архитектуре,
вынесено в Post-MVP backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
4.9 KiB
TypeScript
121 lines
4.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { mapApiReportJob, uiTypeToApi } from '../../resources/js/composables/reportsMapper';
|
|
import type { ApiReportJob } from '../../resources/js/api/reports';
|
|
|
|
function makeApi(overrides: Partial<ApiReportJob> = {}): ApiReportJob {
|
|
return {
|
|
id: 1,
|
|
type: 'deals_export',
|
|
parameters: { format: 'csv', date_from: '2026-04-01', date_to: '2026-04-30' },
|
|
status: 'done',
|
|
file_path: 'reports/1/1.csv',
|
|
file_size: 2048,
|
|
generation_seconds: 2,
|
|
error_message: null,
|
|
created_at: '2026-04-15T10:00:00Z',
|
|
finished_at: '2026-04-15T10:01:00Z',
|
|
expires_at: '2026-05-15T10:01:00Z',
|
|
is_expired: false,
|
|
retry_count: 0,
|
|
retry_max: 3,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('reportsMapper', () => {
|
|
it('маппит status pending → queued', () => {
|
|
const ui = mapApiReportJob(makeApi({ status: 'pending' }));
|
|
expect(ui.status).toBe('queued');
|
|
});
|
|
|
|
it('маппит status processing → running', () => {
|
|
const ui = mapApiReportJob(makeApi({ status: 'processing' }));
|
|
expect(ui.status).toBe('running');
|
|
});
|
|
|
|
it('done и failed остаются как есть', () => {
|
|
expect(mapApiReportJob(makeApi({ status: 'done' })).status).toBe('done');
|
|
expect(mapApiReportJob(makeApi({ status: 'failed' })).status).toBe('failed');
|
|
});
|
|
|
|
it('title включает тип + period (один месяц)', () => {
|
|
const ui = mapApiReportJob(makeApi());
|
|
expect(ui.title).toBe('Сделки · детально · апр 2026');
|
|
});
|
|
|
|
it('title для разных месяцев показывает диапазон', () => {
|
|
const ui = mapApiReportJob(
|
|
makeApi({ parameters: { format: 'csv', date_from: '2026-03-01', date_to: '2026-04-30' } }),
|
|
);
|
|
expect(ui.title).toBe('Сделки · детально · мар 2026 — апр 2026');
|
|
});
|
|
|
|
it('format берётся из parameters.format', () => {
|
|
const ui = mapApiReportJob(
|
|
makeApi({ parameters: { format: 'xlsx', date_from: '2026-04-01', date_to: '2026-04-30' } }),
|
|
);
|
|
expect(ui.format).toBe('xlsx');
|
|
});
|
|
|
|
it('sizeText форматирует bytes (KB / MB)', () => {
|
|
expect(mapApiReportJob(makeApi({ file_size: 500 })).sizeText).toBe('500 B');
|
|
expect(mapApiReportJob(makeApi({ file_size: 2048 })).sizeText).toBe('2.0 KB');
|
|
expect(mapApiReportJob(makeApi({ file_size: 1572864 })).sizeText).toBe('1.5 MB');
|
|
});
|
|
|
|
it('sizeText null если file_size null', () => {
|
|
expect(mapApiReportJob(makeApi({ file_size: null })).sizeText).toBeNull();
|
|
});
|
|
|
|
it('attempt = retry_count + 1', () => {
|
|
expect(mapApiReportJob(makeApi({ retry_count: 0 })).attempt).toBe(1);
|
|
expect(mapApiReportJob(makeApi({ retry_count: 2 })).attempt).toBe(3);
|
|
});
|
|
|
|
it('error из error_message', () => {
|
|
expect(mapApiReportJob(makeApi({ error_message: 'boom' })).error).toBe('boom');
|
|
});
|
|
|
|
it('timeText для pending = «в очереди»', () => {
|
|
expect(mapApiReportJob(makeApi({ status: 'pending', file_path: null })).timeText).toBe('в очереди');
|
|
});
|
|
|
|
it('timeText для processing включает «в работе»', () => {
|
|
const ui = mapApiReportJob(
|
|
makeApi({ status: 'processing', created_at: new Date(Date.now() - 5000).toISOString() }),
|
|
);
|
|
expect(ui.timeText).toContain('в работе');
|
|
});
|
|
|
|
it('timeText для done показывает relative «N мин назад»', () => {
|
|
const ui = mapApiReportJob(
|
|
makeApi({
|
|
status: 'done',
|
|
finished_at: new Date(Date.now() - 10 * 60_000).toISOString(),
|
|
}),
|
|
);
|
|
expect(ui.timeText).toBe('10 мин назад');
|
|
});
|
|
|
|
it('timeText для finished меньше минуты = «только что»', () => {
|
|
const ui = mapApiReportJob(
|
|
makeApi({ status: 'done', finished_at: new Date(Date.now() - 30_000).toISOString() }),
|
|
);
|
|
expect(ui.timeText).toBe('только что');
|
|
});
|
|
|
|
it('uiTypeToApi маппит slug правильно', () => {
|
|
expect(uiTypeToApi('deals')).toBe('deals_export');
|
|
expect(uiTypeToApi('managers')).toBe('managers_summary');
|
|
expect(uiTypeToApi('sources')).toBe('sources_summary');
|
|
expect(uiTypeToApi('billing')).toBe('billing_summary');
|
|
});
|
|
|
|
it('progress=50 для running, null для остальных', () => {
|
|
expect(mapApiReportJob(makeApi({ status: 'processing' })).progress).toBe(50);
|
|
expect(mapApiReportJob(makeApi({ status: 'done' })).progress).toBeNull();
|
|
expect(mapApiReportJob(makeApi({ status: 'pending' })).progress).toBeNull();
|
|
expect(mapApiReportJob(makeApi({ status: 'failed' })).progress).toBeNull();
|
|
});
|
|
});
|