249 lines
6.5 KiB
TypeScript
249 lines
6.5 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import {
|
|
deletePermission,
|
|
upsertPermission,
|
|
} from "@/actions/admin-housekeeping";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AdminHousekeeping({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ q?: string; mode?: "manage" | "import" }>;
|
|
}) {
|
|
const t = await getTranslations("pages.admin.housekeeping");
|
|
|
|
const sp = await searchParams;
|
|
const q = (sp.q ?? "").trim();
|
|
const mode = sp.mode === "import" ? "import" : "manage";
|
|
|
|
let permissions: Awaited<
|
|
ReturnType<typeof prisma.websiteHousekeepingPermissions.findMany>
|
|
> = [];
|
|
try {
|
|
permissions = await prisma.websiteHousekeepingPermissions.findMany({
|
|
orderBy: [{ permission: "asc" }],
|
|
});
|
|
} catch {
|
|
permissions = [];
|
|
}
|
|
|
|
const filtered = q
|
|
? permissions.filter((p) =>
|
|
p.permission.toLowerCase().includes(q.toLowerCase()),
|
|
)
|
|
: permissions;
|
|
|
|
const ranks = permissions.map((p) => p.minRank);
|
|
const maxRank = ranks.length ? Math.max(...ranks) : 0;
|
|
const totalMin = ranks.reduce((sum, r) => sum + r, 0);
|
|
|
|
const selectedIds = new Set<number>();
|
|
if (mode === "import") {
|
|
const importRaw = String(sp.q ?? "");
|
|
if (importRaw) {
|
|
const importIds = importRaw
|
|
.split(",")
|
|
.map((i) => Number(i.trim()))
|
|
.filter((i) => !Number.isNaN(i));
|
|
importIds.forEach((id) => {
|
|
selectedIds.add(id);
|
|
});
|
|
}
|
|
}
|
|
|
|
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">
|
|
<span className="text-lg">🔑</span>
|
|
</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("subtitle")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-stat-row">
|
|
<span className="admin-stat-chip">
|
|
{t("permissionsCount", { count: permissions.length })}
|
|
</span>
|
|
<span className="admin-stat-chip">
|
|
{t("highestMinRank", { rank: maxRank })}
|
|
</span>
|
|
<span className="admin-stat-chip">
|
|
{t("totalMinRank", { total: totalMin })}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="admin-filter-bar">
|
|
<form className="flex gap-2 flex-1">
|
|
<input
|
|
name="q"
|
|
defaultValue={q}
|
|
placeholder={t("searchPermission")}
|
|
className="flex-1 min-w-[200px]"
|
|
/>
|
|
<button type="submit" className="btn btn-primary">
|
|
{t("search")}
|
|
</button>
|
|
</form>
|
|
<Link
|
|
href="/admin/housekeeping?mode=manage"
|
|
className="btn btn-outline"
|
|
>
|
|
{t("manage")}
|
|
</Link>
|
|
<Link
|
|
href="/admin/housekeeping?mode=import&form=selected_ids"
|
|
className="btn btn-outline"
|
|
>
|
|
{t("importSelection")}
|
|
</Link>
|
|
</div>
|
|
|
|
{mode === "manage" ? (
|
|
<section>
|
|
<h2 className="admin-section-title">{t("addPermission")}</h2>
|
|
<form action={upsertPermission} className="admin-card">
|
|
<div className="flex gap-2 flex-wrap">
|
|
<input
|
|
name="permission"
|
|
placeholder={t("permissionKey")}
|
|
required
|
|
className="min-w-[200px]"
|
|
/>
|
|
<input
|
|
name="minRank"
|
|
type="number"
|
|
min={0}
|
|
placeholder={t("minRank")}
|
|
required
|
|
className="w-[110px]"
|
|
/>
|
|
<input
|
|
name="description"
|
|
placeholder={t("description")}
|
|
className="flex-1 min-w-[160px]"
|
|
/>
|
|
<button type="submit" className="btn btn-primary">
|
|
{t("save")}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
) : (
|
|
<section>
|
|
<h2 className="admin-section-title">{t("importPermissions")}</h2>
|
|
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
|
{t("importHint")}
|
|
</p>
|
|
<form action={upsertPermission} className="admin-card">
|
|
<div className="flex gap-2 flex-wrap">
|
|
<input
|
|
name="q"
|
|
placeholder={t("importPlaceholder")}
|
|
required
|
|
className="flex-1 min-w-[200px]"
|
|
/>
|
|
<button type="submit" className="btn btn-primary">
|
|
{t("import")}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
)}
|
|
|
|
<section>
|
|
<h2 className="admin-section-title">
|
|
{t("permissions")}{" "}
|
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/18 text-[var(--admin-text)]">
|
|
{filtered.length}
|
|
</span>
|
|
</h2>
|
|
{filtered.length === 0 ? (
|
|
<div className="admin-empty">{t("noPermissions")}</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th className="w-[40px]">#</th>
|
|
<th>{t("colPermission")}</th>
|
|
<th>{t("colDescription")}</th>
|
|
<th className="w-[200px]">{t("colMinRank")}</th>
|
|
<th className="w-[80px]" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map((p, i) => (
|
|
<tr
|
|
key={String(p.id)}
|
|
style={
|
|
mode === "import" && selectedIds.has(i)
|
|
? {
|
|
backgroundColor:
|
|
"color-mix(in srgb, var(--admin-accent) 5%, transparent)",
|
|
}
|
|
: {}
|
|
}
|
|
>
|
|
<td className="muted" style={{ width: "40px" }}>
|
|
{i + 1}
|
|
</td>
|
|
<td>
|
|
<strong>{p.permission}</strong>
|
|
</td>
|
|
<td className="muted">{p.description ?? "—"}</td>
|
|
<td>
|
|
<form
|
|
action={upsertPermission}
|
|
style={{ display: "flex", gap: "0.5rem" }}
|
|
>
|
|
<input
|
|
type="hidden"
|
|
name="permission"
|
|
value={p.permission}
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
name="description"
|
|
value={p.description ?? ""}
|
|
/>
|
|
<input
|
|
name="minRank"
|
|
type="number"
|
|
min={0}
|
|
defaultValue={p.minRank}
|
|
style={{ width: 90 }}
|
|
/>
|
|
<button type="submit" className="btn btn-outline">
|
|
{t("save")}
|
|
</button>
|
|
</form>
|
|
</td>
|
|
<td>
|
|
<form action={deletePermission}>
|
|
<input type="hidden" name="id" value={String(p.id)} />
|
|
<button type="submit" className="btn btn-danger">
|
|
✕
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|