Files
Wabenhain/src/app/admin/actions/harvests-batches.ts
T
Nick SzaunigandClaude Sonnet 5 7d5c172487
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m21s
feat: eigene Bilder pro Charge
Chargen hatten bisher kein eigenes Bild-Feld (nur Ernten und Produkte) —
neue Migration ergaenzt Batch.images (Json-Array, gleiches Muster wie
Harvest.images/Product.images). Admin kann Chargenfotos jetzt unter
/admin/chargen/[id] hochladen (AdminImageListField, identisch zum
Ernten-Formular). Die oeffentliche Chargen-Seite (/charge/[code]) zeigt
diese Fotos jetzt bevorzugt, faellt ohne eigene Chargenbilder weiterhin
auf die Erntebilder zurueck.

Hinweis fuer den naechsten Live-Deploy: die generierte Migration hat
nebenbei eine bereits laenger bestehende Schema-Abweichung mitkorrigiert
(harvests.images und products.images/tasteTags werden jetzt als NOT NULL
erzwungen, wie im Schema schon laenger deklariert) - "prisma migrate
deploy" schlaegt fehl, falls dort noch echte NULL-Werte stehen sollten.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-12 04:08:39 +02:00

220 lines
7.6 KiB
TypeScript

"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
import { logAdminAction } from "@/lib/audit-log";
import { isValidUploadPath } from "@/lib/uploads";
import { requireAdmin, fail, str, num, strArray } from "./_shared";
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}`;
// 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: strArray(formData, "images").filter(isValidUploadPath),
isPublished,
},
});
await logAdminAction(admin, {
action: "harvest.create",
entityType: "Harvest",
entityId: harvest.id,
detail: `${type} ${year}`,
});
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
export async function updateHarvestAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const isPublished = formData.get("isPublished") === "on";
await prisma.harvest.update({
where: { id },
data: {
year: num(formData, "year"),
type: str(formData, "type"),
harvestDate: new Date(str(formData, "harvestDate")),
location: str(formData, "location") || "Altenmünster",
description: str(formData, "description"),
images: strArray(formData, "images").filter(isValidUploadPath),
isPublished,
},
});
await logAdminAction(admin, { action: "harvest.update", entityType: "Harvest", entityId: id });
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
/**
* Loescht eine Ernte unwiderruflich. harvestId ist auf Batch UND
* harvestId/batchId auf Product jeweils onDelete: Restrict (siehe
* prisma/schema.prisma) — die Datenbank wuerde ein direktes delete() also
* ohnehin ablehnen, sobald noch Chargen oder Produkte auf diese Ernte
* verweisen. Die Zaehlung hier VOR dem Loeschversuch liefert dafuer eine
* konkrete, verstaendliche Fehlermeldung statt eines rohen Datenbankfehlers.
*/
export async function deleteHarvestAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const harvest = await prisma.harvest.findUnique({
where: { id },
include: { _count: { select: { batches: true, products: true } } },
});
if (!harvest) fail("/admin/ernten", "Ernte nicht gefunden.");
if (harvest._count.batches > 0 || harvest._count.products > 0) {
fail(
"/admin/ernten",
`Diese Ernte hat noch ${harvest._count.batches} Charge(n) und ${harvest._count.products} Produkt(e). Lösche oder verschiebe diese zuerst.`
);
}
await prisma.harvest.delete({ where: { id } });
await logAdminAction(admin, {
action: "harvest.delete",
entityType: "Harvest",
entityId: id,
detail: `${harvest.type} ${harvest.year}`,
});
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
/**
* Isolierte "Veroeffentlichen"/"Verbergen"-Aktion, getrennt von
* updateHarvestAction — so kann der Status per Knopfdruck direkt aus der
* Ernten-Liste umgeschaltet werden, ohne durch das komplette Bearbeiten-
* Formular zu muessen (insbesondere fuer den "veroeffentlichen"-Bestaetigungs-
* schritt bei aus ApiCore synchronisierten Entwuerfen, siehe apicore-sync.ts).
*/
export async function toggleHarvestPublishedAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const isPublished = formData.get("isPublished") === "on";
const harvest = await prisma.harvest.update({
where: { id },
data: { isPublished },
});
await logAdminAction(admin, {
action: isPublished ? "harvest.publish" : "harvest.unpublish",
entityType: "Harvest",
entityId: id,
detail: `${harvest.type} ${harvest.year}`,
});
revalidatePath("/admin/ernten");
revalidatePath("/ernten");
redirect("/admin/ernten");
}
export async function createBatchAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const code = str(formData, "code");
const batch = await prisma.batch.create({
data: {
code,
harvestId: str(formData, "harvestId"),
fillDate: new Date(str(formData, "fillDate")),
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
glassCount: num(formData, "glassCount"),
remainingGlasses: num(formData, "remainingGlasses"),
images: strArray(formData, "images").filter(isValidUploadPath),
},
});
await logAdminAction(admin, { action: "batch.create", entityType: "Batch", entityId: batch.id, detail: code });
revalidatePath("/admin/chargen");
redirect("/admin/chargen");
}
export async function updateBatchAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
await prisma.batch.update({
where: { id },
data: {
fillDate: new Date(str(formData, "fillDate")),
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
glassCount: num(formData, "glassCount"),
remainingGlasses: num(formData, "remainingGlasses"),
images: strArray(formData, "images").filter(isValidUploadPath),
},
});
await logAdminAction(admin, { action: "batch.update", entityType: "Batch", entityId: id });
revalidatePath("/admin/chargen");
redirect("/admin/chargen");
}
/**
* Loescht eine Charge unwiderruflich. productId/batchId auf OrderItem sind
* onDelete: Restrict (siehe prisma/schema.prisma) — eine Charge, die schon
* in mindestens einer Bestellposition verwendet wurde, darf aus
* Rechnungs-/Nachweispflicht NIE geloescht werden (Chargen-Code steht auf
* dem QR-Code des Glases und in Rechnungen). Batch.products ist ebenfalls
* Restrict. Die Zaehlung hier VOR dem Loeschversuch liefert dafuer eine
* konkrete, verstaendliche Fehlermeldung statt eines rohen Datenbankfehlers.
*/
export async function deleteBatchAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const batch = await prisma.batch.findUnique({
where: { id },
include: { _count: { select: { products: true, orderItems: true } } },
});
if (!batch) fail("/admin/chargen", "Charge nicht gefunden.");
if (batch._count.orderItems > 0) {
fail(
"/admin/chargen",
`Charge "${batch.code}" wurde bereits in ${batch._count.orderItems} Bestellposition(en) verwendet und kann deshalb nicht gelöscht werden.`
);
}
if (batch._count.products > 0) {
fail(
"/admin/chargen",
`Charge "${batch.code}" ist noch ${batch._count.products} Produkt(en) zugeordnet. Löse diese Zuordnung zuerst.`
);
}
await prisma.batch.delete({ where: { id } });
await logAdminAction(admin, { action: "batch.delete", entityType: "Batch", entityId: id, detail: batch.code });
revalidatePath("/admin/chargen");
redirect("/admin/chargen");
}