Spaces:
Running
Running
| /** | |
| * 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); | |
| } | |