Release v0.1.23
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
<script lang="ts">
|
||||
import { Plus, TriangleAlert, X } from 'lucide-svelte';
|
||||
|
||||
import ThroughputProductPicker from '$lib/components/throughput/ThroughputProductPicker.svelte';
|
||||
import type { ThroughputProduct, ThroughputQuantityType } from '$lib/types';
|
||||
|
||||
let {
|
||||
products,
|
||||
editingId,
|
||||
saving,
|
||||
nDate = $bindable(''),
|
||||
nProductId = $bindable(''),
|
||||
nQuantity = $bindable(''),
|
||||
nType = $bindable<ThroughputQuantityType>('bags'),
|
||||
nBagSize = $bindable(''),
|
||||
nStaff = $bindable(''),
|
||||
nNotes = $bindable(''),
|
||||
nForOrder = $bindable(false),
|
||||
nForStock = $bindable(false),
|
||||
nJobNumber = $bindable(''),
|
||||
nStockQty = $bindable(''),
|
||||
showNote = $bindable(false),
|
||||
addError,
|
||||
isSplit,
|
||||
addTotalKg,
|
||||
formatNumber,
|
||||
onSubmit,
|
||||
onDismissNote,
|
||||
onCancelEdit,
|
||||
composerRef = $bindable<HTMLElement | null>(null)
|
||||
}: {
|
||||
products: ThroughputProduct[];
|
||||
editingId: number | null;
|
||||
saving: boolean;
|
||||
nDate?: string;
|
||||
nProductId?: string;
|
||||
nQuantity?: string;
|
||||
nType?: ThroughputQuantityType;
|
||||
nBagSize?: string;
|
||||
nStaff?: string;
|
||||
nNotes?: string;
|
||||
nForOrder?: boolean;
|
||||
nForStock?: boolean;
|
||||
nJobNumber?: string;
|
||||
nStockQty?: string;
|
||||
showNote?: boolean;
|
||||
addError: string;
|
||||
isSplit: boolean;
|
||||
addTotalKg: number | null;
|
||||
formatNumber: (value: number | null | undefined, digits?: number) => string;
|
||||
onSubmit: () => void;
|
||||
onDismissNote: () => void;
|
||||
onCancelEdit: () => void;
|
||||
composerRef?: HTMLElement | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="composer" class:editing={editingId != null} bind:this={composerRef}>
|
||||
<div class="composer-head">
|
||||
<div class="composer-title">
|
||||
<h2>{editingId != null ? 'Edit packing run' : 'Add a packing run'}</h2>
|
||||
</div>
|
||||
{#if editingId != null}
|
||||
<button type="button" class="cancel-edit" onclick={onCancelEdit}>
|
||||
<X size={16} strokeWidth={2.4} /> Cancel edit
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<form class="add-row" onsubmit={(e) => { e.preventDefault(); onSubmit(); }}>
|
||||
<div class="add-cell">
|
||||
<span class="cell-label">Date</span>
|
||||
<input type="date" bind:value={nDate} aria-label="Production date" />
|
||||
</div>
|
||||
<div class="add-cell add-product">
|
||||
<span class="cell-label">Product</span>
|
||||
<ThroughputProductPicker {products} bind:productId={nProductId} inputId="throughput-add-product" />
|
||||
</div>
|
||||
<div class="add-cell">
|
||||
<span class="cell-label">Packed</span>
|
||||
<div class="packed-inputs">
|
||||
<input
|
||||
class="qty"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
bind:value={nQuantity}
|
||||
placeholder={nType === 'bags' ? 'Bags' : 'Total kg'}
|
||||
aria-label={nType === 'bags' ? 'Number of bags' : 'Total kilograms'}
|
||||
/>
|
||||
<select class="unit" bind:value={nType} aria-label="Bags or kilograms">
|
||||
<option value="bags">bags</option>
|
||||
<option value="kg">kg (bulka)</option>
|
||||
</select>
|
||||
{#if nType === 'bags'}
|
||||
<span class="times" aria-hidden="true">×</span>
|
||||
<input
|
||||
class="bag"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
bind:value={nBagSize}
|
||||
placeholder="kg/bag"
|
||||
aria-label="Kilograms per bag"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if nType === 'bags' && addTotalKg !== null}
|
||||
<span class="packed-total">= {formatNumber(addTotalKg)} kg total</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="add-cell">
|
||||
<span class="cell-label">Packed by</span>
|
||||
<input type="text" bind:value={nStaff} placeholder="Name" aria-label="Packed by" />
|
||||
</div>
|
||||
<div class="add-cell add-dest">
|
||||
<span class="cell-label">Destination</span>
|
||||
<div class="dest-rows">
|
||||
<div class="dest-line">
|
||||
<label class="dest-toggle" class:on={nForOrder}>
|
||||
<input type="checkbox" bind:checked={nForOrder} /> For an order
|
||||
</label>
|
||||
{#if nForOrder}
|
||||
<input
|
||||
class="dest-input"
|
||||
type="text"
|
||||
bind:value={nJobNumber}
|
||||
placeholder="Job number (Order Circle)"
|
||||
aria-label="Job number"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dest-line">
|
||||
<label class="dest-toggle" class:on={nForStock}>
|
||||
<input type="checkbox" bind:checked={nForStock} /> For stock
|
||||
</label>
|
||||
{#if isSplit}
|
||||
<input
|
||||
class="dest-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
bind:value={nStockQty}
|
||||
placeholder={`To stock (${nType === 'bags' ? 'bags' : 'kg'})`}
|
||||
aria-label="Amount going to stock"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-cell add-action">
|
||||
<button type="submit" class="add-entry-button" disabled={saving}>
|
||||
<Plus size={18} strokeWidth={2.6} />
|
||||
<span>{saving ? 'Saving…' : editingId != null ? 'Save' : 'Add'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="add-extra">
|
||||
{#if showNote}
|
||||
<div class="note-field">
|
||||
<input
|
||||
class="note-input"
|
||||
type="text"
|
||||
bind:value={nNotes}
|
||||
placeholder="Note (optional)"
|
||||
aria-label="Note"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="note-dismiss"
|
||||
title="Remove note"
|
||||
aria-label="Remove note"
|
||||
onclick={onDismissNote}
|
||||
>
|
||||
<X size={16} strokeWidth={2.4} />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="button" class="link-button" onclick={() => (showNote = true)}>+ Add a note</button>
|
||||
{/if}
|
||||
{#if addError}
|
||||
<span class="add-error"><TriangleAlert size={15} strokeWidth={2.4} /> {addError}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.composer {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-brand) 16%, var(--color-border));
|
||||
border-radius: 1rem;
|
||||
background: var(--color-bg-surface);
|
||||
box-shadow: 0 16px 34px -28px rgba(15, 23, 42, 0.28);
|
||||
/* The product picker menu is absolutely positioned and needs to escape the
|
||||
card bounds without being cut off. */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.composer-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.45rem 0;
|
||||
}
|
||||
|
||||
.composer-title h2 {
|
||||
margin: 0;
|
||||
font-size: 1.22rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.add-row {
|
||||
display: grid;
|
||||
grid-template-columns: 7.5rem minmax(19rem, 1.7fr) minmax(14rem, 1.15fr) minmax(7rem, 0.65fr) minmax(14rem, 1.2fr) auto;
|
||||
gap: 0.75rem 0.85rem;
|
||||
align-items: start;
|
||||
padding: 0 1.45rem 1.25rem;
|
||||
}
|
||||
|
||||
.add-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-row input:not([type='checkbox']),
|
||||
.add-row select {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0.62rem 0.78rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-brand) 14%, var(--color-border));
|
||||
border-radius: 0.8rem;
|
||||
font-size: 0.98rem;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
.add-row input:not([type='checkbox']):focus-visible,
|
||||
.add-row select:focus-visible,
|
||||
.cancel-edit:focus-visible,
|
||||
.add-entry-button:focus-visible,
|
||||
.note-dismiss:focus-visible {
|
||||
outline: 3px solid var(--color-brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.packed-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.packed-inputs .qty {
|
||||
flex: 1 1 4.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.packed-inputs .unit {
|
||||
flex: 0 0 4.7rem;
|
||||
width: 4.7rem;
|
||||
}
|
||||
|
||||
.packed-inputs .bag {
|
||||
flex: 0 0 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.packed-inputs .times {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.packed-total {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 650;
|
||||
color: var(--color-success);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.add-dest {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.dest-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.dest-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dest-line .dest-toggle {
|
||||
flex: 0 0 auto;
|
||||
min-width: 8.5rem;
|
||||
}
|
||||
|
||||
.dest-line .dest-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.dest-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.68rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-brand) 14%, var(--color-border));
|
||||
border-radius: 0.72rem;
|
||||
background: var(--color-bg-surface);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dest-toggle input {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--color-brand);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dest-toggle.on {
|
||||
border-color: var(--color-brand);
|
||||
background: var(--color-brand-tint);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.dest-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-action {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-entry-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 48px;
|
||||
padding: 0.62rem 1.28rem;
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
border: 1px solid var(--color-brand);
|
||||
border-radius: 0.8rem;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.add-entry-button:hover:not(:disabled) {
|
||||
background: #126a33;
|
||||
}
|
||||
|
||||
.add-entry-button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.add-extra {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 0.15rem;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
padding: 0.25rem 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--color-success);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.note-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1 1 16rem;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.note-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.note-dismiss,
|
||||
.cancel-edit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-surface);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-dismiss {
|
||||
flex-shrink: 0;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
padding: 0;
|
||||
border-radius: 0.55rem;
|
||||
transition: border-color 140ms ease, color 140ms ease, background-color 140ms ease;
|
||||
}
|
||||
|
||||
.cancel-edit {
|
||||
gap: 0.35rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 0.6rem;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.note-dismiss:hover,
|
||||
.cancel-edit:hover {
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-text-muted);
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.add-error {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
color: #8a1622;
|
||||
font-weight: 650;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
display: none;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.composer.editing {
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: 0 0 0 1px var(--color-brand) inset;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.add-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem 1rem;
|
||||
}
|
||||
|
||||
.add-action {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.add-entry-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.add-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.add-cell:nth-child(2),
|
||||
.add-cell:nth-child(3),
|
||||
.add-dest,
|
||||
.add-action {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.add-entry-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.composer-head {
|
||||
padding-left: 1.15rem;
|
||||
padding-right: 1.15rem;
|
||||
}
|
||||
|
||||
.composer-title h2 {
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
|
||||
.add-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.85rem 1rem;
|
||||
padding: 0 1.15rem 1.15rem;
|
||||
}
|
||||
|
||||
.add-cell .cell-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.add-cell:nth-child(2),
|
||||
.add-dest,
|
||||
.add-action {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.add-entry-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user