- Figtree als einzige Schrift, Logo mittig in fester Kopfzeile - Headerbild ueber volle Breite und Hoehe, ohne Preis, mit Terminleiste - "Was dich erwartet" stellt die Villa mit animierter Bilderfolge vor, darunter Tagesablauf als Karussell (Programm folgt als PDF) - Preis: regulaerer Preis durchgestrichen, kein Verkaufsstartdatum, Ueberschrift "Jetzt Platz sichern mit Early Bird" - Team als "Mit dabei", danach drei Reels im Hochformat - Logo, Headerbild, Portraets und Reels ueber feste Dateinamen im Medienverzeichnis (docs/MEDIEN.md), ohne Neustart austauschbar - Widerrufsbelehrung im Admin pflegbar, Seite /widerruf, Live-Verkauf ohne Freigabe gesperrt - Anreise: Abholung am Bahnhof Firenze Santa Maria Novella - Booking-Link entfernt, Gedankenstriche aus Kundentexten entfernt Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
155 lines
6.3 KiB
TypeScript
155 lines
6.3 KiB
TypeScript
"use server";
|
|
|
|
/**
|
|
* Server-Actions des Adminbereichs. JEDE Action prueft zuerst selbst die
|
|
* Anmeldung inkl. 2FA (requireAdminAction) — unabhaengig davon, ob die
|
|
* Seite geschuetzt ist, denn Actions sind eigenstaendig aufrufbar.
|
|
*/
|
|
import { revalidatePath } from "next/cache";
|
|
import { redirect } from "next/navigation";
|
|
import { adminActor, requireAdminAction } from "@/lib/admin-auth";
|
|
import {
|
|
campInputSchema,
|
|
cancelBooking,
|
|
confirmReviewedBooking,
|
|
refundInstallment,
|
|
resendConfirmation,
|
|
saveVoucher,
|
|
updateCamp,
|
|
voucherInputSchema,
|
|
type ActionResult,
|
|
} from "@/lib/admin-service";
|
|
import { getDb } from "@/lib/db";
|
|
import { requireAppSecret } from "@/lib/env";
|
|
import { getGateway } from "@/lib/payment-gateway";
|
|
|
|
function back(path: string, result: ActionResult): never {
|
|
const param = result.ok ? "ok" : "fehler";
|
|
redirect(`${path}?${param}=${encodeURIComponent(result.message)}`);
|
|
}
|
|
|
|
function str(form: FormData, key: string): string {
|
|
const value = form.get(key);
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function bool(form: FormData, key: string): boolean {
|
|
return form.get(key) === "on";
|
|
}
|
|
|
|
function firstIssue(error: { issues: { message: string }[] }): string {
|
|
return error.issues[0]?.message ?? "Ungültige Eingabe.";
|
|
}
|
|
|
|
const ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
|
|
export async function cancelBookingAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const id = str(form, "bookingId");
|
|
if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." });
|
|
const result = await cancelBooking(getDb(), getGateway(), { bookingId: id, reason: str(form, "reason") }, adminActor(admin));
|
|
revalidatePath(`/admin/buchungen/${id}`);
|
|
back(`/admin/buchungen/${id}`, result);
|
|
}
|
|
|
|
export async function refundAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const bookingId = str(form, "bookingId");
|
|
const installmentId = str(form, "installmentId");
|
|
const requestKey = str(form, "requestKey");
|
|
if (!ID.test(bookingId) || !ID.test(installmentId) || !/^[0-9a-f-]{36}$/.test(requestKey)) {
|
|
back("/admin/buchungen", { ok: false, message: "Ungültige Anfrage." });
|
|
}
|
|
const euros = Number(str(form, "amount").replace(",", "."));
|
|
const amountCents = Math.round(euros * 100);
|
|
const result = await refundInstallment(
|
|
getDb(),
|
|
getGateway(),
|
|
{ installmentId, amountCents, requestKey, reason: str(form, "reason") },
|
|
adminActor(admin),
|
|
);
|
|
revalidatePath(`/admin/buchungen/${bookingId}`);
|
|
back(`/admin/buchungen/${bookingId}`, result);
|
|
}
|
|
|
|
export async function confirmReviewAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const id = str(form, "bookingId");
|
|
if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." });
|
|
const result = await confirmReviewedBooking(getDb(), id, adminActor(admin));
|
|
revalidatePath(`/admin/buchungen/${id}`);
|
|
back(`/admin/buchungen/${id}`, result);
|
|
}
|
|
|
|
export async function resendConfirmationAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const id = str(form, "bookingId");
|
|
if (!ID.test(id)) back("/admin/buchungen", { ok: false, message: "Ungültige Buchung." });
|
|
const result = await resendConfirmation(getDb(), id, requireAppSecret(), adminActor(admin));
|
|
back(`/admin/buchungen/${id}`, result);
|
|
}
|
|
|
|
export async function saveVoucherAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const id = str(form, "voucherId") || null;
|
|
if (id && !ID.test(id)) back("/admin/gutscheine", { ok: false, message: "Ungültiger Gutschein." });
|
|
const parsed = voucherInputSchema.safeParse({
|
|
code: str(form, "code"),
|
|
active: bool(form, "active"),
|
|
validFrom: str(form, "validFrom"),
|
|
validUntil: str(form, "validUntil"),
|
|
maxRedemptions: str(form, "maxRedemptions"),
|
|
boundEmail: str(form, "boundEmail"),
|
|
internalNote: str(form, "internalNote"),
|
|
});
|
|
const target = id ? `/admin/gutscheine/${id}` : "/admin/gutscheine";
|
|
if (!parsed.success) back(target, { ok: false, message: firstIssue(parsed.error) });
|
|
const result = await saveVoucher(getDb(), id, parsed.data, adminActor(admin));
|
|
revalidatePath("/admin/gutscheine");
|
|
back(result.ok && result.id ? `/admin/gutscheine/${result.id}` : target, result);
|
|
}
|
|
|
|
export async function updateCampAction(form: FormData) {
|
|
const admin = await requireAdminAction();
|
|
const parsed = campInputSchema.safeParse({
|
|
salesEnabled: bool(form, "salesEnabled"),
|
|
capacity: str(form, "capacity"),
|
|
capacityConfirmed: bool(form, "capacityConfirmed"),
|
|
stammmodelQuota: str(form, "stammmodelQuota"),
|
|
stammmodelQuotaConfirmed: bool(form, "stammmodelQuotaConfirmed"),
|
|
campStartDate: str(form, "campStartDate"),
|
|
campEndDate: str(form, "campEndDate"),
|
|
dateConfirmed: bool(form, "dateConfirmed"),
|
|
locationName: str(form, "locationName"),
|
|
locationRegion: str(form, "locationRegion"),
|
|
locationConfirmed: bool(form, "locationConfirmed"),
|
|
accommodationText: str(form, "accommodationText"),
|
|
accommodationConfirmed: bool(form, "accommodationConfirmed"),
|
|
servicesConfirmed: bool(form, "servicesConfirmed"),
|
|
organizerName: str(form, "organizerName"),
|
|
organizerAddress: str(form, "organizerAddress"),
|
|
organizerEmail: str(form, "organizerEmail"),
|
|
organizerPhone: str(form, "organizerPhone"),
|
|
organizerConfirmed: bool(form, "organizerConfirmed"),
|
|
taxNote: str(form, "taxNote"),
|
|
taxConfirmed: bool(form, "taxConfirmed"),
|
|
termsVersion: str(form, "termsVersion"),
|
|
termsUrl: str(form, "termsUrl"),
|
|
termsApproved: bool(form, "termsApproved"),
|
|
withdrawalText: str(form, "withdrawalText"),
|
|
withdrawalApproved: bool(form, "withdrawalApproved"),
|
|
travelLawChecked: bool(form, "travelLawChecked"),
|
|
installmentsApproved: bool(form, "installmentsApproved"),
|
|
installmentRuleApproved: bool(form, "installmentRuleApproved"),
|
|
reservationMinutes: str(form, "reservationMinutes"),
|
|
finalInstallmentLeadDays: str(form, "finalInstallmentLeadDays"),
|
|
minInstallmentGapDays: str(form, "minInstallmentGapDays"),
|
|
testPhaseOverride: str(form, "testPhaseOverride"),
|
|
});
|
|
if (!parsed.success) back("/admin/einstellungen", { ok: false, message: firstIssue(parsed.error) });
|
|
const result = await updateCamp(getDb(), parsed.data, adminActor(admin));
|
|
revalidatePath("/admin", "layout");
|
|
revalidatePath("/");
|
|
back("/admin/einstellungen", result);
|
|
}
|