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)}
|
||||
<!-- Third layer: child row links to its own page; chevron
|
||||
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}>
|
||||
<a
|
||||
class="rail-row rail-group-link"
|
||||
@@ -356,6 +357,7 @@
|
||||
href={child.href}
|
||||
onclick={() => openSubGroup(key)}
|
||||
>
|
||||
<span class="rail-icon"><ChildIcon size={18} strokeWidth={1.75} /></span>
|
||||
<span class="rail-text">{child.label}</span>
|
||||
{#if child.badge}<span class="rail-badge">{child.badge}</span>{/if}
|
||||
</a>
|
||||
@@ -374,12 +376,12 @@
|
||||
{#if subOpen}
|
||||
<div class="rail-children rail-subchildren">
|
||||
{#each child.children as grandchild}
|
||||
{@render leafLink(grandchild, false)}
|
||||
{@render leafLink(grandchild, true)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{@render leafLink(child, false)}
|
||||
{@render leafLink(child, true)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -251,68 +251,6 @@
|
||||
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;
|
||||
}
|
||||
@@ -459,7 +397,6 @@
|
||||
|
||||
.add-cell:nth-child(2),
|
||||
.add-cell:nth-child(3),
|
||||
.add-dest,
|
||||
.add-action {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -490,7 +427,6 @@
|
||||
}
|
||||
|
||||
.add-cell:nth-child(2),
|
||||
.add-dest,
|
||||
.add-action {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
formatNumber,
|
||||
packedMain,
|
||||
packedDetail,
|
||||
destinationOf,
|
||||
onApplyFilters,
|
||||
onClearFilters,
|
||||
onToggleSort,
|
||||
@@ -59,7 +58,6 @@
|
||||
formatNumber: (value: number | null | undefined, digits?: number) => string;
|
||||
packedMain: (entry: ThroughputEntry) => string;
|
||||
packedDetail: (entry: ThroughputEntry) => string;
|
||||
destinationOf: (entry: ThroughputEntry) => { label: string; detail: string | null };
|
||||
onApplyFilters: () => void;
|
||||
onClearFilters: () => void;
|
||||
onToggleSort: (key: SortKey) => void;
|
||||
@@ -171,10 +169,6 @@
|
||||
<span>Packed by</span>
|
||||
<ArrowUpDown size={14} strokeWidth={2.1} />
|
||||
</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')}>
|
||||
<span>Notes</span>
|
||||
<ArrowUpDown size={14} strokeWidth={2.1} />
|
||||
@@ -194,7 +188,6 @@
|
||||
{/each}
|
||||
{:else}
|
||||
{#each paginatedEntries as entry (entry.id)}
|
||||
{@const dest = destinationOf(entry)}
|
||||
<div class="row" class:just-added={entry.id === highlightId}>
|
||||
<span class="col-date">
|
||||
<span class="cell-label">Date</span>
|
||||
@@ -217,16 +210,6 @@
|
||||
<span class="cell-label">Packed by</span>
|
||||
{entry.staff_name ?? '—'}
|
||||
</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">
|
||||
<button
|
||||
type="button"
|
||||
@@ -442,7 +425,7 @@
|
||||
.log-head,
|
||||
.row {
|
||||
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;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -494,7 +477,6 @@
|
||||
}
|
||||
|
||||
.col-product,
|
||||
.col-dest,
|
||||
.col-packed,
|
||||
.col-total {
|
||||
display: flex;
|
||||
@@ -509,15 +491,13 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.dest-detail,
|
||||
.packed-detail {
|
||||
font-size: 0.88rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.total-kg,
|
||||
.packed-main,
|
||||
.dest-detail {
|
||||
.packed-main {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -591,32 +571,6 @@
|
||||
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 {
|
||||
padding: 1.15rem 1.45rem;
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
@@ -741,7 +695,7 @@
|
||||
@media (min-width: 1280px) {
|
||||
.log-head,
|
||||
.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 {
|
||||
@@ -749,7 +703,7 @@
|
||||
}
|
||||
|
||||
.row-notes {
|
||||
grid-column: 7;
|
||||
grid-column: 6;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
padding-top: 0;
|
||||
@@ -758,7 +712,7 @@
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
grid-column: 8;
|
||||
grid-column: 7;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { toast } from '$lib/toast';
|
||||
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.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 { TableController } from '$lib/table/table.svelte';
|
||||
import { formatNumber } from '$lib/format';
|
||||
@@ -297,7 +298,7 @@
|
||||
</label>
|
||||
<label>
|
||||
<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>
|
||||
<span>Rounding</span>
|
||||
@@ -367,7 +368,7 @@
|
||||
|
||||
<div class="cell">
|
||||
<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 class="cell">
|
||||
@@ -443,12 +444,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<datalist id="ingredient-categories">
|
||||
{#each knownCategories as category (category)}
|
||||
<option value={category}></option>
|
||||
{/each}
|
||||
</datalist>
|
||||
|
||||
{#if historyIngredient}
|
||||
<ChangeHistoryModal
|
||||
entityType="ingredient"
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
buildConfetti,
|
||||
compareDate,
|
||||
compareText,
|
||||
isStockEntry,
|
||||
startOfWeekMonday,
|
||||
toISODate,
|
||||
type SortDirection,
|
||||
@@ -70,24 +69,6 @@
|
||||
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 ──────────────────────────────
|
||||
const today = toISODate(ausToday());
|
||||
let nDate = $state(today);
|
||||
@@ -495,10 +476,6 @@
|
||||
result = (a.calculated_kg ?? 0) - (b.calculated_kg ?? 0);
|
||||
} else if (sortKey === 'staff') {
|
||||
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') {
|
||||
result = compareText(a.notes, b.notes);
|
||||
}
|
||||
@@ -582,7 +559,6 @@
|
||||
{formatNumber}
|
||||
{packedMain}
|
||||
{packedDetail}
|
||||
{destinationOf}
|
||||
onApplyFilters={applyFilters}
|
||||
onClearFilters={clearFilters}
|
||||
onToggleSort={toggleSort}
|
||||
|
||||
Reference in New Issue
Block a user