Files
Epicnabbo-Catalogus-Updated…/src/app/admin/maintenance/page.tsx
T
simoleoandCursor 0e89d03940 Finish fine-grained ACL across remaining admin pages and actions.
Replace leftover requireStaff gates with module PERMS, drop hardcoded room rank thresholds, and expand contract tests so admin mutations cannot regress to dashboard-only checks.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:15:22 +02:00

163 lines
4.8 KiB
TypeScript

import { Wrench } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { saveMaintenance } from "@/actions/admin-maintenance";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
const KEYS = [
"maintenance_enabled",
"maintenance_message",
"min_maintenance_login_rank",
] as const;
export default async function AdminMaintenance() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.maintenance");
let rows: { key: string; value: string }[] = [];
try {
rows = await prisma.websiteSetting.findMany({
where: { key: { in: [...KEYS] } },
select: { key: true, value: true },
});
} catch {
rows = [];
}
const byKey = new Map(rows.map((r) => [r.key, r.value]));
const enabledRaw = byKey.get("maintenance_enabled") ?? "0";
const enabled = enabledRaw === "1" || enabledRaw.toLowerCase() === "true";
const message = byKey.get("maintenance_message") ?? "";
const minRankRaw = byKey.get("min_maintenance_login_rank") ?? "5";
const minRank = Number.parseInt(minRankRaw, 10);
const minRankValue = Number.isFinite(minRank) ? minRank : 5;
return (
<main>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
<Wrench size={20} className="text-[var(--admin-accent)]" />
</div>
<div>
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
{t("title")}
</h1>
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">
{t.rich("subtitle", { code: (chunks) => <code>{chunks}</code> })}
</p>
</div>
</div>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
<StatusCard
label={t("maintenance")}
value={enabled ? t("enabled") : t("disabled")}
state={enabled ? "danger" : "ok"}
hint={enabled ? t("hotelLockedDown") : t("hotelOpen")}
icon={enabled ? "🚧" : "🟢"}
/>
<StatusCard
label={t("minLoginRank")}
value={minRankValue}
hint={t("duringMaintenance")}
icon="🛡️"
/>
<StatusCard
label={t("splashMessage")}
value={message ? t("set") : t("empty")}
state={message ? "ok" : "neutral"}
icon="💬"
/>
</div>
<section className="mt-6">
<h2 className="admin-section-title">{t("currentState")}</h2>
<div className="admin-card flex items-center gap-3">
<span
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-[var(--admin-error-subtle)] text-[var(--admin-text)]" : "bg-[var(--admin-success-subtle)] text-[var(--admin-text)]"}`}
>
{enabled ? t("enabled") : t("disabled")}
</span>
<div>
<strong>
{t("maintenanceCurrently", {
status: enabled ? t("enabled") : t("disabled"),
})}
</strong>
<p className="text-xs theme-text-muted dark:theme-text-muted mt-1 m-0">
{enabled
? t("onlyRankCanLogin", { rank: minRankValue })
: t("hotelOpenToEveryone")}
</p>
</div>
</div>
</section>
<section className="mt-6">
<h2 className="admin-section-title">{t("settings")}</h2>
<form action={saveMaintenance} className="admin-card">
<label htmlFor="mt-enabled" className="flex items-center gap-2 mb-4">
<input
id="mt-enabled"
type="checkbox"
name="enabled"
value="1"
defaultChecked={enabled}
className="w-auto"
/>
<span>{t("enableMaintenance")}</span>
</label>
<div className="mb-4 max-w-[220px]">
<label
htmlFor="mt-min-rank"
className="text-sm theme-text-muted dark:theme-text-muted block mb-1"
>
{t("minLoginRankLabel")}
</label>
<input
id="mt-min-rank"
type="number"
name="min_rank"
min={0}
step={1}
defaultValue={minRankValue}
className="w-full"
/>
</div>
<div className="mb-4">
<label
htmlFor="mt-message"
className="text-sm theme-text-muted dark:theme-text-muted block mb-1"
>
{t("maintenanceMessageLabel")}
</label>
<textarea
id="mt-message"
name="message"
rows={8}
defaultValue={message}
placeholder={t("maintenanceMessagePlaceholder")}
className="w-full resize-y"
/>
</div>
<button type="submit" className="btn btn-primary">
{t("saveChanges")}
</button>
</form>
</section>
</main>
);
}