This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+226
-178
@@ -1,198 +1,246 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deletePermission, upsertPermission } from "@/actions/admin-housekeeping";
|
||||
import {
|
||||
deletePermission,
|
||||
upsertPermission,
|
||||
} from "@/actions/admin-housekeeping";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminHousekeeping({
|
||||
searchParams,
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ q?: string; mode?: "manage" | "import" }>;
|
||||
searchParams: Promise<{ q?: string; mode?: "manage" | "import" }>;
|
||||
}) {
|
||||
const t = await getTranslations("pages.admin.housekeeping");
|
||||
const t = await getTranslations("pages.admin.housekeeping");
|
||||
|
||||
const sp = await searchParams;
|
||||
const q = (sp.q ?? "").trim();
|
||||
const mode = sp.mode === "import" ? "import" : "manage";
|
||||
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 = [];
|
||||
}
|
||||
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 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 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) => !isNaN(i));
|
||||
importIds.forEach((id) => selectedIds.add(id));
|
||||
}
|
||||
}
|
||||
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>
|
||||
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-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>
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user