v0.1.32 - Mix Calculator search, ingredient categories, throughput tidy-up
Mix Calculator: searchable Mix Name picker (mirrors Throughput search) Ingredients Editor: add manual Category column; used to order Mix Calculator output Mix Calculator: surface formula-only mixes (no product yet) via -mix_id sentinel Throughput: remove unused For order / For stock destination controls from composer Editor change history: show timestamps in local time (stored UTC) instead of raw UTC
This commit is contained in:
@@ -0,0 +1,301 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import { Check, Plus, Search } from 'lucide-svelte';
|
||||||
|
|
||||||
|
// A searchable category picker that also lets you create a new category inline.
|
||||||
|
// The value is just the category string — picking an existing option keeps
|
||||||
|
// spelling consistent, while "Create" commits whatever was typed. The menu is
|
||||||
|
// rendered fixed-position so it escapes the ingredients table's clipped
|
||||||
|
// (overflow:hidden) scroll container.
|
||||||
|
let {
|
||||||
|
value = $bindable(''),
|
||||||
|
options = [],
|
||||||
|
placeholder = 'Category',
|
||||||
|
inputId,
|
||||||
|
disabled = false,
|
||||||
|
ariaLabel = 'Category'
|
||||||
|
}: {
|
||||||
|
value?: string;
|
||||||
|
options?: string[];
|
||||||
|
placeholder?: string;
|
||||||
|
inputId?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let highlighted = $state(-1);
|
||||||
|
let root = $state<HTMLDivElement | null>(null);
|
||||||
|
let inputEl = $state<HTMLInputElement | null>(null);
|
||||||
|
let menuStyle = $state('');
|
||||||
|
|
||||||
|
const query = $derived(value.trim());
|
||||||
|
|
||||||
|
const filtered = $derived.by(() => {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
if (!q) return options;
|
||||||
|
return options.filter((option) => option.toLowerCase().includes(q));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Offer "Create" only when the typed text doesn't already exist (case-insensitive).
|
||||||
|
const exactExists = $derived(options.some((option) => option.toLowerCase() === query.toLowerCase()));
|
||||||
|
const showCreate = $derived(query.length > 0 && !exactExists);
|
||||||
|
|
||||||
|
// Total selectable rows = filtered options, plus the optional create row last.
|
||||||
|
const rowCount = $derived(filtered.length + (showCreate ? 1 : 0));
|
||||||
|
|
||||||
|
function positionMenu() {
|
||||||
|
if (!inputEl) return;
|
||||||
|
const rect = inputEl.getBoundingClientRect();
|
||||||
|
menuStyle = `top: ${rect.bottom + 4}px; left: ${rect.left}px; width: ${Math.max(rect.width, 200)}px;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openMenu() {
|
||||||
|
if (disabled) return;
|
||||||
|
open = true;
|
||||||
|
highlighted = -1;
|
||||||
|
await tick();
|
||||||
|
positionMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(option: string) {
|
||||||
|
value = option;
|
||||||
|
open = false;
|
||||||
|
highlighted = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitTyped() {
|
||||||
|
value = query;
|
||||||
|
open = false;
|
||||||
|
highlighted = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(event: Event) {
|
||||||
|
value = (event.target as HTMLInputElement).value;
|
||||||
|
open = true;
|
||||||
|
highlighted = -1;
|
||||||
|
positionMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
open = true;
|
||||||
|
highlighted = Math.min(highlighted + 1, rowCount - 1);
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
highlighted = Math.max(highlighted - 1, 0);
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
if (open && highlighted >= 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (highlighted < filtered.length) choose(filtered[highlighted]);
|
||||||
|
else if (showCreate) commitTyped();
|
||||||
|
} else {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
open = false;
|
||||||
|
highlighted = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusOut(event: FocusEvent) {
|
||||||
|
if (root && event.relatedTarget instanceof Node && root.contains(event.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
open = false;
|
||||||
|
highlighted = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the fixed menu glued to the input while scrolling/resizing.
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const handler = () => positionMenu();
|
||||||
|
window.addEventListener('scroll', handler, true);
|
||||||
|
window.addEventListener('resize', handler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handler, true);
|
||||||
|
window.removeEventListener('resize', handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="combo" bind:this={root} onfocusout={onFocusOut}>
|
||||||
|
<span class="combo-icon" aria-hidden="true"><Search size={15} strokeWidth={2.2} /></span>
|
||||||
|
<input
|
||||||
|
id={inputId}
|
||||||
|
bind:this={inputEl}
|
||||||
|
class="combo-input"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
{placeholder}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded={open}
|
||||||
|
role="combobox"
|
||||||
|
aria-controls={inputId ? `${inputId}-list` : undefined}
|
||||||
|
value={value}
|
||||||
|
{disabled}
|
||||||
|
oninput={onInput}
|
||||||
|
onfocus={openMenu}
|
||||||
|
onkeydown={onKeydown}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if open && !disabled}
|
||||||
|
<ul class="menu" id={inputId ? `${inputId}-list` : undefined} role="listbox" style={menuStyle}>
|
||||||
|
{#each filtered as option, i (option)}
|
||||||
|
<li
|
||||||
|
class="row"
|
||||||
|
class:highlighted={i === highlighted}
|
||||||
|
class:selected={option.toLowerCase() === query.toLowerCase()}
|
||||||
|
role="option"
|
||||||
|
aria-selected={option.toLowerCase() === query.toLowerCase()}
|
||||||
|
onmousedown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
choose(option);
|
||||||
|
}}
|
||||||
|
onmouseenter={() => (highlighted = i)}
|
||||||
|
>
|
||||||
|
<span class="row-label">{option}</span>
|
||||||
|
{#if option.toLowerCase() === query.toLowerCase()}
|
||||||
|
<span class="row-check" aria-hidden="true"><Check size={14} strokeWidth={2.6} /></span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if showCreate}
|
||||||
|
<li
|
||||||
|
class="row create"
|
||||||
|
class:highlighted={highlighted === filtered.length}
|
||||||
|
role="option"
|
||||||
|
aria-selected={false}
|
||||||
|
onmousedown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
commitTyped();
|
||||||
|
}}
|
||||||
|
onmouseenter={() => (highlighted = filtered.length)}
|
||||||
|
>
|
||||||
|
<span class="create-icon" aria-hidden="true"><Plus size={14} strokeWidth={2.6} /></span>
|
||||||
|
<span class="row-label">Create “{query}”</span>
|
||||||
|
</li>
|
||||||
|
{:else if filtered.length === 0}
|
||||||
|
<li class="row empty">Type to add a category.</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.combo {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Match the editor's compact inputs (the parent's scoped `input` rule can't
|
||||||
|
reach this child component). */
|
||||||
|
.combo-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0.38rem 0.5rem 0.38rem 1.65rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.42rem;
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-input::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-input:hover {
|
||||||
|
border-color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-input:focus-visible {
|
||||||
|
outline: 3px solid var(--color-brand);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: var(--color-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-input:disabled {
|
||||||
|
background: color-mix(in srgb, var(--color-bg-app) 70%, var(--color-bg-surface));
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 300;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25rem;
|
||||||
|
list-style: none;
|
||||||
|
max-height: 16rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.42rem 0.55rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.highlighted {
|
||||||
|
background: var(--color-brand-tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.selected {
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.empty {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-check {
|
||||||
|
color: var(--color-brand);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.create {
|
||||||
|
color: var(--color-brand);
|
||||||
|
font-weight: 650;
|
||||||
|
border-top: 1px solid var(--color-divider);
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -349,6 +349,7 @@
|
|||||||
{@const subActive = subGroupActive(child)}
|
{@const subActive = subGroupActive(child)}
|
||||||
<!-- Third layer: child row links to its own page; chevron
|
<!-- Third layer: child row links to its own page; chevron
|
||||||
reveals the nested submenu (e.g. Integrations → Xero). -->
|
reveals the nested submenu (e.g. Integrations → Xero). -->
|
||||||
|
{@const ChildIcon = child.icon}
|
||||||
<div class="rail-group-head rail-subgroup-head" class:within-active={subActive && !subOpen}>
|
<div class="rail-group-head rail-subgroup-head" class:within-active={subActive && !subOpen}>
|
||||||
<a
|
<a
|
||||||
class="rail-row rail-group-link"
|
class="rail-row rail-group-link"
|
||||||
@@ -356,6 +357,7 @@
|
|||||||
href={child.href}
|
href={child.href}
|
||||||
onclick={() => openSubGroup(key)}
|
onclick={() => openSubGroup(key)}
|
||||||
>
|
>
|
||||||
|
<span class="rail-icon"><ChildIcon size={18} strokeWidth={1.75} /></span>
|
||||||
<span class="rail-text">{child.label}</span>
|
<span class="rail-text">{child.label}</span>
|
||||||
{#if child.badge}<span class="rail-badge">{child.badge}</span>{/if}
|
{#if child.badge}<span class="rail-badge">{child.badge}</span>{/if}
|
||||||
</a>
|
</a>
|
||||||
@@ -374,12 +376,12 @@
|
|||||||
{#if subOpen}
|
{#if subOpen}
|
||||||
<div class="rail-children rail-subchildren">
|
<div class="rail-children rail-subchildren">
|
||||||
{#each child.children as grandchild}
|
{#each child.children as grandchild}
|
||||||
{@render leafLink(grandchild, false)}
|
{@render leafLink(grandchild, true)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{@render leafLink(child, false)}
|
{@render leafLink(child, true)}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -251,68 +251,6 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
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 {
|
.add-action {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -459,7 +397,6 @@
|
|||||||
|
|
||||||
.add-cell:nth-child(2),
|
.add-cell:nth-child(2),
|
||||||
.add-cell:nth-child(3),
|
.add-cell:nth-child(3),
|
||||||
.add-dest,
|
|
||||||
.add-action {
|
.add-action {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
@@ -490,7 +427,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-cell:nth-child(2),
|
.add-cell:nth-child(2),
|
||||||
.add-dest,
|
|
||||||
.add-action {
|
.add-action {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
formatNumber,
|
formatNumber,
|
||||||
packedMain,
|
packedMain,
|
||||||
packedDetail,
|
packedDetail,
|
||||||
destinationOf,
|
|
||||||
onApplyFilters,
|
onApplyFilters,
|
||||||
onClearFilters,
|
onClearFilters,
|
||||||
onToggleSort,
|
onToggleSort,
|
||||||
@@ -59,7 +58,6 @@
|
|||||||
formatNumber: (value: number | null | undefined, digits?: number) => string;
|
formatNumber: (value: number | null | undefined, digits?: number) => string;
|
||||||
packedMain: (entry: ThroughputEntry) => string;
|
packedMain: (entry: ThroughputEntry) => string;
|
||||||
packedDetail: (entry: ThroughputEntry) => string;
|
packedDetail: (entry: ThroughputEntry) => string;
|
||||||
destinationOf: (entry: ThroughputEntry) => { label: string; detail: string | null };
|
|
||||||
onApplyFilters: () => void;
|
onApplyFilters: () => void;
|
||||||
onClearFilters: () => void;
|
onClearFilters: () => void;
|
||||||
onToggleSort: (key: SortKey) => void;
|
onToggleSort: (key: SortKey) => void;
|
||||||
@@ -171,10 +169,6 @@
|
|||||||
<span>Packed by</span>
|
<span>Packed by</span>
|
||||||
<ArrowUpDown size={14} strokeWidth={2.1} />
|
<ArrowUpDown size={14} strokeWidth={2.1} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="sort-head" class:active={sortKey === 'destination'} onclick={() => onToggleSort('destination')}>
|
|
||||||
<span>Destination</span>
|
|
||||||
<ArrowUpDown size={14} strokeWidth={2.1} />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="sort-head col-notes-head" class:active={sortKey === 'notes'} onclick={() => onToggleSort('notes')}>
|
<button type="button" class="sort-head col-notes-head" class:active={sortKey === 'notes'} onclick={() => onToggleSort('notes')}>
|
||||||
<span>Notes</span>
|
<span>Notes</span>
|
||||||
<ArrowUpDown size={14} strokeWidth={2.1} />
|
<ArrowUpDown size={14} strokeWidth={2.1} />
|
||||||
@@ -194,7 +188,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each paginatedEntries as entry (entry.id)}
|
{#each paginatedEntries as entry (entry.id)}
|
||||||
{@const dest = destinationOf(entry)}
|
|
||||||
<div class="row" class:just-added={entry.id === highlightId}>
|
<div class="row" class:just-added={entry.id === highlightId}>
|
||||||
<span class="col-date">
|
<span class="col-date">
|
||||||
<span class="cell-label">Date</span>
|
<span class="cell-label">Date</span>
|
||||||
@@ -217,16 +210,6 @@
|
|||||||
<span class="cell-label">Packed by</span>
|
<span class="cell-label">Packed by</span>
|
||||||
{entry.staff_name ?? '—'}
|
{entry.staff_name ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
<span class="col-dest">
|
|
||||||
<span class="cell-label">Destination</span>
|
|
||||||
<span
|
|
||||||
class="pill"
|
|
||||||
class:pill-stock={dest.label === 'Stock'}
|
|
||||||
class:pill-order={dest.label === 'Order'}
|
|
||||||
class:pill-split={dest.label === 'Split'}
|
|
||||||
>{dest.label}</span>
|
|
||||||
{#if dest.detail}<span class="dest-detail">{dest.detail}</span>{/if}
|
|
||||||
</span>
|
|
||||||
<span class="col-actions">
|
<span class="col-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -442,7 +425,7 @@
|
|||||||
.log-head,
|
.log-head,
|
||||||
.row {
|
.row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 8rem minmax(0, 1.1fr) minmax(0, 0.95fr) 6rem minmax(0, 0.8fr) 10.5rem 4.8rem;
|
grid-template-columns: 8rem minmax(0, 1.1fr) minmax(0, 0.95fr) 6rem minmax(0, 0.8fr) 4.8rem;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -494,7 +477,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-product,
|
.col-product,
|
||||||
.col-dest,
|
|
||||||
.col-packed,
|
.col-packed,
|
||||||
.col-total {
|
.col-total {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -509,15 +491,13 @@
|
|||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dest-detail,
|
|
||||||
.packed-detail {
|
.packed-detail {
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-kg,
|
.total-kg,
|
||||||
.packed-main,
|
.packed-main {
|
||||||
.dest-detail {
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,32 +571,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.42rem 0.78rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
font-weight: 650;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-stock {
|
|
||||||
background: #e8f1fc;
|
|
||||||
color: #0b5cad;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-order {
|
|
||||||
background: var(--color-brand-tint);
|
|
||||||
color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-split {
|
|
||||||
background: #f3e8fc;
|
|
||||||
color: #6b21a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-skeleton {
|
.row-skeleton {
|
||||||
padding: 1.15rem 1.45rem;
|
padding: 1.15rem 1.45rem;
|
||||||
border-bottom: 1px solid var(--color-divider);
|
border-bottom: 1px solid var(--color-divider);
|
||||||
@@ -741,7 +695,7 @@
|
|||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
.log-head,
|
.log-head,
|
||||||
.row {
|
.row {
|
||||||
grid-template-columns: 8rem minmax(0, 1.1fr) minmax(0, 0.95fr) 6rem minmax(0, 0.8fr) 10.5rem minmax(0, 1.2fr) 4.8rem;
|
grid-template-columns: 8rem minmax(0, 1.1fr) minmax(0, 0.95fr) 6rem minmax(0, 0.8fr) minmax(0, 1.2fr) 4.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-notes-head {
|
.col-notes-head {
|
||||||
@@ -749,7 +703,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row-notes {
|
.row-notes {
|
||||||
grid-column: 7;
|
grid-column: 6;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
@@ -758,7 +712,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-actions {
|
.col-actions {
|
||||||
grid-column: 8;
|
grid-column: 7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { toast } from '$lib/toast';
|
import { toast } from '$lib/toast';
|
||||||
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.svelte';
|
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.svelte';
|
||||||
import ChangeHistoryModal from '$lib/components/editor/ChangeHistoryModal.svelte';
|
import ChangeHistoryModal from '$lib/components/editor/ChangeHistoryModal.svelte';
|
||||||
|
import CategoryCombobox from '$lib/components/editor/CategoryCombobox.svelte';
|
||||||
import SortHeader from '$lib/table/SortHeader.svelte';
|
import SortHeader from '$lib/table/SortHeader.svelte';
|
||||||
import { TableController } from '$lib/table/table.svelte';
|
import { TableController } from '$lib/table/table.svelte';
|
||||||
import { formatNumber } from '$lib/format';
|
import { formatNumber } from '$lib/format';
|
||||||
@@ -297,7 +298,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Category</span>
|
<span>Category</span>
|
||||||
<input bind:value={newIngredient.category} list="ingredient-categories" placeholder="e.g. Grains" />
|
<CategoryCombobox bind:value={newIngredient.category} options={knownCategories} placeholder="e.g. Grains" inputId="new-ingredient-category" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Rounding</span>
|
<span>Rounding</span>
|
||||||
@@ -367,7 +368,7 @@
|
|||||||
|
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<span class="cell-label">Category</span>
|
<span class="cell-label">Category</span>
|
||||||
<input bind:value={row.draft_category} list="ingredient-categories" placeholder="—" aria-label="Category" />
|
<CategoryCombobox bind:value={row.draft_category} options={knownCategories} placeholder="—" ariaLabel={`Category for ${row.name}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -443,12 +444,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<datalist id="ingredient-categories">
|
|
||||||
{#each knownCategories as category (category)}
|
|
||||||
<option value={category}></option>
|
|
||||||
{/each}
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
{#if historyIngredient}
|
{#if historyIngredient}
|
||||||
<ChangeHistoryModal
|
<ChangeHistoryModal
|
||||||
entityType="ingredient"
|
entityType="ingredient"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
buildConfetti,
|
buildConfetti,
|
||||||
compareDate,
|
compareDate,
|
||||||
compareText,
|
compareText,
|
||||||
isStockEntry,
|
|
||||||
startOfWeekMonday,
|
startOfWeekMonday,
|
||||||
toISODate,
|
toISODate,
|
||||||
type SortDirection,
|
type SortDirection,
|
||||||
@@ -70,24 +69,6 @@
|
|||||||
statsEntries = statsEntries.filter((e) => e.id !== id);
|
statsEntries = statsEntries.filter((e) => e.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The destination shown in the log: an order (with job number), stock, or a
|
|
||||||
// split across both.
|
|
||||||
function destinationOf(entry: ThroughputEntry): { label: string; detail: string | null } {
|
|
||||||
const unit = entry.quantity_type === 'bags' ? 'bags' : 'kg';
|
|
||||||
if (entry.for_order && entry.for_stock) {
|
|
||||||
const stock = entry.stock_quantity != null ? `${formatNumber(entry.stock_quantity, 1)} ${unit} to stock` : 'split';
|
|
||||||
const job = entry.job_number ? `Order ${entry.job_number}` : 'Order';
|
|
||||||
return { label: 'Split', detail: `${job} · ${stock}` };
|
|
||||||
}
|
|
||||||
if (entry.for_order) {
|
|
||||||
return { label: 'Order', detail: entry.job_number ? `Job ${entry.job_number}` : null };
|
|
||||||
}
|
|
||||||
if (isStockEntry(entry)) {
|
|
||||||
return { label: 'Stock', detail: null };
|
|
||||||
}
|
|
||||||
return { label: '—', detail: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Inline "spreadsheet" add row ──────────────────────────────
|
// ── Inline "spreadsheet" add row ──────────────────────────────
|
||||||
const today = toISODate(ausToday());
|
const today = toISODate(ausToday());
|
||||||
let nDate = $state(today);
|
let nDate = $state(today);
|
||||||
@@ -495,10 +476,6 @@
|
|||||||
result = (a.calculated_kg ?? 0) - (b.calculated_kg ?? 0);
|
result = (a.calculated_kg ?? 0) - (b.calculated_kg ?? 0);
|
||||||
} else if (sortKey === 'staff') {
|
} else if (sortKey === 'staff') {
|
||||||
result = compareText(a.staff_name, b.staff_name);
|
result = compareText(a.staff_name, b.staff_name);
|
||||||
} else if (sortKey === 'destination') {
|
|
||||||
const aDest = destinationOf(a);
|
|
||||||
const bDest = destinationOf(b);
|
|
||||||
result = compareText(`${aDest.label} ${aDest.detail ?? ''}`, `${bDest.label} ${bDest.detail ?? ''}`);
|
|
||||||
} else if (sortKey === 'notes') {
|
} else if (sortKey === 'notes') {
|
||||||
result = compareText(a.notes, b.notes);
|
result = compareText(a.notes, b.notes);
|
||||||
}
|
}
|
||||||
@@ -582,7 +559,6 @@
|
|||||||
{formatNumber}
|
{formatNumber}
|
||||||
{packedMain}
|
{packedMain}
|
||||||
{packedDetail}
|
{packedDetail}
|
||||||
{destinationOf}
|
|
||||||
onApplyFilters={applyFilters}
|
onApplyFilters={applyFilters}
|
||||||
onClearFilters={clearFilters}
|
onClearFilters={clearFilters}
|
||||||
onToggleSort={toggleSort}
|
onToggleSort={toggleSort}
|
||||||
|
|||||||
Reference in New Issue
Block a user