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
+46
View File
@@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest';
import type { CollectionStatus } from '@/db/types';
import {
countOwnedCopies,
countOwnedCopiesFor,
isOwningStatus,
isSetOwned,
isSetOwnedBy,
OWNED_STATUSES,
parseOwnershipFilter,
} from '@/lib/ownership';
@@ -59,3 +61,47 @@ describe('Besitz-Filter aus der URL', () => {
expect(parseOwnershipFilter('')).toBe('alle');
});
});
describe('Besitz je Sammler', () => {
const copies = [
{ status: 'built' as const, collectorId: 'max' },
{ status: 'owned' as const, collectorId: 'mia' },
{ status: 'sold' as const, collectorId: 'max' },
];
it('zaehlt ohne Sammler alles, was irgendjemand hier hat', () => {
expect(countOwnedCopiesFor(copies)).toBe(2);
expect(isSetOwnedBy(copies)).toBe(true);
});
it('zaehlt mit Sammler nur dessen Exemplare', () => {
expect(countOwnedCopiesFor(copies, 'max')).toBe(1);
expect(countOwnedCopiesFor(copies, 'mia')).toBe(1);
});
it('sieht ein Set als fehlend, wenn nur das Geschwisterkind es hat', () => {
const onlyMia = [{ status: 'built' as const, collectorId: 'mia' }];
expect(isSetOwnedBy(onlyMia, 'mia')).toBe(true);
// Genau der Fall, fuer den es mehrere Sammler gibt.
expect(isSetOwnedBy(onlyMia, 'max')).toBe(false);
});
it('ignoriert verkaufte Exemplare auch je Sammler', () => {
const sold = [{ status: 'sold' as const, collectorId: 'max' }];
expect(isSetOwnedBy(sold, 'max')).toBe(false);
expect(isSetOwnedBy(sold)).toBe(false);
});
it('behandelt einen unbekannten Sammler als "hat nichts"', () => {
expect(countOwnedCopiesFor(copies, 'niemand')).toBe(0);
});
it('kommt mit Exemplaren ohne Sammler klar', () => {
const unassigned = [{ status: 'owned' as const, collectorId: null }];
expect(isSetOwnedBy(unassigned)).toBe(true);
expect(isSetOwnedBy(unassigned, 'max')).toBe(false);
});
});