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
@@ -8,6 +8,7 @@ import { deleteCollectionItem, updateCollectionItem } from '@/app/(app)/sammlung
import type { MissingPartRow } from '@/lib/queries/collection';
import { MissingPartsDialog } from './missing-parts-dialog';
import type { CollectionStatus, Completeness, Condition } from '@/db/types';
import { collectorBadgeClass } from '@/lib/collector-colors';
import { formatDate, toDateInputValue } from '@/lib/format';
import {
COMPLETENESS_LABELS,
@@ -58,14 +59,19 @@ export type CollectionItemRow = {
notes: string | null;
setName: string;
missingParts: MissingPartRow[];
collectorId: string | null;
collectorName: string | null;
collectorColor: string | null;
};
export function CollectionItemsSection({
items,
locations,
collectors,
}: {
items: CollectionItemRow[];
locations: Array<{ id: string; name: string }>;
collectors: Array<{ id: string; name: string }>;
}) {
const router = useRouter();
const [pending, startTransition] = useTransition();
@@ -126,6 +132,11 @@ export function CollectionItemsSection({
<li key={item.id} className="flex items-start gap-3 p-3">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex flex-wrap items-center gap-2">
{collectors.length > 1 && item.collectorName ? (
<Badge className={collectorBadgeClass(item.collectorColor)}>
{item.collectorName}
</Badge>
) : null}
<Badge variant={item.status === 'sold' ? 'outline' : 'default'}>
{statusLabel(item.status)}
</Badge>
@@ -194,6 +205,22 @@ export function CollectionItemsSection({
<form id="collection-item-form" onSubmit={submit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
{collectors.length > 1 ? (
<Field label="Sammler" htmlFor="collectorId" className="sm:col-span-2">
<NativeSelect
id="collectorId"
name="collectorId"
defaultValue={editing?.collectorId ?? collectors[0]?.id ?? 'none'}
>
{collectors.map((entry) => (
<option key={entry.id} value={entry.id}>
{entry.name}
</option>
))}
</NativeSelect>
</Field>
) : null}
<Field label="Status" htmlFor="status">
<NativeSelect id="status" name="status" defaultValue={editing?.status ?? 'owned'}>
{Object.entries(STATUS_LABELS).map(([value, label]) => (