Files
data-entry-app/frontend/src/lib/components/MixCalculatorPrintSheet.svelte
T

322 lines
6.7 KiB
Svelte
Raw Normal View History

2026-04-29 23:05:27 +12:00
<script lang="ts">
import type { MixCalculatorSession } from '$lib/types';
let { session }: { session: MixCalculatorSession } = $props();
function formatDate(value: string) {
return new Intl.DateTimeFormat('en-NZ', {
dateStyle: 'medium',
timeStyle: undefined
}).format(new Date(value));
}
function formatTimestamp(value: string) {
return new Intl.DateTimeFormat('en-NZ', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(value));
}
function formatNumber(value: number, digits = 2) {
return value.toFixed(digits);
}
const printableTitle = $derived(
`MixCalculator_${session.client_name}_${session.product_name}_${session.mix_date}_${session.session_number}`.replace(/[^\w.-]+/g, '_')
);
</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>
<button class="primary-button" type="button" onclick={() => window.print()}>Print / Save PDF</button>
</div>
<article class="sheet">
<header class="sheet-header">
<div>
<p class="eyebrow">Mix Calculator</p>
<h1>{session.session_number}</h1>
<p>Generated from the saved session snapshot without re-reading live product or recipe data.</p>
</div>
<div class="sheet-meta">
<div>
<span>Generated</span>
<strong>{formatTimestamp(new Date().toISOString())}</strong>
</div>
<div>
<span>Status</span>
<strong>{session.status}</strong>
</div>
</div>
</header>
<section class="summary-grid">
<div>
<span>Date</span>
<strong>{formatDate(session.mix_date)}</strong>
</div>
<div>
<span>Client</span>
<strong>{session.client_name}</strong>
</div>
<div>
<span>Product</span>
<strong>{session.product_name}</strong>
</div>
<div>
<span>Mix source</span>
<strong>{session.mix_name}</strong>
</div>
<div>
<span>Batch size</span>
<strong>{formatNumber(session.batch_size_kg, 2)}kg</strong>
</div>
<div>
<span>Total bags</span>
<strong>{formatNumber(session.total_bags, 2)}</strong>
</div>
<div>
<span>Total kilograms</span>
<strong>{formatNumber(session.total_kg, 2)}kg</strong>
</div>
<div>
<span>Prepared by</span>
<strong>{session.prepared_by_name}</strong>
</div>
</section>
{#if session.notes}
<section class="notes-card">
<h2>Session notes</h2>
<p>{session.notes}</p>
</section>
{/if}
{#if session.warnings.length}
<section class="warning-card">
<h2>Warnings</h2>
{#each session.warnings as warning}
<p>{warning}</p>
{/each}
</section>
{/if}
<section class="table-card">
<div class="table-header">
<h2>Required raw materials</h2>
<span>{session.product_unit_of_measure} · {formatNumber(session.product_unit_size_kg, 2)}kg per unit</span>
</div>
<table>
<thead>
<tr>
<th>Raw material</th>
<th>Mix %</th>
<th>Required kg</th>
<th>Unit</th>
</tr>
</thead>
<tbody>
{#each session.lines as line}
<tr>
<td>{line.raw_material_name}</td>
<td>{formatNumber(line.mix_percentage, 2)}%</td>
<td>{formatNumber(line.required_kg, 2)}kg</td>
<td>{line.unit}</td>
</tr>
{/each}
</tbody>
</table>
</section>
</article>
</section>
<style>
h1,
h2,
p {
margin: 0;
}
.print-page {
display: grid;
gap: 1rem;
}
.print-toolbar {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.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;
background: linear-gradient(135deg, var(--green) 0%, var(--green-deep) 100%);
color: #fff;
}
.secondary-button {
background: #fff;
color: #304038;
}
.sheet {
width: min(960px, 100%);
margin: 0 auto;
padding: 2rem;
border: 1px solid var(--line);
border-radius: 1.5rem;
background: #fff;
box-shadow: var(--shadow);
}
.eyebrow {
color: #7d8d84;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.sheet-header {
display: flex;
justify-content: space-between;
gap: 1.5rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--line);
}
.sheet-header h1 {
margin: 0.3rem 0 0.45rem;
font-size: clamp(2rem, 4vw, 2.6rem);
}
.sheet-header p:last-child,
.sheet-meta span,
.summary-grid span,
.table-header span {
color: var(--muted);
}
.sheet-meta {
min-width: 14rem;
display: grid;
gap: 0.9rem;
}
.sheet-meta div,
.summary-grid div {
display: grid;
gap: 0.16rem;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
padding: 1.35rem 0;
}
.notes-card,
.warning-card,
.table-card {
margin-top: 1rem;
}
.notes-card,
.warning-card {
padding: 1rem;
border-radius: 1rem;
}
.notes-card {
background: var(--panel-soft);
}
.warning-card {
background: #fff6e6;
color: #8b5b1e;
}
.warning-card h2,
.notes-card h2,
.table-header h2 {
margin-bottom: 0.45rem;
font-size: 1rem;
}
.table-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.88rem 0.75rem;
text-align: left;
border-bottom: 1px solid var(--line);
}
th {
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
@media (max-width: 900px) {
.sheet-header,
.table-header {
flex-direction: column;
}
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media print {
:global(body) {
background: #fff;
}
.print-toolbar {
display: none;
}
.sheet {
width: 100%;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
box-shadow: none;
}
}
</style>