This commit is contained in:
2026-06-09 21:28:53 +12:00
parent daa6e60a69
commit 349e4a4b5b
61 changed files with 6404 additions and 1382 deletions
@@ -1,23 +1,62 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { api } from '$lib/api';
import MixCalculatorPrintDocument from '$lib/components/MixCalculatorPrintDocument.svelte';
import type { MixCalculatorSession } from '$lib/types';
let { session }: { session: MixCalculatorSession } = $props();
let { session, autoPrint = true }: { session: MixCalculatorSession; autoPrint?: boolean } = $props();
let pdfUrl = $state<string | null>(null);
let loading = $state(true);
let error = $state('');
let printAfterLoad = $state(false);
let pdfFrame = $state<HTMLIFrameElement | null>(null);
const printableTitle = $derived(
`MixCalculator_${session.client_name}_${session.product_name}_${session.mix_date}_${session.session_number}`.replace(/[^\w.-]+/g, '_')
);
async function openPdf() {
const blob = await api.mixCalculatorSessionPdf(session.id);
const url = URL.createObjectURL(blob);
window.open(url, '_blank', 'noopener,noreferrer');
setTimeout(() => URL.revokeObjectURL(url), 60_000);
function revokePdfUrl() {
if (pdfUrl) {
URL.revokeObjectURL(pdfUrl);
pdfUrl = null;
}
}
async function loadPdf() {
loading = true;
error = '';
revokePdfUrl();
try {
const blob = await api.mixCalculatorSessionPdf(session.id);
pdfUrl = URL.createObjectURL(blob);
printAfterLoad = autoPrint;
} catch (loadError) {
error = loadError instanceof Error ? loadError.message : 'Unable to load the PDF preview.';
} finally {
loading = false;
}
}
function printPage() {
if (!pdfUrl) {
printAfterLoad = true;
loadPdf();
return;
}
pdfFrame?.contentWindow?.focus();
pdfFrame?.contentWindow?.print();
}
function handlePreviewLoaded() {
if (printAfterLoad) {
printAfterLoad = false;
requestAnimationFrame(() => printPage());
}
}
async function downloadPdf() {
const blob = await api.mixCalculatorSessionPdf(session.id);
const blob = pdfUrl ? await fetch(pdfUrl).then((response) => response.blob()) : await api.mixCalculatorSessionPdf(session.id);
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
@@ -27,6 +66,14 @@
anchor.remove();
URL.revokeObjectURL(url);
}
onMount(() => {
loadPdf();
});
onDestroy(() => {
revokePdfUrl();
});
</script>
<svelte:head>
@@ -36,11 +83,28 @@
<section class="print-page">
<div class="print-toolbar">
<a class="secondary-button" href={`/mix-calculator/${session.id}`}>Back to session</a>
<button class="primary-button" type="button" onclick={openPdf}>Open Styled PDF</button>
<button class="secondary-button" type="button" onclick={downloadPdf}>Download PDF</button>
<button class="primary-button" type="button" disabled={!pdfUrl && loading} onclick={printPage}>Print</button>
<button class="secondary-button" type="button" disabled={!pdfUrl && loading} onclick={downloadPdf}>Download PDF</button>
</div>
<MixCalculatorPrintDocument session={session} generatedAt={new Date().toISOString()} />
<div class="pdf-preview-shell">
{#if loading}
<div class="preview-state">Loading PDF preview...</div>
{:else if error}
<div class="preview-state error">
<strong>PDF preview unavailable</strong>
<span>{error}</span>
<button class="secondary-button" type="button" onclick={loadPdf}>Retry</button>
</div>
{:else if pdfUrl}
<iframe
bind:this={pdfFrame}
src={pdfUrl}
title={`${printableTitle} PDF preview`}
onload={handlePreviewLoaded}
></iframe>
{/if}
</div>
</section>
<style>
@@ -57,6 +121,7 @@
display: flex;
justify-content: flex-end;
gap: 0.75rem;
width: min(100%, 210mm);
}
.primary-button,
@@ -82,6 +147,52 @@
color: #304038;
}
button:disabled {
cursor: wait;
opacity: 0.65;
}
.pdf-preview-shell {
width: min(100%, 210mm);
aspect-ratio: 210 / 297;
border: 1px solid var(--line-strong);
border-radius: 0.75rem;
background: #fff;
overflow: hidden;
box-shadow: 0 18px 50px rgba(25, 35, 30, 0.16);
}
iframe {
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
.preview-state {
display: grid;
place-items: center;
gap: 0.75rem;
height: 100%;
padding: 2rem;
color: var(--color-text-secondary);
text-align: center;
font-weight: 600;
}
.preview-state.error {
align-content: center;
}
.preview-state strong,
.preview-state span {
display: block;
}
.preview-state strong {
color: var(--color-text-primary);
}
@media (max-width: 900px) {
.print-toolbar {
justify-content: stretch;
@@ -93,16 +204,9 @@
}
@media print {
:global(body) {
background: #fff;
}
.print-page {
padding: 0;
background: #fff;
}
.print-toolbar {
.print-page,
.print-toolbar,
.pdf-preview-shell {
display: none;
}
}