"use client"; import { type EstimateExportArtifactPayload, EstimateExportFormat, } from "@planarchy/shared"; import type { EstimateExportView, EstimateVersionView, EstimateWorkspaceView, } from "~/components/estimates/EstimateWorkspace.types.js"; import { formatDateLong, formatMoney } from "~/lib/format.js"; const EXPORT_FORMATS: EstimateExportFormat[] = [ EstimateExportFormat.XLSX, EstimateExportFormat.CSV, EstimateExportFormat.JSON, EstimateExportFormat.SAP, EstimateExportFormat.MMP, ]; function formatBytes(value: number | null | undefined) { const bytes = value ?? 0; if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function isEstimateExportArtifactPayload( payload: EstimateExportView["payload"], ): payload is EstimateExportArtifactPayload { return ( typeof payload === "object" && payload !== null && typeof payload.content === "string" && typeof payload.mimeType === "string" && typeof payload.encoding === "string" && typeof payload.fileExtension === "string" && typeof payload.generatedAt === "string" && typeof payload.byteLength === "number" && typeof payload.summary === "object" && payload.summary !== null ); } function decodeBase64(value: string) { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; } function downloadEstimateExport(estimateExport: EstimateExportView) { const payload = estimateExport.payload; if (!isEstimateExportArtifactPayload(payload)) { return; } const blob = payload.encoding === "base64" ? new Blob([decodeBase64(payload.content)], { type: payload.mimeType }) : new Blob([payload.content], { type: payload.mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = estimateExport.fileName; document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); } export interface ExportsTabProps { estimate: EstimateWorkspaceView; canEdit: boolean; onCreateExport: (versionId: string, format: EstimateExportFormat) => void; isCreatingExport: boolean; } export function ExportsTab({ estimate, canEdit, onCreateExport, isCreatingExport, }: ExportsTabProps) { const versions = estimate.versions as EstimateVersionView[]; const latestVersion = versions[0] ?? null; const exports = latestVersion?.exports ?? []; return (

Export delivery

Generate format-specific artifacts from the current version and download them directly from the stored serializer payload.

{latestVersion && canEdit && (
{EXPORT_FORMATS.map((format) => ( ))}
)}

Generated exports

{exports.length === 0 ? (

No exports have been generated for the current version yet.

) : (
{exports.map((estimateExport) => { const payload = isEstimateExportArtifactPayload(estimateExport.payload) ? estimateExport.payload : null; return (

{estimateExport.fileName}

{estimateExport.format} {payload?.sheetNames?.length ? ( {payload.sheetNames.length} sheets ) : null}
{formatDateLong(estimateExport.createdAt)} {payload ? {formatBytes(payload.byteLength)} : null} {payload?.rowCount != null ? {payload.rowCount} rows : null} {payload?.lineCount != null ? {payload.lineCount} lines : null}
{payload ? (

Hours

{payload.summary.totalHours.toFixed(1)}

Cost

{formatMoney( payload.summary.totalCostCents, payload.summary.baseCurrency, )}

Price

{formatMoney( payload.summary.totalPriceCents, payload.summary.baseCurrency, )}

Margin

{payload.summary.marginPercent.toFixed(0)}%

) : (

Legacy export record detected. Regenerate it to get downloadable serializer output.

)}
{payload ? ( ) : null}
{payload?.previewText ? (
                      {payload.previewText}
                    
) : null}
); })}
)}
); }