feat: seed a starter annual task-template calendar

TaskTemplate had zero seed data - a fresh deployment showed a
completely empty recurring-task list with no hint of what to even put
there. Seeds 12 YEARLY templates, one per month, covering the standard
German-beekeeping annual cycle (Frühjahrsdurchsicht, Schwarmkontrolle,
Einfütterung, Varroa-Restentmilbung/Oxalsäure, Wintersitz-Kontrolle,
etc.) so a new beekeeper gets a ready-made starting point instead of a
blank page. Runs once (guarded by a taskTemplate.count() check, same
idempotency shape as the existing admin-user seed) and only ever adds
rows - admins can edit or deactivate any of them afterward.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-05 17:58:28 +02:00
1 parent c4f970f071
commit 81986b3275
1 file changed
+161
+161
View File
@@ -30,11 +30,172 @@ function printLoginBanner(email: string, password: string): void {
console.log(line); console.log(line);
} }
// Duplicated from src/task-templates/recurrence.ts rather than imported:
// tsconfig.seed.json scopes this build to rootDir "./prisma" only (its own
// isolated compile, separate from the main Nest build), so a cross-rootDir
// import would fail to compile. Only the YEARLY case is needed here.
function nextYearlyOccurrenceUtc(
month: number,
dayOfMonth: number,
from: Date,
): Date {
const year = from.getUTCFullYear();
const clampedDay = (y: number): number =>
Math.min(dayOfMonth, new Date(Date.UTC(y, month, 0)).getUTCDate());
const today = new Date(
Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate()),
);
const candidate = new Date(Date.UTC(year, month - 1, clampedDay(year)));
if (candidate.getTime() >= today.getTime()) {
return candidate;
}
return new Date(Date.UTC(year + 1, month - 1, clampedDay(year + 1)));
}
interface DefaultTaskTemplate {
title: string;
description: string;
priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
month: number;
dayOfMonth: number;
}
// The standard German-beekeeping annual cycle, one flagship task per month -
// gives a new deployment a ready-made starter calendar instead of a
// completely empty task-template list. Admins can edit/deactivate any of
// these afterward; this only ever runs once (see the count() guard below).
const DEFAULT_TASK_TEMPLATES: DefaultTaskTemplate[] = [
{
title: 'Wintersitz kontrollieren',
description:
'Ruhige Kontrolle von außen: Flugloch frei, Gewicht per Anheben schätzen, auf Mäuse-/Spechtschäden achten. Beute möglichst nicht öffnen.',
priority: 'NORMAL',
month: 1,
dayOfMonth: 15,
},
{
title: 'Gemülldiagnose',
description:
'Windel auswerten: natürlicher Varroa-Totenfall, Bienensitz und Lage der Wintertraube anhand des Gemülls einschätzen.',
priority: 'NORMAL',
month: 2,
dayOfMonth: 15,
},
{
title: 'Frühjahrsdurchsicht',
description:
'Erste Innendurchsicht bei ca. 15°C und ruhigem Wetter: Brutnest, Futtervorrat, Volksstärke und Weiselrichtigkeit prüfen.',
priority: 'HIGH',
month: 3,
dayOfMonth: 15,
},
{
title: 'Volk erweitern',
description:
'Zargen/Rähmchen nach Volksstärke erweitern, Baurahmen einhängen, Platz für die Frühtracht schaffen.',
priority: 'NORMAL',
month: 4,
dayOfMonth: 1,
},
{
title: 'Schwarmkontrolle beginnen',
description:
'Wöchentlich auf Weiselzellen kontrollieren, bis zum Ende der Schwarmzeit fortführen. Maßnahmen bei Bedarf: Ableger bilden, Zellen brechen.',
priority: 'HIGH',
month: 5,
dayOfMonth: 1,
},
{
title: 'Honigernte vorbereiten',
description:
'Reifegrad der Frühtracht prüfen (Spritzprobe), Schleuderplan und Gerätereinigung vorbereiten.',
priority: 'NORMAL',
month: 6,
dayOfMonth: 15,
},
{
title: 'Varroa-Kontrolle nach der Ernte',
description:
'Milbenbefall messen (z. B. Puderzuckermethode oder natürlicher Totenfall) und Behandlung planen, sobald die Honigräume abgeräumt sind.',
priority: 'HIGH',
month: 7,
dayOfMonth: 15,
},
{
title: 'Einfütterung beginnen',
description:
'Wintervorrat auffüttern (Zielgewicht je nach Beutensystem), Futteraufnahme nach ein bis zwei Wochen kontrollieren.',
priority: 'HIGH',
month: 8,
dayOfMonth: 1,
},
{
title: 'Varroa-Restentmilbung / Winterfestigkeit prüfen',
description:
'Zweite Varroabehandlung durchführen, Volksstärke und Weiselrichtigkeit für den Winter beurteilen.',
priority: 'HIGH',
month: 9,
dayOfMonth: 15,
},
{
title: 'Fütterung abschließen, Mäusegitter anbringen',
description:
'Letzte Futtergabe, Fluglochkeile bzw. Mäusegitter montieren, Bienenstand winterfest machen.',
priority: 'NORMAL',
month: 10,
dayOfMonth: 1,
},
{
title: 'Winterbehandlung (Oxalsäure)',
description:
'Brutfreiheit abwarten und Träufel- oder Sprühbehandlung gegen Varroa durchführen.',
priority: 'HIGH',
month: 11,
dayOfMonth: 15,
},
{
title: 'Jahresrückblick und Gerätewartung',
description:
'Saison auswerten, Zargen und Rähmchen reinigen/reparieren, Material für die kommende Saison planen.',
priority: 'LOW',
month: 12,
dayOfMonth: 15,
},
];
async function seedDefaultTaskTemplates(prisma: PrismaClient): Promise<void> {
const existingCount = await prisma.taskTemplate.count();
if (existingCount > 0) {
return;
}
const now = new Date();
await prisma.taskTemplate.createMany({
data: DEFAULT_TASK_TEMPLATES.map((template) => ({
title: template.title,
description: template.description,
priority: template.priority,
recurrenceType: 'YEARLY',
recurrenceMonth: template.month,
recurrenceDayOfMonth: template.dayOfMonth,
nextDueDate: nextYearlyOccurrenceUtc(
template.month,
template.dayOfMonth,
now,
),
})),
});
console.log(
`Seed: ${DEFAULT_TASK_TEMPLATES.length} Standard-Aufgabenvorlagen angelegt.`,
);
}
async function main(): Promise<void> { async function main(): Promise<void> {
const prisma = new PrismaClient({ const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
}); });
await seedDefaultTaskTemplates(prisma);
const email = process.env.SEED_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL; const email = process.env.SEED_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL;
const password = process.env.SEED_ADMIN_PASSWORD ?? generateRandomPassword(); const password = process.env.SEED_ADMIN_PASSWORD ?? generateRandomPassword();
const existing = await prisma.user.findUnique({ where: { email } }); const existing = await prisma.user.findUnique({ where: { email } });