Mix calculator
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user