- Dashboard with stats grid, checked-out/low-stock/recent items - Scan route with check-in/out flow - Items list with search, filters (category/type/custody), sorting - Item detail with full metadata, check-in/out, edit, delete - New item form wired to inventory store - Locations browser with tree view and items-at-location - Labels page with batch generation and PDF download - Settings page with DB stats - Fixed Svelte 5 event modifier syntax (no pipe modifiers) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
92 lines
2.5 KiB
Svelte
92 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
import type { Location } from '$lib/types';
|
|
import { inventory } from '$lib/stores/inventory.svelte';
|
|
|
|
let {
|
|
onselect,
|
|
selectedId = null,
|
|
}: {
|
|
onselect?: (location: Location) => void;
|
|
selectedId?: string | null;
|
|
} = $props();
|
|
|
|
let expanded = $state<Set<string>>(new Set(['home']));
|
|
|
|
const tree = $derived(inventory.getLocationTree());
|
|
const roots = $derived(tree.get(null) ?? []);
|
|
|
|
function toggle(id: string) {
|
|
const next = new Set(expanded);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
expanded = next;
|
|
}
|
|
|
|
function getItemCount(locationId: string): number {
|
|
return inventory.items.filter((i) => i.lastSeenAt === locationId).length;
|
|
}
|
|
|
|
function getChildren(parentId: string): Location[] {
|
|
return (tree.get(parentId) ?? []).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
}
|
|
|
|
function hasChildren(id: string): boolean {
|
|
return (tree.get(id) ?? []).length > 0;
|
|
}
|
|
</script>
|
|
|
|
{#snippet locationNode(location: Location, depth: number)}
|
|
{@const children = getChildren(location.id)}
|
|
{@const itemCount = getItemCount(location.id)}
|
|
{@const isExpanded = expanded.has(location.id)}
|
|
{@const isSelected = selectedId === location.id}
|
|
|
|
<div class="select-none" style="padding-left: {depth * 16}px">
|
|
<button
|
|
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left
|
|
min-h-[44px] transition-colors
|
|
{isSelected ? 'bg-blue-500/20 text-blue-400' : 'hover:bg-slate-700 text-white'}"
|
|
onclick={() => onselect?.(location)}
|
|
>
|
|
<!-- Expand/collapse toggle -->
|
|
{#if hasChildren(location.id)}
|
|
<button
|
|
class="w-5 h-5 flex items-center justify-center text-slate-400 hover:text-white shrink-0"
|
|
onclick={(e) => { e.stopPropagation(); toggle(location.id); }}
|
|
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
>
|
|
<span class="text-xs transition-transform {isExpanded ? 'rotate-90' : ''}"
|
|
>▶</span
|
|
>
|
|
</button>
|
|
{:else}
|
|
<span class="w-5 shrink-0"></span>
|
|
{/if}
|
|
|
|
<span class="flex-1 truncate text-sm">{location.name}</span>
|
|
|
|
{#if itemCount > 0}
|
|
<span class="text-xs text-slate-500 tabular-nums">{itemCount}</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
|
|
{#if isExpanded && children.length > 0}
|
|
{#each children as child}
|
|
{@render locationNode(child, depth + 1)}
|
|
{/each}
|
|
{/if}
|
|
{/snippet}
|
|
|
|
<div class="space-y-0.5">
|
|
{#each roots as root}
|
|
{@render locationNode(root, 0)}
|
|
{/each}
|
|
|
|
{#if roots.length === 0}
|
|
<p class="text-sm text-slate-500 text-center py-4">No locations configured</p>
|
|
{/if}
|
|
</div>
|