hasari-api / apps /desktop /src /lib /export.ts
erdoganpeker's picture
v0.3.0 — multimodal vehicle damage MVP
e327f0d
/**
* Client-side CSV / lightweight-PDF builders for inspection data.
* The CSV is RFC 4180-compatible (quotes-doubled, CRLF). The PDF here is a fallback
* "text PDF" used when the backend report endpoint is unavailable.
*/
import type { Inspection, InspectionListItem } from '@arac-hasar/types';
function csvEscape(v: unknown): string {
if (v === null || v === undefined) return '';
const s = String(v);
if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
return s;
}
export function inspectionsToCsv(items: InspectionListItem[]): string {
const header = [
'inspection_id',
'created_at',
'status',
'damage_count',
'total_cost_midpoint_tl',
];
const rows = items.map((it) => [
it.inspection_id,
it.created_at,
it.status,
it.damage_count,
it.total_cost_midpoint_tl ?? '',
]);
return [header, ...rows].map((r) => r.map(csvEscape).join(',')).join('\r\n');
}
export function inspectionDetailToCsv(inspection: Inspection): string {
const header = [
'part',
'part_status',
'damage_type',
'severity_level',
'confidence',
'cost_min_tl',
'cost_midpoint_tl',
'cost_max_tl',
'area_ratio',
];
const rows: unknown[][] = [];
for (const part of inspection.parts) {
if (part.damages.length === 0) {
rows.push([part.name, part.status, '', '', '', '', '', '', '']);
continue;
}
for (const d of part.damages) {
rows.push([
part.name,
part.status,
d.type,
d.severity?.level ?? '',
d.confidence,
d.cost?.min_tl ?? '',
d.cost?.midpoint_tl ?? '',
d.cost?.max_tl ?? '',
d.area_ratio,
]);
}
}
return [header, ...rows].map((r) => r.map(csvEscape).join(',')).join('\r\n');
}
/**
* Tiny single-page text PDF builder — used as a graceful fallback when no
* server-side PDF endpoint is reachable. Real reports come from `api.exportInspectionPdf`.
*/
export function buildTextPdfBase64(title: string, lines: string[]): string {
const escape = (s: string) =>
s.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
const body =
`BT /F1 14 Tf 50 780 Td (${escape(title)}) Tj ET\n` +
lines
.map((l, i) => `BT /F1 10 Tf 50 ${750 - i * 14} Td (${escape(l)}) Tj ET`)
.join('\n');
const stream = body;
const objects = [
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`,
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
];
let pdf = '%PDF-1.4\n';
const offsets: number[] = [];
objects.forEach((obj, idx) => {
offsets.push(pdf.length);
pdf += `${idx + 1} 0 obj\n${obj}\nendobj\n`;
});
const xrefStart = pdf.length;
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
offsets.forEach((o) => {
pdf += `${o.toString().padStart(10, '0')} 00000 n \n`;
});
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF`;
// base64 encode
let bin = '';
for (let i = 0; i < pdf.length; i++) bin += String.fromCharCode(pdf.charCodeAt(i) & 0xff);
return btoa(bin);
}