2026-04-29 23:05:27 +12:00
|
|
|
<script lang="ts">
|
2026-06-09 21:28:53 +12:00
|
|
|
import { onDestroy, onMount } from 'svelte';
|
2026-05-08 23:07:01 +12:00
|
|
|
import { api } from '$lib/api';
|
2026-04-29 23:05:27 +12:00
|
|
|
import type { MixCalculatorSession } from '$lib/types';
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
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);
|
2026-04-29 23:05:27 +12:00
|
|
|
|
|
|
|
|
const printableTitle = $derived(
|
|
|
|
|
`MixCalculator_${session.client_name}_${session.product_name}_${session.mix_date}_${session.session_number}`.replace(/[^\w.-]+/g, '_')
|
|
|
|
|
);
|
2026-05-08 23:07:01 +12:00
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
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());
|
|
|
|
|
}
|
2026-05-31 20:19:44 +12:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
async function downloadPdf() {
|
2026-06-09 21:28:53 +12:00
|
|
|
const blob = pdfUrl ? await fetch(pdfUrl).then((response) => response.blob()) : await api.mixCalculatorSessionPdf(session.id);
|
2026-05-08 23:07:01 +12:00
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const anchor = document.createElement('a');
|
|
|
|
|
anchor.href = url;
|
|
|
|
|
anchor.download = `${printableTitle}.pdf`;
|
|
|
|
|
document.body.appendChild(anchor);
|
|
|
|
|
anchor.click();
|
|
|
|
|
anchor.remove();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
2026-06-09 21:28:53 +12:00
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
loadPdf();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
|
revokePdfUrl();
|
|
|
|
|
});
|
2026-04-29 23:05:27 +12:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
<title>{printableTitle}</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<section class="print-page">
|
|
|
|
|
<div class="print-toolbar">
|
|
|
|
|
<a class="secondary-button" href={`/mix-calculator/${session.id}`}>Back to session</a>
|
2026-06-09 21:28:53 +12:00
|
|
|
<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>
|
2026-04-29 23:05:27 +12:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
<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>
|
2026-04-29 23:05:27 +12:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.print-page {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 1rem;
|
2026-05-08 23:07:01 +12:00
|
|
|
justify-items: center;
|
|
|
|
|
padding: 1.5rem 1rem 2.5rem;
|
|
|
|
|
background:
|
|
|
|
|
linear-gradient(180deg, #eef4f0 0%, #e6eee9 100%);
|
2026-04-29 23:05:27 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.print-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 0.75rem;
|
2026-06-09 21:28:53 +12:00
|
|
|
width: min(100%, 210mm);
|
2026-04-29 23:05:27 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary-button,
|
|
|
|
|
.secondary-button {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 0.78rem 0.96rem;
|
|
|
|
|
border-radius: 0.9rem;
|
|
|
|
|
border: 1px solid var(--line-strong);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary-button {
|
|
|
|
|
border: none;
|
2026-05-08 09:06:14 +12:00
|
|
|
background: var(--color-brand);
|
2026-04-29 23:05:27 +12:00
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secondary-button {
|
|
|
|
|
background: #fff;
|
|
|
|
|
color: #304038;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 23:05:27 +12:00
|
|
|
@media (max-width: 900px) {
|
2026-05-08 23:07:01 +12:00
|
|
|
.print-toolbar {
|
|
|
|
|
justify-content: stretch;
|
2026-04-29 23:05:27 +12:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
.print-toolbar > :global(*) {
|
|
|
|
|
flex: 1 1 auto;
|
2026-04-29 23:05:27 +12:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media print {
|
2026-06-09 21:28:53 +12:00
|
|
|
.print-page,
|
|
|
|
|
.print-toolbar,
|
|
|
|
|
.pdf-preview-shell {
|
2026-05-08 23:07:01 +12:00
|
|
|
display: none;
|
2026-04-29 23:05:27 +12:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|