kammer/src/lib/components/LocationTree.svelte
Christopher Mühl 1ffdb937ce feat: all route pages — dashboard, scan, items, locations, labels, settings
- 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>
2026-02-26 16:22:19 +01:00

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' : ''}"
>&#9654;</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>