feat(sammler): mehrere sammler mit eigener sammlung und wunschliste

Der Katalog bleibt gemeinsam - ein Set existiert einmal, unabhaengig
davon, wer es hat. Besitz und Wunschlisten gehoeren dagegen je einem
Sammler. Damit beantwortet "hat er das schon?" jetzt die Frage "hat *er*
das schon?": ein Set, das nur die Schwester besitzt, faellt fuer den
Bruder wieder unter "fehlt noch" und darf auf seine Wunschliste.

Wichtig fuer den Alltag: Solange es nur einen Sammler gibt, taucht die
Unterscheidung nirgends in der Oberflaeche auf. Kein Umschalter, keine
Abfrage beim Hinzufuegen, keine Badges - die App bleibt exakt so
einfach wie vorher. Erst der zweite Sammler schaltet all das frei.

Die Besitzlogik liegt weiterhin an genau einer Stelle
(src/lib/ownership.ts) und hat nur eine Dimension dazubekommen. Das
automatische "erfuellt" auf Wunschlisten fragt jetzt ebenfalls je Liste:
ein Wunsch auf Max' Liste ist erfuellt, wenn Max das Set bekommt - nicht
wenn Mia es hat. Listen ohne Sammler verhalten sich wie bisher.

Die Migration legt einen Sammler "Sammlung" an und ordnet ihm alles
Vorhandene zu; umbenennen laesst er sich unter Verwaltung -> Sammler.
Der Seed uebernimmt ihn statt einen dritten anzulegen.

Refs Phase 3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
unknown
2026-09-07 09:00:35 +02:00
co-authored by Claude Opus 5
parent 7265c03548
commit c23b754515
35 changed files with 3104 additions and 66 deletions
+25 -3
View File
@@ -1,10 +1,12 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Boxes, Euro, ImageOff, Layers, PackageSearch } from 'lucide-react';
import { CollectorFilter } from '@/components/layout/collector-filter';
import { CollectionFilters } from '@/components/sammlung/collection-filters';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { formatDate, formatNumber, pluralize } from '@/lib/format';
import { collectorBadgeClass } from '@/lib/collector-colors';
import { setImageSrc } from '@/lib/images';
import { completenessLabel, statusLabel } from '@/lib/labels';
import { formatEuro } from '@/lib/money';
@@ -15,6 +17,7 @@ import {
listCollectionItems,
type CollectionFilters as Filters,
} from '@/lib/queries/collection';
import { listCollectors, resolveCollectorId } from '@/lib/queries/collectors';
import { listLocations, listManufacturers } from '@/lib/queries/master-data';
import { requireUser } from '@/lib/session';
import type { CollectionStatus, Completeness } from '@/db/types';
@@ -32,18 +35,23 @@ export default async function SammlungPage({ searchParams }: { searchParams: Sea
await requireUser();
const params = await searchParams;
const collectors = listCollectors();
const collectorId = resolveCollectorId(single(params.sammler));
const activeCollector = collectors.find((row) => row.id === collectorId) ?? null;
const filters: Filters = {
q: single(params.q),
status: single(params.status) as CollectionStatus | undefined,
completeness: single(params.zustand) as Completeness | undefined,
locationId: single(params.standort),
manufacturerId: single(params.hersteller),
collectorId,
};
const items = listCollectionItems(filters);
const openParts = countOpenMissingParts(items.map((item) => item.id));
const stats = getCollectionStats();
const missing = countMissingSets();
const stats = getCollectionStats(collectorId);
const missing = countMissingSets(collectorId);
const tiles = [
{ icon: Boxes, label: 'Sets', value: formatNumber(stats.setCount) },
@@ -56,9 +64,18 @@ export default async function SammlungPage({ searchParams }: { searchParams: Sea
<div className="space-y-5">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Sammlung</h1>
<p className="text-sm text-muted-foreground">Alles, was tatsächlich da ist.</p>
<p className="text-sm text-muted-foreground">
{activeCollector
? `Alles, was ${activeCollector.name} tatsächlich hat.`
: 'Alles, was tatsächlich da ist.'}
</p>
</div>
<CollectorFilter
collectors={collectors.map((row) => ({ id: row.id, name: row.name, color: row.color }))}
allLabel="Alle zusammen"
/>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{tiles.map((tile) => (
<Card key={tile.label}>
@@ -134,6 +151,11 @@ export default async function SammlungPage({ searchParams }: { searchParams: Sea
{item.completeness !== 'unknown' ? (
<Badge variant="secondary">{completenessLabel(item.completeness)}</Badge>
) : null}
{collectors.length > 1 && item.collectorName ? (
<Badge className={collectorBadgeClass(item.collectorColor)}>
{item.collectorName}
</Badge>
) : null}
{openParts.get(item.id) ? (
<Badge variant="outline">{openParts.get(item.id)} Fehlteile</Badge>
) : null}