"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 (
Generate format-specific artifacts from the current version and download them directly from the stored serializer payload.
No exports have been generated for the current version yet.
{estimateExport.fileName}
{estimateExport.format} {payload?.sheetNames?.length ? ( {payload.sheetNames.length} sheets ) : null}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.previewText}
) : null}