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
@@ -0,0 +1,87 @@
'use client';
import {
createCollector,
deleteCollector,
updateCollector,
} from '@/app/(app)/verwaltung/sammler/actions';
import { COLLECTOR_COLOR_LABELS } from '@/lib/collector-colors';
import { toDateInputValue } from '@/lib/format';
import { CrudSection, type CrudItem } from './crud-section';
import { Field } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { NativeSelect } from '@/components/ui/native-select';
import { Textarea } from '@/components/ui/textarea';
export type CollectorRowItem = CrudItem & {
color: string;
birthday: Date | null;
notes: string | null;
itemCount: number;
};
export function CollectorList({ collectors }: { collectors: CollectorRowItem[] }) {
return (
<CrudSection<CollectorRowItem>
items={collectors}
emptyText="Noch kein Sammler angelegt."
createLabel="Sammler anlegen"
dialogTitle={(item) => (item ? 'Sammler bearbeiten' : 'Neuer Sammler')}
dialogDescription="Der Katalog bleibt gemeinsam – Sammlung und Wunschlisten gehören je einem Sammler."
onCreate={createCollector}
onUpdate={updateCollector}
onDelete={deleteCollector}
deleteWarning={(item) =>
item.itemCount > 0
? `„${item.title}“ hat ${item.itemCount} Sets in der Sammlung. Ordne sie erst jemand anderem zu – erst dann lässt sich der Sammler löschen.`
: `„${item.title}“ wird gelöscht. Der Katalog bleibt unverändert.`
}
renderFields={(item) => (
<>
<Field label="Name" htmlFor="name" required>
<Input
id="name"
name="name"
defaultValue={item?.title ?? ''}
required
maxLength={60}
placeholder="Max"
/>
</Field>
<Field
label="Farbe"
htmlFor="color"
hint="Nur zur Unterscheidung auf einen Blick."
>
<NativeSelect id="color" name="color" defaultValue={item?.color ?? 'blue'}>
{Object.entries(COLLECTOR_COLOR_LABELS).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</NativeSelect>
</Field>
<Field label="Geburtstag" htmlFor="birthday" hint="Optional.">
<Input
id="birthday"
name="birthday"
type="date"
defaultValue={toDateInputValue(item?.birthday)}
/>
</Field>
<Field label="Notizen" htmlFor="collector-notes">
<Textarea
id="collector-notes"
name="notes"
defaultValue={item?.notes ?? ''}
rows={3}
/>
</Field>
</>
)}
/>
);
}