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

106 lines
4.0 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,
downloadUrl: api.download_url,
};
}
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];
}