Files
portal/app/resources/js/composables/reportsMapper.ts
T

105 lines
3.9 KiB
TypeScript
Raw Normal View History

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];
}