v0.1.12
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user