fix(harvests): allow several harvests of the same type per year
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m15s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m15s
The ApiCore sync failed with "Unique constraint failed on the constraint:
harvests_year_type_key" as soon as ApiCore had two harvests of the same
honey type in one year, which is normal (several extractions per season).
Every bottling of that harvest was then skipped too.
- Drop the (year, type) unique index. The slug is now the only unique,
public identifier: /ernten/<slug>, e.g. bluetenhonig-2026, then
bluetenhonig-2026-07-14 for a further harvest of the same type.
- New Harvest.apicoreHarvestId as the sync key, backfilled from the old
"apicore-<id>" slugs; the sync gives those rows a readable slug.
- Slugs are generated by one shared slugify() that normalizes Unicode
(decomposed umlauts) and writes out umlauts. The migration repairs
manual slugs created by the old umlaut bug ("fr-htracht-2026").
- Slug collisions are resolved by the database unique index, so parallel
writers cannot produce duplicates; known slugs are skipped up front to
keep the logs free of handled P2002 errors.
- Old URLs /ernten/<year>/<type> answer with 308 to the new address.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
f1c0da4673
commit
641afa1ae4
15 files changed
+407
-83
No files matched your search
@@ -6,6 +6,8 @@ import { logAdminAction } from "@/lib/audit-log";
|
||||
import { isValidUploadPath } from "@/lib/uploads";
|
||||
import { requireAdmin, fail, str, num, strArraySlots } from "./_shared";
|
||||
import { succeed } from "@/lib/flash";
|
||||
import { harvestSlugCandidates, writeWithFreeSlug } from "@/lib/harvest-slug";
|
||||
import { findTakenHarvestSlugs } from "@/lib/harvest-slug-db";
|
||||
|
||||
/**
|
||||
* Bildslots fuer Ernte und Charge. Die Position entscheidet, WELCHE Kachel
|
||||
@@ -26,25 +28,33 @@ export async function createHarvestAction(formData: FormData): Promise<void> {
|
||||
const admin = await requireAdmin();
|
||||
const year = num(formData, "year");
|
||||
const type = str(formData, "type");
|
||||
const slug = `${type.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${year}`;
|
||||
const harvestDate = new Date(str(formData, "harvestDate"));
|
||||
if (!type || !Number.isInteger(year) || year < 1 || Number.isNaN(harvestDate.getTime())) {
|
||||
return fail("/admin/ernten", "Bitte Sorte, Jahr und ein gültiges Erntedatum angeben.");
|
||||
}
|
||||
// Checkbox: nur in formData vorhanden, wenn angehakt — fehlt sie ganz
|
||||
// (kein Feld im Formular), gilt "veroeffentlicht" als sicherer Default fuer
|
||||
// manuell im Admin-Bereich angelegte Ernten (bisheriges Verhalten vor
|
||||
// Einfuehrung von isPublished).
|
||||
const isPublished = !formData.has("isPublished") || formData.get("isPublished") === "on";
|
||||
|
||||
const harvest = await prisma.harvest.create({
|
||||
data: {
|
||||
year,
|
||||
type,
|
||||
slug,
|
||||
harvestDate: new Date(str(formData, "harvestDate")),
|
||||
location: str(formData, "location") || "Altenmünster",
|
||||
description: str(formData, "description"),
|
||||
images: imageSlots(formData),
|
||||
isPublished,
|
||||
},
|
||||
});
|
||||
// Dieselbe Sorte darf mehrmals im Jahr geerntet werden; die zweite bekommt
|
||||
// eine Adresse mit Erntedatum statt eines Fehlers (lib/harvest-slug.ts).
|
||||
const harvest = await writeWithFreeSlug(harvestSlugCandidates(type, harvestDate), (slug) =>
|
||||
prisma.harvest.create({
|
||||
data: {
|
||||
year,
|
||||
type,
|
||||
slug,
|
||||
harvestDate,
|
||||
location: str(formData, "location") || "Altenmünster",
|
||||
description: str(formData, "description"),
|
||||
images: imageSlots(formData),
|
||||
isPublished,
|
||||
},
|
||||
}),
|
||||
findTakenHarvestSlugs
|
||||
);
|
||||
|
||||
await logAdminAction(admin, {
|
||||
action: "harvest.create",
|
||||
|
||||
Reference in new issue
Block a user