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:
@@ -8,6 +8,7 @@ import {
|
||||
text,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
import { set } from './catalog';
|
||||
import { collector } from './collector';
|
||||
|
||||
export const location = sqliteTable('location', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -31,6 +32,12 @@ export const collectionItem = sqliteTable(
|
||||
setId: text('set_id')
|
||||
.notNull()
|
||||
.references(() => set.id, { onDelete: 'cascade' }),
|
||||
/**
|
||||
* Who owns this copy. Nullable only for the sake of the migration path -
|
||||
* the app always sets it, and the migration assigned every existing row to
|
||||
* the first collector.
|
||||
*/
|
||||
collectorId: text('collector_id').references(() => collector.id, { onDelete: 'cascade' }),
|
||||
status: text('status', {
|
||||
enum: ['owned', 'built', 'disassembled', 'lent_out', 'sold'],
|
||||
})
|
||||
@@ -54,6 +61,7 @@ export const collectionItem = sqliteTable(
|
||||
},
|
||||
(t) => [
|
||||
index('collection_item_set_idx').on(t.setId),
|
||||
index('collection_item_collector_idx').on(t.collectorId),
|
||||
index('collection_item_status_idx').on(t.status),
|
||||
index('collection_item_location_idx').on(t.locationId),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
/**
|
||||
* A collector - in practice: a child.
|
||||
*
|
||||
* The catalog is shared (a set exists once, no matter who owns it), while
|
||||
* ownership and wishlists belong to one collector. With a single collector the
|
||||
* whole concept stays invisible in the UI; it only appears once a second one
|
||||
* exists, so a one-child household never pays for the feature.
|
||||
*/
|
||||
export const collector = sqliteTable(
|
||||
'collector',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
/** For telling two collectors apart at a glance. */
|
||||
color: text('color', {
|
||||
enum: ['blue', 'green', 'amber', 'rose', 'violet', 'slate'],
|
||||
})
|
||||
.notNull()
|
||||
.default('blue'),
|
||||
birthday: integer('birthday', { mode: 'timestamp' }),
|
||||
notes: text('notes'),
|
||||
sortOrder: integer('sort_order').notNull().default(0),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
},
|
||||
(t) => [index('collector_sort_idx').on(t.sortOrder, t.name)],
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './auth';
|
||||
export * from './catalog';
|
||||
export * from './collector';
|
||||
export * from './collection';
|
||||
export * from './wishlist';
|
||||
export * from './relations';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { relations } from 'drizzle-orm';
|
||||
import { passkey, session, user } from './auth';
|
||||
import { instruction, manufacturer, set, setTag, tag, theme } from './catalog';
|
||||
import { collectionItem, location, missingPart, photo } from './collection';
|
||||
import { collector } from './collector';
|
||||
import { wishlist, wishlistItem } from './wishlist';
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
@@ -73,8 +74,17 @@ export const locationRelations = relations(location, ({ one, many }) => ({
|
||||
collectionItems: many(collectionItem),
|
||||
}));
|
||||
|
||||
export const collectorRelations = relations(collector, ({ many }) => ({
|
||||
collectionItems: many(collectionItem),
|
||||
wishlists: many(wishlist),
|
||||
}));
|
||||
|
||||
export const collectionItemRelations = relations(collectionItem, ({ one, many }) => ({
|
||||
set: one(set, { fields: [collectionItem.setId], references: [set.id] }),
|
||||
collector: one(collector, {
|
||||
fields: [collectionItem.collectorId],
|
||||
references: [collector.id],
|
||||
}),
|
||||
location: one(location, {
|
||||
fields: [collectionItem.locationId],
|
||||
references: [location.id],
|
||||
@@ -100,6 +110,10 @@ export const photoRelations = relations(photo, ({ one }) => ({
|
||||
|
||||
export const wishlistRelations = relations(wishlist, ({ one, many }) => ({
|
||||
owner: one(user, { fields: [wishlist.ownerUserId], references: [user.id] }),
|
||||
collector: one(collector, {
|
||||
fields: [wishlist.collectorId],
|
||||
references: [collector.id],
|
||||
}),
|
||||
items: many(wishlistItem),
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { sql } from 'drizzle-orm';
|
||||
import { check, index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
import { user } from './auth';
|
||||
import { set } from './catalog';
|
||||
import { collector } from './collector';
|
||||
|
||||
export const wishlist = sqliteTable(
|
||||
'wishlist',
|
||||
@@ -10,6 +11,11 @@ export const wishlist = sqliteTable(
|
||||
ownerUserId: text('owner_user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
/**
|
||||
* Whose list this is. Null means "not tied to one collector" - useful
|
||||
* while there is only one, and for a shared list.
|
||||
*/
|
||||
collectorId: text('collector_id').references(() => collector.id, { onDelete: 'set null' }),
|
||||
/** e.g. "Weihnachten 2026". */
|
||||
title: text('title').notNull(),
|
||||
occasion: text('occasion', { enum: ['christmas', 'birthday', 'easter', 'other'] })
|
||||
|
||||
Reference in New Issue
Block a user