b912724cf7
- 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>
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
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];
|
||
}
|