Files
portal/app/resources/js/composables/reportsMapper.ts
T
Дмитрий b912724cf7 chore(frontend): Sprint 4 Phase C — bundle analyzer + dead-code cleanup (audit O-refactor-06)
- rollup-plugin-visualizer + script `npm run build:analyze` (env BUILD_ANALYZE=1)
  Output: storage/bundle-analyze.html (gzip + brotli sizes), gitignored.
- cross-env установлен для Windows-совместимости env-переменной build:analyze.
- knip + knip.config.ts (entry app.ts + router/index.ts; ignore *.story.vue + tests/).
  ВАЖНО: knip падает с oxc-parser ArrayBuffer fail на этой машине
  (Windows quirk feedback memory) — конфиг сохранён для будущих запусков на
  Linux/macOS CI. Dead-code search выполнен вручную через grep по composables/.
- Удалены 4 unused exports + 4 private helpers, инициируемых только ими:
  * mockReports.ts: MOCK_JOBS, QuotaInfo (interface), MOCK_QUOTA
  * reportsMapper.ts: reportTypes()
  * mockTenantDetail.ts: expandTenantDetail() + 4 SAMPLE_* consts
    (SAMPLE_USERS/SAMPLE_PROJECTS/SAMPLE_BALANCE_HISTORY/SAMPLE_ACTIVITY)
  * useCsvDownload.ts: csvEscape() — снят `export` (используется внутри файла)

ESLint + vue-tsc + Vitest 416/416 + build — зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:59:58 +03:00

105 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ApiReportJob, ApiReportStatus } from '../api/reports';
import type { ReportFormat, ReportJob, ReportStatus, ReportType } from './mockReports';
/**
* API → UI converter для ReportsView.
*
* Schema-канон → UI mock format:
* pending → queued
* processing → running
* done → done
* failed → failed
*
* Title строится на frontend'е (бэк не хранит) из type + period.
* timeText зависит от status: «в очереди» / «в работе» / «time1 → time2» / «N дней назад».
*/
const STATUS_MAP: Record<ApiReportStatus, ReportStatus> = {
pending: 'queued',
processing: 'running',
done: 'done',
failed: 'failed',
};
const TYPE_TITLES: Record<ApiReportJob['type'], string> = {
deals_export: 'Сделки · детально',
managers_summary: 'Менеджеры',
sources_summary: 'Источники',
billing_summary: 'Биллинг',
};
export function mapApiReportJob(api: ApiReportJob, now: Date = new Date()): ReportJob {
const baseTitle = TYPE_TITLES[api.type] ?? api.type;
const title = `${baseTitle} · ${formatPeriod(api.parameters.date_from, api.parameters.date_to)}`;
return {
id: api.id,
title,
format: api.parameters.format as ReportFormat,
status: STATUS_MAP[api.status],
sizeText: api.file_size !== null ? formatBytes(api.file_size) : null,
rowsText: null,
timeText: buildTimeText(api, now),
progress: api.status === 'processing' ? 50 : null,
attempt: api.retry_count + 1,
error: api.error_message,
};
}
function formatPeriod(dateFrom: string, dateTo: string): string {
const months = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
const parse = (s: string): { month: string; year: number } | null => {
const parts = s.split('-');
if (parts.length < 3) return null;
const yr = parseInt(parts[0]!, 10);
const mo = parseInt(parts[1]!, 10);
if (Number.isNaN(yr) || Number.isNaN(mo) || mo < 1 || mo > 12) return null;
return { month: months[mo - 1]!, year: yr };
};
const from = parse(dateFrom);
const to = parse(dateTo);
if (!from || !to) return `${dateFrom}${dateTo}`;
if (from.year === to.year && from.month === to.month) {
return `${from.month} ${from.year}`;
}
return `${from.month} ${from.year}${to.month} ${to.year}`;
}
function buildTimeText(api: ApiReportJob, now: Date): string {
if (api.status === 'pending') return 'в очереди';
if (api.status === 'processing') {
const sec = api.created_at
? Math.max(1, Math.floor((now.getTime() - new Date(api.created_at).getTime()) / 1000))
: 0;
return `в работе · ${sec}с`;
}
// done | failed
if (api.finished_at !== null) {
const minutesAgo = Math.floor((now.getTime() - new Date(api.finished_at).getTime()) / 60_000);
if (minutesAgo < 1) return 'только что';
if (minutesAgo < 60) return `${minutesAgo} мин назад`;
if (minutesAgo < 60 * 24) return `${Math.floor(minutesAgo / 60)} ч назад`;
return `${Math.floor(minutesAgo / (60 * 24))} д назад`;
}
return '';
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
/**
* UI ReportType slug ('deals') → API ApiReportType ('deals_export').
*/
export function uiTypeToApi(uiType: ReportType): ApiReportJob['type'] {
const map: Record<ReportType, ApiReportJob['type']> = {
deals: 'deals_export',
managers: 'managers_summary',
sources: 'sources_summary',
billing: 'billing_summary',
};
return map[uiType];
}