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

@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { SiteSettingsModule } from '../site-settings/site-settings.module';
import { PublicController } from './public.controller';
import { PublicService } from './public.service';
@Module({
imports: [SiteSettingsModule],
controllers: [PublicController],
providers: [PublicService],
})
@@ -1,14 +1,26 @@
import { NotFoundException } from '@nestjs/common';
import { PublicService } from './public.service';
import { PrismaService } from '../prisma/prisma.service';
import { SiteSettingsService } from '../site-settings/site-settings.service';
describe('PublicService', () => {
let service: PublicService;
let prisma: { bottlingBatch: { findUnique: jest.Mock } };
let siteSettings: { get: jest.Mock };
beforeEach(() => {
prisma = { bottlingBatch: { findUnique: jest.fn() } };
service = new PublicService(prisma as unknown as PrismaService);
siteSettings = {
get: jest.fn().mockResolvedValue({
youtubeUrl: null,
instagramUrl: null,
tiktokUrl: null,
}),
};
service = new PublicService(
prisma as unknown as PrismaService,
siteSettings as unknown as SiteSettingsService,
);
});
it('throws NotFoundException when the lot does not exist', async () => {
@@ -66,4 +78,32 @@ describe('PublicService', () => {
expect(result.apiaries).toEqual([{ name: 'Stand Wald', location: 'Wald' }]);
expect(result.harvestDates).toEqual([harvestDate1, harvestDate2]);
});
it('includes the configured social links', async () => {
siteSettings.get.mockResolvedValue({
youtubeUrl: 'https://youtube.com/@wabenhain',
instagramUrl: 'https://instagram.com/wabenhain',
tiktokUrl: null,
});
prisma.bottlingBatch.findUnique.mockResolvedValueOnce({
lotNumber: 'WH-2026-001',
date: new Date('2026-08-20'),
bestBeforeDate: new Date('2028-08-20'),
jarSize: 500,
product: null,
honeyBatch: {
honeyType: null,
waterContent: null,
extraction: { date: null, harvests: [] },
},
});
const result = await service.findBatchByLot('WH-2026-001');
expect(result.socialLinks).toEqual({
youtube: 'https://youtube.com/@wabenhain',
instagram: 'https://instagram.com/wabenhain',
tiktok: null,
});
});
});
+19 -1
View File
@@ -1,11 +1,18 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { SiteSettingsService } from '../site-settings/site-settings.service';
export interface PublicApiaryOrigin {
name: string;
location: string | null;
}
export interface PublicSocialLinks {
youtube: string | null;
instagram: string | null;
tiktok: string | null;
}
export interface PublicBatch {
lotNumber: string;
productName: string | null;
@@ -18,11 +25,15 @@ export interface PublicBatch {
extractedDate: Date | null;
harvestDates: Date[];
apiaries: PublicApiaryOrigin[];
socialLinks: PublicSocialLinks;
}
@Injectable()
export class PublicService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly siteSettings: SiteSettingsService,
) {}
async findBatchByLot(lotNumber: string): Promise<PublicBatch> {
const batch = await this.prisma.bottlingBatch.findUnique({
@@ -60,6 +71,8 @@ export class PublicService {
.sort((a, b) => a - b)
.map((time) => new Date(time));
const settings = await this.siteSettings.get();
return {
lotNumber: batch.lotNumber,
productName: batch.product?.name ?? null,
@@ -72,6 +85,11 @@ export class PublicService {
extractedDate: batch.honeyBatch.extraction.date,
harvestDates,
apiaries: [...apiaryByName.values()],
socialLinks: {
youtube: settings.youtubeUrl,
instagram: settings.instagramUrl,
tiktok: settings.tiktokUrl,
},
};
}
}