feat: Abfüllungs-Archiv und Social-Media-Links auf der Herkunftsseite

Abfüllungen hatten keine Übersichtsseite - die Navigation ging direkt
auf /bottling/new, sodass eine angelegte Abfüllung danach nirgends
mehr sichtbar war. Neue Seite /bottling listet alle Abfüllungen
(Archiv), Klick führt zur bestehenden Detail-/Bearbeiten-Seite.
Navigationslink zeigt jetzt auf die Liste statt direkt auf "Neu".

Neues Modul site-settings (Singleton-Tabelle SiteSettings) für
YouTube-/Instagram-/TikTok-Links, verwaltbar unter Verwaltung > Website
(neue Berechtigung site-settings:read/update). Die öffentliche
Herkunftsseite (/herkunft/[lot]) zeigt die konfigurierten Links jetzt
zusätzlich zur Chargen-Herkunft an - PublicService liefert sie über
das bereits oeffentliche /public/batches/:lotNumber mit aus, keine
zusaetzliche unauthentifizierte Route noetig.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-02 19:57:29 +02:00
1 parent 33fa768062
commit 351f9ef34c
19 files changed
+612 -4

No files matched your search

@@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { SiteSettings } from '../../generated/prisma';
import { UpdateSiteSettingsDto } from './dto/update-site-settings.dto';
const SINGLETON_ID = 'singleton';
function emptyToNull(value: string | undefined): string | null {
return value === '' ? null : (value ?? null);
}
@Injectable()
export class SiteSettingsService {
constructor(
private readonly prisma: PrismaService,
private readonly auditLog: AuditLogService,
) {}
// Find-or-create rather than a migration-time seed row: keeps the
// singleton pattern self-contained in this service instead of coupling it
// to seed.ts/migration ordering.
async get(): Promise<SiteSettings> {
const existing = await this.prisma.siteSettings.findUnique({
where: { id: SINGLETON_ID },
});
if (existing) {
return existing;
}
return this.prisma.siteSettings.create({ data: { id: SINGLETON_ID } });
}
async update(
dto: UpdateSiteSettingsDto,
actingUserId: string,
): Promise<SiteSettings> {
const before = await this.get();
const settings = await this.prisma.siteSettings.update({
where: { id: SINGLETON_ID },
data: {
youtubeUrl:
dto.youtubeUrl !== undefined
? emptyToNull(dto.youtubeUrl)
: undefined,
instagramUrl:
dto.instagramUrl !== undefined
? emptyToNull(dto.instagramUrl)
: undefined,
tiktokUrl:
dto.tiktokUrl !== undefined ? emptyToNull(dto.tiktokUrl) : undefined,
},
});
await this.auditLog.record({
entityType: 'SiteSettings',
entityId: SINGLETON_ID,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: settings,
});
return settings;
}
}