feat(stammdaten): crud fuer hersteller, themen und standorte

Gemeinsame CrudSection fuer die drei kleinen Stammdatentabellen; sie
unterscheiden sich nur in den Formularfeldern. Loeschen ist bewusst
blockiert, solange Sets am Hersteller oder Thema haengen - Standorte
duerfen weg, die Sammlungseintraege verlieren dann nur den Ort.

Dazu die gemeinsamen Bausteine: zod-Schemata fuer alle Formulare,
einheitliches ActionResult, Slug-Erzeugung mit deutschen Umlauten,
Geld als Cent und Datumsformatierung dd.MM.yyyy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
unknown
2026-09-06 22:46:45 +02:00
co-authored by Claude Opus 5
parent 4c78c5672b
commit af0ae039cb
18 changed files with 1254 additions and 70 deletions
+71
View File
@@ -0,0 +1,71 @@
import 'server-only';
import { asc, count, eq, sql } from 'drizzle-orm';
import { db } from '@/db';
import { location, manufacturer, set, theme } from '@/db/schema';
export function listManufacturers() {
return db
.select({
id: manufacturer.id,
name: manufacturer.name,
slug: manufacturer.slug,
website: manufacturer.website,
setCount: count(set.id),
})
.from(manufacturer)
.leftJoin(set, eq(set.manufacturerId, manufacturer.id))
.groupBy(manufacturer.id)
.orderBy(asc(manufacturer.name))
.all();
}
export function listThemes() {
const parent = sql<string | null>`(
select parent.name from theme as parent where parent.id = ${theme.parentThemeId}
)`.as('parent_name');
return db
.select({
id: theme.id,
name: theme.name,
manufacturerId: theme.manufacturerId,
manufacturerName: manufacturer.name,
parentThemeId: theme.parentThemeId,
parentName: parent,
})
.from(theme)
.innerJoin(manufacturer, eq(manufacturer.id, theme.manufacturerId))
.orderBy(asc(manufacturer.name), asc(theme.name))
.all();
}
/** Themes of one manufacturer - used by the set form and the catalog filter. */
export function listThemesByManufacturer(manufacturerId: string) {
return db
.select({ id: theme.id, name: theme.name })
.from(theme)
.where(eq(theme.manufacturerId, manufacturerId))
.orderBy(asc(theme.name))
.all();
}
export function listLocations() {
const parent = sql<string | null>`(
select parent.name from location as parent where parent.id = ${location.parentLocationId}
)`.as('parent_name');
return db
.select({
id: location.id,
name: location.name,
parentLocationId: location.parentLocationId,
parentName: parent,
})
.from(location)
.orderBy(asc(location.name))
.all();
}
export type ManufacturerListRow = ReturnType<typeof listManufacturers>[number];
export type ThemeListRow = ReturnType<typeof listThemes>[number];
export type LocationListRow = ReturnType<typeof listLocations>[number];