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:
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* The small colour palette that tells collectors apart.
|
||||
*
|
||||
* Fixed classes rather than computed ones, because Tailwind only ships the
|
||||
* classes it can see in the source.
|
||||
*/
|
||||
export const COLLECTOR_COLOR_LABELS: Record<string, string> = {
|
||||
blue: 'Blau',
|
||||
green: 'Grün',
|
||||
amber: 'Gelb',
|
||||
rose: 'Rot',
|
||||
violet: 'Violett',
|
||||
slate: 'Grau',
|
||||
};
|
||||
|
||||
const BADGE_CLASSES: Record<string, string> = {
|
||||
blue: 'bg-blue-100 text-blue-900 dark:bg-blue-950 dark:text-blue-200',
|
||||
green: 'bg-emerald-100 text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200',
|
||||
amber: 'bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-200',
|
||||
rose: 'bg-rose-100 text-rose-900 dark:bg-rose-950 dark:text-rose-200',
|
||||
violet: 'bg-violet-100 text-violet-900 dark:bg-violet-950 dark:text-violet-200',
|
||||
slate: 'bg-slate-200 text-slate-900 dark:bg-slate-800 dark:text-slate-100',
|
||||
};
|
||||
|
||||
export function collectorBadgeClass(color: string | null | undefined): string {
|
||||
return BADGE_CLASSES[color ?? 'blue'] ?? BADGE_CLASSES.blue!;
|
||||
}
|
||||
|
||||
const DOT_CLASSES: Record<string, string> = {
|
||||
blue: 'bg-blue-500',
|
||||
green: 'bg-emerald-500',
|
||||
amber: 'bg-amber-500',
|
||||
rose: 'bg-rose-500',
|
||||
violet: 'bg-violet-500',
|
||||
slate: 'bg-slate-500',
|
||||
};
|
||||
|
||||
export function collectorDotClass(color: string | null | undefined): string {
|
||||
return DOT_CLASSES[color ?? 'blue'] ?? DOT_CLASSES.blue!;
|
||||
}
|
||||
+65
-8
@@ -8,8 +8,9 @@ import type { CollectionStatus } from '@/db/types';
|
||||
* status is anything but `sold`. A sold copy is history, not ownership - so a
|
||||
* set that was sold shows up under "fehlt" again and can go back on a wishlist.
|
||||
*
|
||||
* Every query that asks about ownership goes through this module. If the rule
|
||||
* ever changes, it changes here.
|
||||
* With more than one collector the question gains a "for whom": the catalog is
|
||||
* shared, ownership is not. Every query that asks about ownership goes through
|
||||
* this module, optionally scoped to one collector.
|
||||
*/
|
||||
|
||||
export const OWNED_STATUSES = ['owned', 'built', 'disassembled', 'lent_out'] as const;
|
||||
@@ -18,6 +19,12 @@ export const SOLD_STATUS: CollectionStatus = 'sold';
|
||||
|
||||
export type OwnedStatus = (typeof OWNED_STATUSES)[number];
|
||||
|
||||
/** One owned copy, as far as ownership is concerned. */
|
||||
export type OwnableCopy = {
|
||||
status: CollectionStatus;
|
||||
collectorId?: string | null;
|
||||
};
|
||||
|
||||
/** Does a single collection item count towards ownership? */
|
||||
export function isOwningStatus(status: CollectionStatus): status is OwnedStatus {
|
||||
return status !== SOLD_STATUS;
|
||||
@@ -37,22 +44,72 @@ export function countOwnedCopies(statuses: readonly CollectionStatus[]): number
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL counterpart of `countOwnedCopies`, as a correlated subquery.
|
||||
* Ownership from the point of view of one collector.
|
||||
*
|
||||
* Without a collector the question is "does anyone here have it", which is
|
||||
* what a single-collector household asks anyway.
|
||||
*/
|
||||
export function countOwnedCopiesFor(
|
||||
copies: readonly OwnableCopy[],
|
||||
collectorId?: string | null,
|
||||
): number {
|
||||
return copies.filter(
|
||||
(copy) =>
|
||||
isOwningStatus(copy.status) &&
|
||||
(collectorId === undefined || collectorId === null || copy.collectorId === collectorId),
|
||||
).length;
|
||||
}
|
||||
|
||||
export function isSetOwnedBy(
|
||||
copies: readonly OwnableCopy[],
|
||||
collectorId?: string | null,
|
||||
): boolean {
|
||||
return countOwnedCopiesFor(copies, collectorId) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL counterpart of `countOwnedCopiesFor`, as a correlated subquery.
|
||||
*
|
||||
* A subquery instead of a join, because a set owned twice must not appear
|
||||
* twice in the catalog list.
|
||||
*/
|
||||
export function ownedCopiesSql(setIdColumn: SQL | unknown): SQL<number> {
|
||||
export function ownedCopiesSql(
|
||||
setIdColumn: SQL | unknown,
|
||||
collectorId?: string | null,
|
||||
): SQL<number> {
|
||||
if (!collectorId) {
|
||||
return sql<number>`(
|
||||
select count(*) from collection_item ci
|
||||
where ci.set_id = ${setIdColumn} and ci.status <> ${SOLD_STATUS}
|
||||
)`;
|
||||
}
|
||||
|
||||
return sql<number>`(
|
||||
select count(*) from collection_item ci
|
||||
where ci.set_id = ${setIdColumn} and ci.status <> ${SOLD_STATUS}
|
||||
where ci.set_id = ${setIdColumn}
|
||||
and ci.status <> ${SOLD_STATUS}
|
||||
and ci.collector_id = ${collectorId}
|
||||
)`;
|
||||
}
|
||||
|
||||
/** How many wishlists this set is currently on. */
|
||||
export function wishlistCountSql(setIdColumn: SQL | unknown): SQL<number> {
|
||||
/**
|
||||
* How many wishlists this set is on. Scoped to a collector where one is
|
||||
* selected - a set on the other child's list is not "already wished for" here.
|
||||
*/
|
||||
export function wishlistCountSql(
|
||||
setIdColumn: SQL | unknown,
|
||||
collectorId?: string | null,
|
||||
): SQL<number> {
|
||||
if (!collectorId) {
|
||||
return sql<number>`(
|
||||
select count(*) from wishlist_item wi where wi.set_id = ${setIdColumn}
|
||||
)`;
|
||||
}
|
||||
|
||||
return sql<number>`(
|
||||
select count(*) from wishlist_item wi where wi.set_id = ${setIdColumn}
|
||||
select count(*) from wishlist_item wi
|
||||
join wishlist w on w.id = wi.wishlist_id
|
||||
where wi.set_id = ${setIdColumn} and w.collector_id = ${collectorId}
|
||||
)`;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export type CatalogFilters = {
|
||||
ownership?: OwnershipFilter;
|
||||
/** Hides sets that are already on this wishlist (for "aus fehlenden Sets hinzufügen"). */
|
||||
excludeWishlistId?: string;
|
||||
/** Scopes "owned" and "on a wishlist" to one collector. */
|
||||
collectorId?: string;
|
||||
page?: number;
|
||||
};
|
||||
|
||||
@@ -52,7 +54,7 @@ function buildWhere(filters: CatalogFilters): SQL | undefined {
|
||||
if (filters.themeId) conditions.push(eq(set.themeId, filters.themeId));
|
||||
if (filters.year !== undefined) conditions.push(eq(set.year, filters.year));
|
||||
|
||||
const owned = ownedCopiesSql(set.id);
|
||||
const owned = ownedCopiesSql(set.id, filters.collectorId);
|
||||
switch (filters.ownership) {
|
||||
case 'sammlung':
|
||||
conditions.push(sql`${owned} > 0`);
|
||||
@@ -61,7 +63,7 @@ function buildWhere(filters: CatalogFilters): SQL | undefined {
|
||||
conditions.push(sql`${owned} = 0`);
|
||||
break;
|
||||
case 'wunschliste':
|
||||
conditions.push(sql`${wishlistCountSql(set.id)} > 0`);
|
||||
conditions.push(sql`${wishlistCountSql(set.id, filters.collectorId)} > 0`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -95,8 +97,8 @@ export function listSets(filters: CatalogFilters): { rows: CatalogRow[]; total:
|
||||
retailPriceCents: set.retailPriceCents,
|
||||
manufacturerName: manufacturer.name,
|
||||
themeName: theme.name,
|
||||
ownedCopies: ownedCopiesSql(set.id).mapWith(Number),
|
||||
wishlistCount: wishlistCountSql(set.id).mapWith(Number),
|
||||
ownedCopies: ownedCopiesSql(set.id, filters.collectorId).mapWith(Number),
|
||||
wishlistCount: wishlistCountSql(set.id, filters.collectorId).mapWith(Number),
|
||||
})
|
||||
.from(set)
|
||||
.innerJoin(manufacturer, eq(manufacturer.id, set.manufacturerId))
|
||||
@@ -126,7 +128,7 @@ export function listSetIds(filters: CatalogFilters): string[] {
|
||||
.map((row) => row.id);
|
||||
}
|
||||
|
||||
export function getSetById(id: string) {
|
||||
export function getSetById(id: string, collectorId?: string) {
|
||||
const row = db
|
||||
.select({
|
||||
id: set.id,
|
||||
@@ -148,8 +150,8 @@ export function getSetById(id: string) {
|
||||
notes: set.notes,
|
||||
createdAt: set.createdAt,
|
||||
updatedAt: set.updatedAt,
|
||||
ownedCopies: ownedCopiesSql(set.id).mapWith(Number),
|
||||
wishlistCount: wishlistCountSql(set.id).mapWith(Number),
|
||||
ownedCopies: ownedCopiesSql(set.id, collectorId).mapWith(Number),
|
||||
wishlistCount: wishlistCountSql(set.id, collectorId).mapWith(Number),
|
||||
})
|
||||
.from(set)
|
||||
.innerJoin(manufacturer, eq(manufacturer.id, set.manufacturerId))
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import 'server-only';
|
||||
import { and, asc, countDistinct, eq, inArray, sql, type SQL } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { collectionItem, location, manufacturer, missingPart, set, theme } from '@/db/schema';
|
||||
import { SOLD_STATUS } from '@/lib/ownership';
|
||||
import {
|
||||
collectionItem,
|
||||
collector,
|
||||
location,
|
||||
manufacturer,
|
||||
missingPart,
|
||||
set,
|
||||
theme,
|
||||
} from '@/db/schema';
|
||||
import { ownedCopiesSql, SOLD_STATUS } from '@/lib/ownership';
|
||||
import type { CollectionStatus, Completeness, Condition } from '@/db/types';
|
||||
|
||||
export type CollectionFilters = {
|
||||
@@ -11,6 +19,7 @@ export type CollectionFilters = {
|
||||
completeness?: Completeness;
|
||||
locationId?: string;
|
||||
manufacturerId?: string;
|
||||
collectorId?: string;
|
||||
};
|
||||
|
||||
export type CollectionRow = {
|
||||
@@ -21,6 +30,9 @@ export type CollectionRow = {
|
||||
condition: Condition | null;
|
||||
locationId: string | null;
|
||||
locationName: string | null;
|
||||
collectorId: string | null;
|
||||
collectorName: string | null;
|
||||
collectorColor: string | null;
|
||||
acquiredAt: Date | null;
|
||||
acquiredPriceCents: number | null;
|
||||
acquiredFrom: string | null;
|
||||
@@ -51,6 +63,7 @@ function buildWhere(filters: CollectionFilters): SQL | undefined {
|
||||
if (filters.completeness) conditions.push(eq(collectionItem.completeness, filters.completeness));
|
||||
if (filters.locationId) conditions.push(eq(collectionItem.locationId, filters.locationId));
|
||||
if (filters.manufacturerId) conditions.push(eq(set.manufacturerId, filters.manufacturerId));
|
||||
if (filters.collectorId) conditions.push(eq(collectionItem.collectorId, filters.collectorId));
|
||||
|
||||
return conditions.length > 0 ? and(...conditions) : undefined;
|
||||
}
|
||||
@@ -65,6 +78,9 @@ export function listCollectionItems(filters: CollectionFilters): CollectionRow[]
|
||||
condition: collectionItem.condition,
|
||||
locationId: collectionItem.locationId,
|
||||
locationName: location.name,
|
||||
collectorId: collectionItem.collectorId,
|
||||
collectorName: collector.name,
|
||||
collectorColor: collector.color,
|
||||
acquiredAt: collectionItem.acquiredAt,
|
||||
acquiredPriceCents: collectionItem.acquiredPriceCents,
|
||||
acquiredFrom: collectionItem.acquiredFrom,
|
||||
@@ -86,6 +102,7 @@ export function listCollectionItems(filters: CollectionFilters): CollectionRow[]
|
||||
.innerJoin(manufacturer, eq(manufacturer.id, set.manufacturerId))
|
||||
.leftJoin(theme, eq(theme.id, set.themeId))
|
||||
.leftJoin(location, eq(location.id, collectionItem.locationId))
|
||||
.leftJoin(collector, eq(collector.id, collectionItem.collectorId))
|
||||
.where(buildWhere(filters))
|
||||
.orderBy(asc(manufacturer.name), asc(set.setNumber))
|
||||
.all();
|
||||
@@ -104,8 +121,13 @@ export type CollectionStats = {
|
||||
* The numbers on top of the collection page. Sold copies are history and do
|
||||
* not count anywhere - same rule as everywhere else in the app.
|
||||
*/
|
||||
export function getCollectionStats(): CollectionStats {
|
||||
const active = sql`${collectionItem.status} <> ${SOLD_STATUS}`;
|
||||
export function getCollectionStats(collectorId?: string): CollectionStats {
|
||||
const active = collectorId
|
||||
? and(
|
||||
sql`${collectionItem.status} <> ${SOLD_STATUS}`,
|
||||
eq(collectionItem.collectorId, collectorId),
|
||||
)
|
||||
: sql`${collectionItem.status} <> ${SOLD_STATUS}`;
|
||||
|
||||
const [totals] = db
|
||||
.select({
|
||||
@@ -144,16 +166,11 @@ export function getCollectionStats(): CollectionStats {
|
||||
}
|
||||
|
||||
/** Number of catalog sets that are not in the collection - the "fehlt" count. */
|
||||
export function countMissingSets(): number {
|
||||
export function countMissingSets(collectorId?: string): number {
|
||||
const [row] = db
|
||||
.select({ total: sql<number>`count(*)`.mapWith(Number) })
|
||||
.from(set)
|
||||
.where(
|
||||
sql`not exists (
|
||||
select 1 from collection_item ci
|
||||
where ci.set_id = ${set.id} and ci.status <> ${SOLD_STATUS}
|
||||
)`,
|
||||
)
|
||||
.where(sql`${ownedCopiesSql(set.id, collectorId)} = 0`)
|
||||
.all();
|
||||
|
||||
return row?.total ?? 0;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'server-only';
|
||||
import { asc, eq, sql } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { collectionItem, collector } from '@/db/schema';
|
||||
import { SOLD_STATUS } from '@/lib/ownership';
|
||||
|
||||
export type CollectorRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
birthday: Date | null;
|
||||
notes: string | null;
|
||||
sortOrder: number;
|
||||
itemCount: number;
|
||||
};
|
||||
|
||||
export function listCollectors(): CollectorRow[] {
|
||||
const itemCount = sql<number>`(
|
||||
select count(*) from collection_item ci
|
||||
where ci.collector_id = ${collector.id} and ci.status <> ${SOLD_STATUS}
|
||||
)`.mapWith(Number);
|
||||
|
||||
return db
|
||||
.select({
|
||||
id: collector.id,
|
||||
name: collector.name,
|
||||
color: collector.color,
|
||||
birthday: collector.birthday,
|
||||
notes: collector.notes,
|
||||
sortOrder: collector.sortOrder,
|
||||
itemCount,
|
||||
})
|
||||
.from(collector)
|
||||
.orderBy(asc(collector.sortOrder), asc(collector.name))
|
||||
.all();
|
||||
}
|
||||
|
||||
export function getCollector(id: string) {
|
||||
return db.select().from(collector).where(eq(collector.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The collector a new collection item belongs to when the user did not pick
|
||||
* one - which is every time there is only a single collector.
|
||||
*/
|
||||
export function defaultCollectorId(): string | null {
|
||||
const row = db
|
||||
.select({ id: collector.id })
|
||||
.from(collector)
|
||||
.orderBy(asc(collector.sortOrder), asc(collector.name))
|
||||
.get();
|
||||
return row?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole multi-collector idea only becomes visible once a second collector
|
||||
* exists. Until then the UI stays exactly as simple as it was.
|
||||
*/
|
||||
export function hasMultipleCollectors(): boolean {
|
||||
const [row] = db
|
||||
.select({ total: sql<number>`count(*)`.mapWith(Number) })
|
||||
.from(collector)
|
||||
.all();
|
||||
return (row?.total ?? 0) > 1;
|
||||
}
|
||||
|
||||
/** Reads the collector from a URL parameter, ignoring anything unknown. */
|
||||
export function resolveCollectorId(value: string | undefined | null): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const row = db.select({ id: collector.id }).from(collector).where(eq(collector.id, value)).get();
|
||||
return row?.id;
|
||||
}
|
||||
|
||||
/** Owned copies per collector - used by the collector management page. */
|
||||
export function countItemsPerCollector(): Map<string, number> {
|
||||
const rows = db
|
||||
.select({
|
||||
collectorId: collectionItem.collectorId,
|
||||
total: sql<number>`count(*)`.mapWith(Number),
|
||||
})
|
||||
.from(collectionItem)
|
||||
.where(sql`${collectionItem.status} <> ${SOLD_STATUS}`)
|
||||
.groupBy(collectionItem.collectorId)
|
||||
.all();
|
||||
|
||||
return new Map(
|
||||
rows.flatMap((row) => (row.collectorId ? [[row.collectorId, row.total] as const] : [])),
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'server-only';
|
||||
import { and, asc, count, eq, inArray, isNotNull, isNull, sql } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { manufacturer, set, theme, wishlist, wishlistItem } from '@/db/schema';
|
||||
import { ownedCopiesSql } from '@/lib/ownership';
|
||||
import { collector, manufacturer, set, theme, wishlist, wishlistItem } from '@/db/schema';
|
||||
import { ownedCopiesSql, SOLD_STATUS } from '@/lib/ownership';
|
||||
|
||||
/** Short list for the "auf welche Wunschliste?" dialogs. */
|
||||
export function listWishlistTargets() {
|
||||
@@ -34,12 +34,16 @@ export function listWishlists(ownerUserId: string) {
|
||||
isPublic: wishlist.isPublic,
|
||||
surpriseMode: wishlist.surpriseMode,
|
||||
shareToken: wishlist.shareToken,
|
||||
collectorId: wishlist.collectorId,
|
||||
collectorName: collector.name,
|
||||
collectorColor: collector.color,
|
||||
createdAt: wishlist.createdAt,
|
||||
itemCount: count(wishlistItem.id),
|
||||
openCount: openItems,
|
||||
})
|
||||
.from(wishlist)
|
||||
.leftJoin(wishlistItem, eq(wishlistItem.wishlistId, wishlist.id))
|
||||
.leftJoin(collector, eq(collector.id, wishlist.collectorId))
|
||||
.where(eq(wishlist.ownerUserId, ownerUserId))
|
||||
.groupBy(wishlist.id)
|
||||
.orderBy(asc(wishlist.title))
|
||||
@@ -76,7 +80,10 @@ export type WishlistItemRow = {
|
||||
ownedCopies: number;
|
||||
};
|
||||
|
||||
export function listWishlistItems(wishlistId: string): WishlistItemRow[] {
|
||||
export function listWishlistItems(
|
||||
wishlistId: string,
|
||||
collectorId?: string | null,
|
||||
): WishlistItemRow[] {
|
||||
return db
|
||||
.select({
|
||||
id: wishlistItem.id,
|
||||
@@ -97,7 +104,7 @@ export function listWishlistItems(wishlistId: string): WishlistItemRow[] {
|
||||
manufacturerName: manufacturer.name,
|
||||
manufacturerWebsite: manufacturer.website,
|
||||
themeName: theme.name,
|
||||
ownedCopies: ownedCopiesSql(set.id).mapWith(Number),
|
||||
ownedCopies: ownedCopiesSql(set.id, collectorId).mapWith(Number),
|
||||
})
|
||||
.from(wishlistItem)
|
||||
.innerJoin(set, eq(set.id, wishlistItem.setId))
|
||||
@@ -145,17 +152,28 @@ export function listWishlistsForSet(setId: string) {
|
||||
* Marks wishlist entries as fulfilled once the set is in the collection, and
|
||||
* clears the flag again if the copy was sold. Called after every change to the
|
||||
* collection, so the lists stay honest without a background job.
|
||||
*
|
||||
* With several collectors the question is per list: a wish on Max's list is
|
||||
* fulfilled when *Max* owns the set, not when his sister does. A list without
|
||||
* a collector falls back to "anyone here owns it".
|
||||
*/
|
||||
export function reconcileFulfilledWishlistItems(): void {
|
||||
const owned = ownedCopiesSql(wishlistItem.setId);
|
||||
const ownedForThisList = sql`exists (
|
||||
select 1
|
||||
from collection_item ci
|
||||
join wishlist w on w.id = ${wishlistItem.wishlistId}
|
||||
where ci.set_id = ${wishlistItem.setId}
|
||||
and ci.status <> ${SOLD_STATUS}
|
||||
and (w.collector_id is null or ci.collector_id = w.collector_id)
|
||||
)`;
|
||||
|
||||
db.update(wishlistItem)
|
||||
.set({ fulfilledAt: new Date() })
|
||||
.where(and(isNull(wishlistItem.fulfilledAt), sql`${owned} > 0`))
|
||||
.where(and(isNull(wishlistItem.fulfilledAt), ownedForThisList))
|
||||
.run();
|
||||
|
||||
db.update(wishlistItem)
|
||||
.set({ fulfilledAt: null })
|
||||
.where(and(isNotNull(wishlistItem.fulfilledAt), sql`${owned} = 0`))
|
||||
.where(and(isNotNull(wishlistItem.fulfilledAt), sql`not ${ownedForThisList}`))
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,22 @@ export const locationSchema = z.object({
|
||||
parentLocationId: optionalId,
|
||||
});
|
||||
|
||||
export const collectorColorEnum = z.enum([
|
||||
'blue',
|
||||
'green',
|
||||
'amber',
|
||||
'rose',
|
||||
'violet',
|
||||
'slate',
|
||||
]);
|
||||
|
||||
export const collectorSchema = z.object({
|
||||
name: z.string().trim().min(1, 'Bitte einen Namen angeben.').max(60),
|
||||
color: collectorColorEnum.default('blue'),
|
||||
birthday: optionalDate,
|
||||
notes: optionalText(500),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -127,6 +143,7 @@ export const conditionEnum = z.enum(['new_sealed', 'like_new', 'good', 'used', '
|
||||
|
||||
export const collectionItemSchema = z.object({
|
||||
setId: z.string().trim().min(1),
|
||||
collectorId: optionalId,
|
||||
status: collectionStatusEnum.default('owned'),
|
||||
completeness: completenessEnum.default('unknown'),
|
||||
condition: z
|
||||
@@ -171,6 +188,7 @@ export const occasionEnum = z.enum(['christmas', 'birthday', 'easter', 'other'])
|
||||
|
||||
export const wishlistSchema = z.object({
|
||||
title: z.string().trim().min(1, 'Bitte einen Titel angeben.').max(80),
|
||||
collectorId: optionalId,
|
||||
occasion: occasionEnum.default('other'),
|
||||
eventDate: optionalDate,
|
||||
note: optionalText(1000),
|
||||
@@ -205,3 +223,4 @@ export type LocationInput = z.infer<typeof locationSchema>;
|
||||
export type SetInput = z.infer<typeof setSchema>;
|
||||
export type CollectionItemInput = z.infer<typeof collectionItemSchema>;
|
||||
export type WishlistInput = z.infer<typeof wishlistSchema>;
|
||||
export type CollectorInput = z.infer<typeof collectorSchema>;
|
||||
|
||||
Reference in New Issue
Block a user