refactor(housekeeping): modal confirm, bulk delete, search debounce, audit inline, form reset
Local Build and Deploy / deploy (push) Successful in 1m6s
Local Build and Deploy / deploy (push) Successful in 1m6s
- Replace browser confirm() with custom ConfirmModal component - Add search debounce (300ms) before syncing URL params - Reset add-permission form after successful submit - Add bulk select/delete with checkbox column and bulkDeletePermissions action - AuditSection fetches logs server-side via getAuditLogs and shows inline table - Add 10 new i18n keys (cancel, confirm, confirmDeletePermission, confirmBulkDelete, deleteSelected, confirmClearAllDesc, auditTime, auditUser, auditAction, auditTarget) synced to all 22 locales
This commit is contained in:
1 parent
d693d169dd
commit
f1e4e32a1e
25 files changed
+645
-137
No files matched your search
@@ -870,4 +870,27 @@ export async function clearAllPermissions(): Promise<void> {
|
|||||||
revalidatePath("/admin/housekeeping");
|
revalidatePath("/admin/housekeeping");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bulkDeletePermissions(
|
||||||
|
ids: bigint[],
|
||||||
|
): Promise<{ count: number }> {
|
||||||
|
await requirePermission(PERMS.SETTINGS_EDIT);
|
||||||
|
|
||||||
|
if (!ids.length) return { count: 0 };
|
||||||
|
|
||||||
|
const userId = await currentUserId();
|
||||||
|
|
||||||
|
await prisma.websiteHousekeepingPermissions.deleteMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await logAudit({
|
||||||
|
userId: userId ?? 0,
|
||||||
|
action: "housekeeping.permission.bulk_delete",
|
||||||
|
target: "WebsiteHousekeepingPermissions",
|
||||||
|
after: { deletedCount: ids.length },
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath("/admin/housekeeping");
|
||||||
|
return { count: ids.length };
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
applyPreset,
|
applyPreset,
|
||||||
|
bulkDeletePermissions,
|
||||||
bulkImportPermissions,
|
bulkImportPermissions,
|
||||||
clearAllPermissions,
|
clearAllPermissions,
|
||||||
deletePermission,
|
deletePermission,
|
||||||
@@ -31,6 +32,61 @@ interface SharedProps {
|
|||||||
permSet: Set<string>;
|
permSet: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Confirm Modal ────────── */
|
||||||
|
|
||||||
|
interface ConfirmModalProps {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
variant?: "danger" | "primary";
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmModal({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel,
|
||||||
|
variant = "danger",
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: ConfirmModalProps) {
|
||||||
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
|
if (!open) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="admin-card max-w-sm w-full mx-4 p-5 shadow-xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 className="font-bold text-lg mb-2">{title}</h3>
|
||||||
|
<p className="text-sm opacity-70 mb-5">{message}</p>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button type="button" onClick={onCancel} className="btn btn-outline">
|
||||||
|
{cancelLabel ?? t("cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onConfirm}
|
||||||
|
className={variant === "danger" ? "btn btn-danger" : "btn btn-primary"}
|
||||||
|
>
|
||||||
|
{confirmLabel ?? t("confirm")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────── Manage Section ────────── */
|
||||||
|
|
||||||
export function ManageSection(props: SharedProps) {
|
export function ManageSection(props: SharedProps) {
|
||||||
const t = useTranslations("pages.admin.housekeeping");
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -39,6 +95,11 @@ export function ManageSection(props: SharedProps) {
|
|||||||
const [groupFilter, setGroupFilter] = useState(sp.get("group") ?? "");
|
const [groupFilter, setGroupFilter] = useState(sp.get("group") ?? "");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pendingDel, setPendingDel] = useState<string | null>(null);
|
const [pendingDel, setPendingDel] = useState<string | null>(null);
|
||||||
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
const [busyBulk, setBusyBulk] = useState(false);
|
||||||
|
const [confirmDel, setConfirmDel] = useState<string | null>(null);
|
||||||
|
const searchTimer = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
return props.permissions.filter((p) => {
|
return props.permissions.filter((p) => {
|
||||||
@@ -70,11 +131,26 @@ export function ManageSection(props: SharedProps) {
|
|||||||
[router],
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debouncedSync = useCallback(
|
||||||
|
(s: string, g: string) => {
|
||||||
|
if (searchTimer.current) clearTimeout(searchTimer.current);
|
||||||
|
searchTimer.current = setTimeout(() => syncUrl(s, g), 300);
|
||||||
|
},
|
||||||
|
[syncUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (searchTimer.current) clearTimeout(searchTimer.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleUpsert = useCallback(
|
const handleUpsert = useCallback(
|
||||||
async (formData: FormData) => {
|
async (formData: FormData) => {
|
||||||
try {
|
try {
|
||||||
await upsertPermission(formData);
|
await upsertPermission(formData);
|
||||||
toast.success(t("saved"));
|
toast.success(t("saved"));
|
||||||
|
if (formRef.current) formRef.current.reset();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t("errorOccurred"));
|
toast.error(t("errorOccurred"));
|
||||||
@@ -85,7 +161,6 @@ export function ManageSection(props: SharedProps) {
|
|||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
if (!confirm(t("confirmDelete"))) return;
|
|
||||||
setPendingDel(id);
|
setPendingDel(id);
|
||||||
try {
|
try {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@@ -97,12 +172,62 @@ export function ManageSection(props: SharedProps) {
|
|||||||
toast.error(t("errorOccurred"));
|
toast.error(t("errorOccurred"));
|
||||||
}
|
}
|
||||||
setPendingDel(null);
|
setPendingDel(null);
|
||||||
|
setConfirmDel(null);
|
||||||
},
|
},
|
||||||
[router, t],
|
[router, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleBulkDelete = useCallback(async () => {
|
||||||
|
if (!selected.size) return;
|
||||||
|
setBusyBulk(true);
|
||||||
|
try {
|
||||||
|
const ids = [...selected].map((s) => BigInt(s));
|
||||||
|
await bulkDeletePermissions(ids);
|
||||||
|
toast.success(t("deleted"));
|
||||||
|
setSelected(new Set());
|
||||||
|
router.refresh();
|
||||||
|
} catch {
|
||||||
|
toast.error(t("errorOccurred"));
|
||||||
|
}
|
||||||
|
setBusyBulk(false);
|
||||||
|
setConfirmDel(null);
|
||||||
|
}, [selected, router, t]);
|
||||||
|
|
||||||
|
const toggleSelect = useCallback((id: string) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleAll = useCallback(() => {
|
||||||
|
if (selected.size === paginated.length) {
|
||||||
|
setSelected(new Set());
|
||||||
|
} else {
|
||||||
|
setSelected(new Set(paginated.map((p) => String(p.id))));
|
||||||
|
}
|
||||||
|
}, [paginated, selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<ConfirmModal
|
||||||
|
open={confirmDel !== null}
|
||||||
|
title={t("confirmDelete")}
|
||||||
|
message={
|
||||||
|
confirmDel === "__bulk__"
|
||||||
|
? t("confirmBulkDelete", { count: selected.size })
|
||||||
|
: t("confirmDeletePermission")
|
||||||
|
}
|
||||||
|
onConfirm={() =>
|
||||||
|
confirmDel === "__bulk__"
|
||||||
|
? handleBulkDelete()
|
||||||
|
: handleDelete(confirmDel!)
|
||||||
|
}
|
||||||
|
onCancel={() => setConfirmDel(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="admin-filter-bar">
|
<div className="admin-filter-bar">
|
||||||
<div className="flex gap-2 flex-1 flex-wrap">
|
<div className="flex gap-2 flex-1 flex-wrap">
|
||||||
<input
|
<input
|
||||||
@@ -111,7 +236,7 @@ export function ManageSection(props: SharedProps) {
|
|||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
setSearch(v);
|
setSearch(v);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
syncUrl(v, groupFilter);
|
debouncedSync(v, groupFilter);
|
||||||
}}
|
}}
|
||||||
placeholder={t("searchPermission")}
|
placeholder={t("searchPermission")}
|
||||||
className="flex-1 min-w-[200px]"
|
className="flex-1 min-w-[200px]"
|
||||||
@@ -122,7 +247,7 @@ export function ManageSection(props: SharedProps) {
|
|||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
setGroupFilter(v);
|
setGroupFilter(v);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
syncUrl(search, v);
|
debouncedSync(search, v);
|
||||||
}}
|
}}
|
||||||
className="w-[180px]"
|
className="w-[180px]"
|
||||||
>
|
>
|
||||||
@@ -141,6 +266,7 @@ export function ManageSection(props: SharedProps) {
|
|||||||
{t("addPermission")}
|
{t("addPermission")}
|
||||||
</h2>
|
</h2>
|
||||||
<form
|
<form
|
||||||
|
ref={formRef}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleUpsert(new FormData(e.currentTarget));
|
handleUpsert(new FormData(e.currentTarget));
|
||||||
@@ -188,12 +314,24 @@ export function ManageSection(props: SharedProps) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="admin-section-title">
|
<div className="flex items-center justify-between mb-3">
|
||||||
{t("permissions")}{" "}
|
<h2 className="admin-section-title mb-0">
|
||||||
<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)]">
|
{t("permissions")}{" "}
|
||||||
{filtered.length}
|
<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)]">
|
||||||
</span>
|
{filtered.length}
|
||||||
</h2>
|
</span>
|
||||||
|
</h2>
|
||||||
|
{selected.size > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setConfirmDel("__bulk__")}
|
||||||
|
disabled={busyBulk}
|
||||||
|
className="btn btn-danger btn-sm"
|
||||||
|
>
|
||||||
|
{t("deleteSelected", { count: selected.size })}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="admin-empty">{t("noPermissions")}</div>
|
<div className="admin-empty">{t("noPermissions")}</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -201,6 +339,16 @@ export function ManageSection(props: SharedProps) {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th className="w-[36px]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={
|
||||||
|
paginated.length > 0 &&
|
||||||
|
selected.size === paginated.length
|
||||||
|
}
|
||||||
|
onChange={toggleAll}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
<th className="w-[40px]">#</th>
|
<th className="w-[40px]">#</th>
|
||||||
<th>{t("colPermission")}</th>
|
<th>{t("colPermission")}</th>
|
||||||
<th>{t("colDescription")}</th>
|
<th>{t("colDescription")}</th>
|
||||||
@@ -211,100 +359,110 @@ export function ManageSection(props: SharedProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{paginated.map((p, i) => (
|
{paginated.map((p, i) => {
|
||||||
<tr key={String(p.id)}>
|
const sid = String(p.id);
|
||||||
<td className="muted w-[40px]">
|
return (
|
||||||
{(safePage - 1) * PAGE_SIZE + i + 1}
|
<tr key={sid}>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
|
||||||
<strong>{p.permission}</strong>
|
|
||||||
</td>
|
|
||||||
<td className="muted">{p.description || "\u2014"}</td>
|
|
||||||
<td>
|
|
||||||
{p.groupName ? (
|
|
||||||
<span className="inline-block px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/12 text-xs font-medium">
|
|
||||||
{p.groupName}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs opacity-50">\u2014</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleUpsert(new FormData(e.currentTarget));
|
|
||||||
}}
|
|
||||||
style={{ display: "flex", gap: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="checkbox"
|
||||||
name="permission"
|
checked={selected.has(sid)}
|
||||||
value={p.permission}
|
onChange={() => toggleSelect(sid)}
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="description"
|
|
||||||
value={p.description ?? ""}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="groupName"
|
|
||||||
value={p.groupName ?? ""}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="dependsOn"
|
|
||||||
value={p.dependsOn ?? ""}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
name="minRank"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
defaultValue={p.minRank}
|
|
||||||
style={{ width: 80 }}
|
|
||||||
/>
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="muted w-[40px]">
|
||||||
|
{(safePage - 1) * PAGE_SIZE + i + 1}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{p.permission}</strong>
|
||||||
|
</td>
|
||||||
|
<td className="muted">{p.description || "\u2014"}</td>
|
||||||
|
<td>
|
||||||
|
{p.groupName ? (
|
||||||
|
<span className="inline-block px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/12 text-xs font-medium">
|
||||||
|
{p.groupName}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs opacity-50">\u2014</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUpsert(new FormData(e.currentTarget));
|
||||||
|
}}
|
||||||
|
style={{ display: "flex", gap: "0.5rem" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="permission"
|
||||||
|
value={p.permission}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="description"
|
||||||
|
value={p.description ?? ""}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="groupName"
|
||||||
|
value={p.groupName ?? ""}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="dependsOn"
|
||||||
|
value={p.dependsOn ?? ""}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="minRank"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
defaultValue={p.minRank}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td className="text-xs">
|
||||||
|
{p.dependsOn ? (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
props.permSet.has(p.dependsOn)
|
||||||
|
? "opacity-70"
|
||||||
|
: "opacity-100 font-semibold"
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
!props.permSet.has(p.dependsOn)
|
||||||
|
? { color: "var(--color-danger)" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{p.dependsOn}
|
||||||
|
{!props.permSet.has(p.dependsOn) ? " " + t("missing") : ""}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="opacity-50">\u2014</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
className="btn btn-outline btn-sm"
|
className="btn btn-danger btn-sm"
|
||||||
|
onClick={() => setConfirmDel(sid)}
|
||||||
|
disabled={pendingDel === sid}
|
||||||
>
|
>
|
||||||
{t("save")}
|
{pendingDel === sid ? "..." : "\u2715"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
<td className="text-xs">
|
);
|
||||||
{p.dependsOn ? (
|
})}
|
||||||
<span
|
|
||||||
className={
|
|
||||||
props.permSet.has(p.dependsOn)
|
|
||||||
? "opacity-70"
|
|
||||||
: "opacity-100 font-semibold"
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
!props.permSet.has(p.dependsOn)
|
|
||||||
? { color: "var(--color-danger)" }
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{p.dependsOn}
|
|
||||||
{!props.permSet.has(p.dependsOn) ? " " + t("missing") : ""}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="opacity-50">\u2014</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-danger btn-sm"
|
|
||||||
onClick={() => handleDelete(String(p.id))}
|
|
||||||
disabled={pendingDel === String(p.id)}
|
|
||||||
>
|
|
||||||
{pendingDel === String(p.id) ? "..." : "\u2715"}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,10 +495,13 @@ export function ManageSection(props: SharedProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Presets Section ────────── */
|
||||||
|
|
||||||
export function PresetsSection() {
|
export function PresetsSection() {
|
||||||
const t = useTranslations("pages.admin.housekeeping");
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [busy, setBusy] = useState<string | null>(null);
|
const [busy, setBusy] = useState<string | null>(null);
|
||||||
|
const [confirmClear, setConfirmClear] = useState(false);
|
||||||
|
|
||||||
const handleApplyPreset = useCallback(
|
const handleApplyPreset = useCallback(
|
||||||
async (preset: string) => {
|
async (preset: string) => {
|
||||||
@@ -360,7 +521,6 @@ export function PresetsSection() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClearAll = useCallback(async () => {
|
const handleClearAll = useCallback(async () => {
|
||||||
if (!confirm(t("confirmClearAll"))) return;
|
|
||||||
setBusy("__clear__");
|
setBusy("__clear__");
|
||||||
try {
|
try {
|
||||||
await clearAllPermissions();
|
await clearAllPermissions();
|
||||||
@@ -370,10 +530,19 @@ export function PresetsSection() {
|
|||||||
toast.error(t("errorOccurred"));
|
toast.error(t("errorOccurred"));
|
||||||
}
|
}
|
||||||
setBusy(null);
|
setBusy(null);
|
||||||
|
setConfirmClear(false);
|
||||||
}, [router, t]);
|
}, [router, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
<ConfirmModal
|
||||||
|
open={confirmClear}
|
||||||
|
title={t("confirmClearAll")}
|
||||||
|
message={t("confirmClearAllDesc")}
|
||||||
|
onConfirm={handleClearAll}
|
||||||
|
onCancel={() => setConfirmClear(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
<h2 className="admin-section-title">{t("presets")}</h2>
|
<h2 className="admin-section-title">{t("presets")}</h2>
|
||||||
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
||||||
{t("presetsHint")}
|
{t("presetsHint")}
|
||||||
@@ -410,7 +579,7 @@ export function PresetsSection() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearAll}
|
onClick={() => setConfirmClear(true)}
|
||||||
disabled={busy !== null}
|
disabled={busy !== null}
|
||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
>
|
>
|
||||||
@@ -421,6 +590,8 @@ export function PresetsSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Test Section ────────── */
|
||||||
|
|
||||||
export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
||||||
const t = useTranslations("pages.admin.housekeeping");
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
const [testRank, setTestRank] = useState("");
|
const [testRank, setTestRank] = useState("");
|
||||||
@@ -480,6 +651,8 @@ export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Import Section ────────── */
|
||||||
|
|
||||||
export function ImportSection() {
|
export function ImportSection() {
|
||||||
const t = useTranslations("pages.admin.housekeeping");
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -551,6 +724,8 @@ export function ImportSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Export Section ────────── */
|
||||||
|
|
||||||
export function ExportSection() {
|
export function ExportSection() {
|
||||||
const t = useTranslations("pages.admin.housekeeping");
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
const [data, setData] = useState<string | null>(null);
|
const [data, setData] = useState<string | null>(null);
|
||||||
@@ -611,3 +786,99 @@ export function ExportSection() {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ────────── Audit Section ────────── */
|
||||||
|
|
||||||
|
interface AuditEntry {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
username: string | null;
|
||||||
|
action: string;
|
||||||
|
target: string;
|
||||||
|
targetId: number | null;
|
||||||
|
before: unknown;
|
||||||
|
after: unknown;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditSectionProps {
|
||||||
|
initialLogs: {
|
||||||
|
rows: AuditEntry[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
perPage: number;
|
||||||
|
lastPage: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuditSection({ initialLogs }: AuditSectionProps) {
|
||||||
|
const t = useTranslations("pages.admin.housekeeping");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!initialLogs || initialLogs.rows.length === 0) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2 className="admin-section-title">{t("auditTitle")}</h2>
|
||||||
|
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
||||||
|
{t("auditHint")}
|
||||||
|
</p>
|
||||||
|
<div className="admin-card">
|
||||||
|
<p className="text-sm opacity-70">{t("auditLogsAt")}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/admin/logs/audit")}
|
||||||
|
className="btn btn-outline mt-2"
|
||||||
|
>
|
||||||
|
{t("viewAuditLogs")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2 className="admin-section-title">{t("auditTitle")}</h2>
|
||||||
|
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
||||||
|
{t("auditHint")}
|
||||||
|
</p>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="w-[160px]">{t("auditTime")}</th>
|
||||||
|
<th className="w-[120px]">{t("auditUser")}</th>
|
||||||
|
<th>{t("auditAction")}</th>
|
||||||
|
<th>{t("auditTarget")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{initialLogs.rows.map((log) => (
|
||||||
|
<tr key={log.id}>
|
||||||
|
<td className="text-xs whitespace-nowrap">
|
||||||
|
{new Date(log.createdAt).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="text-xs">{log.username ?? "System"}</td>
|
||||||
|
<td className="text-xs font-mono">{log.action}</td>
|
||||||
|
<td className="text-xs">
|
||||||
|
{log.target}
|
||||||
|
{log.targetId != null ? ` #${log.targetId}` : ""}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-xs opacity-60 text-center">
|
||||||
|
{t("auditLogsAt")}{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/admin/logs/audit")}
|
||||||
|
className="underline hover:no-underline"
|
||||||
|
>
|
||||||
|
{t("viewAuditLogs")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import { redirect } from "next/navigation";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { getAuditLogs } from "@/lib/services/audit";
|
||||||
import {
|
import {
|
||||||
|
AuditSection,
|
||||||
ExportSection,
|
ExportSection,
|
||||||
ImportSection,
|
ImportSection,
|
||||||
ManageSection,
|
ManageSection,
|
||||||
@@ -77,6 +79,11 @@ export default async function AdminHousekeeping({
|
|||||||
|
|
||||||
const allGroups = [...new Set([...GROUPS, ...Object.keys(groupCounts)])];
|
const allGroups = [...new Set([...GROUPS, ...Object.keys(groupCounts)])];
|
||||||
|
|
||||||
|
const auditLogs = await getAuditLogs({
|
||||||
|
search: "housekeeping",
|
||||||
|
perPage: 25,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="admin-tabs mb-4">
|
<div className="admin-tabs mb-4">
|
||||||
@@ -308,20 +315,7 @@ export default async function AdminHousekeeping({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "audit" && (
|
{tab === "audit" && <AuditSection initialLogs={auditLogs} />}
|
||||||
<section>
|
|
||||||
<h2 className="admin-section-title">{t("auditTitle")}</h2>
|
|
||||||
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
|
||||||
{t("auditHint")}
|
|
||||||
</p>
|
|
||||||
<div className="admin-card">
|
|
||||||
<p className="text-sm opacity-70">{t("auditLogsAt")}</p>
|
|
||||||
<Link href="/admin/logs/audit" className="btn btn-outline mt-2">
|
|
||||||
{t("viewAuditLogs")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Стаи",
|
"title": "Стаи",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Pokoje",
|
"title": "Pokoje",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Værelser",
|
"title": "Værelser",
|
||||||
|
|||||||
+11
-1
@@ -1922,7 +1922,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Räume",
|
"title": "Räume",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Δωμάτια",
|
"title": "Δωμάτια",
|
||||||
|
|||||||
+11
-1
@@ -1895,6 +1895,12 @@
|
|||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
},
|
},
|
||||||
"confirmClearAll": "Delete ALL permissions? This action cannot be undone!",
|
"confirmClearAll": "Delete ALL permissions? This action cannot be undone!",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
"colGroup": "Group",
|
"colGroup": "Group",
|
||||||
"colDepends": "Depends on",
|
"colDepends": "Depends on",
|
||||||
"colPermission": "Permission",
|
"colPermission": "Permission",
|
||||||
@@ -1957,7 +1963,11 @@
|
|||||||
"auditTitle": "Permission audit log",
|
"auditTitle": "Permission audit log",
|
||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs"
|
"viewAuditLogs": "View audit logs",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Rooms",
|
"title": "Rooms",
|
||||||
|
|||||||
+11
-1
@@ -1922,7 +1922,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Salas",
|
"title": "Salas",
|
||||||
|
|||||||
+11
-1
@@ -1922,7 +1922,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Salons",
|
"title": "Salons",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Sobe",
|
"title": "Sobe",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Szobák",
|
"title": "Szobák",
|
||||||
|
|||||||
+11
-1
@@ -1922,7 +1922,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Stanze",
|
"title": "Stanze",
|
||||||
|
|||||||
+11
-1
@@ -1895,6 +1895,12 @@
|
|||||||
"cancel": "Annuleren"
|
"cancel": "Annuleren"
|
||||||
},
|
},
|
||||||
"confirmClearAll": "ALLE permissies verwijderen? Dit kan niet ongedaan worden gemaakt!",
|
"confirmClearAll": "ALLE permissies verwijderen? Dit kan niet ongedaan worden gemaakt!",
|
||||||
|
"confirmClearAllDesc": "Dit verwijdert alle permissies uit de database. Weet je het zeker?",
|
||||||
|
"confirmDeletePermission": "Deze permissie permanent verwijderen?",
|
||||||
|
"confirmBulkDelete": "{count} geselecteerde permissies verwijderen?",
|
||||||
|
"deleteSelected": "Verwijder {count} geselecteerd",
|
||||||
|
"cancel": "Annuleren",
|
||||||
|
"confirm": "Bevestigen",
|
||||||
"colGroup": "Groep",
|
"colGroup": "Groep",
|
||||||
"colDepends": "Afhankelijk van",
|
"colDepends": "Afhankelijk van",
|
||||||
"colPermission": "Permissie",
|
"colPermission": "Permissie",
|
||||||
@@ -1957,7 +1963,11 @@
|
|||||||
"auditTitle": "Permissie auditlog",
|
"auditTitle": "Permissie auditlog",
|
||||||
"auditHint": "Alle wijzigingen aan housekeeping permissies worden gelogd.",
|
"auditHint": "Alle wijzigingen aan housekeeping permissies worden gelogd.",
|
||||||
"auditLogsAt": "Auditlogs zijn beschikbaar op de Auditlog-pagina.",
|
"auditLogsAt": "Auditlogs zijn beschikbaar op de Auditlog-pagina.",
|
||||||
"viewAuditLogs": "Bekijk auditlogs"
|
"viewAuditLogs": "Bekijk auditlogs",
|
||||||
|
"auditTime": "Tijd",
|
||||||
|
"auditUser": "Gebruiker",
|
||||||
|
"auditAction": "Actie",
|
||||||
|
"auditTarget": "Doel"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Kamers",
|
"title": "Kamers",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Rom",
|
"title": "Rom",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Pokoje",
|
"title": "Pokoje",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Quartos",
|
"title": "Quartos",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Camerele",
|
"title": "Camerele",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Rooms",
|
"title": "Rooms",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Izby",
|
"title": "Izby",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Собе",
|
"title": "Собе",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Rum",
|
"title": "Rum",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Odalar",
|
"title": "Odalar",
|
||||||
|
|||||||
+11
-1
@@ -1958,7 +1958,17 @@
|
|||||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||||
"viewAuditLogs": "View audit logs",
|
"viewAuditLogs": "View audit logs",
|
||||||
"importedWithErrors": "Imported {count} permissions with {errors} errors"
|
"importedWithErrors": "Imported {count} permissions with {errors} errors",
|
||||||
|
"confirmClearAllDesc": "This will remove every permission entry from the database. Are you sure?",
|
||||||
|
"confirmDeletePermission": "Delete this permission permanently?",
|
||||||
|
"confirmBulkDelete": "Delete {count} selected permissions?",
|
||||||
|
"deleteSelected": "Delete {count} selected",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"auditTime": "Time",
|
||||||
|
"auditUser": "User",
|
||||||
|
"auditAction": "Action",
|
||||||
|
"auditTarget": "Target"
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Кімнати",
|
"title": "Кімнати",
|
||||||
|
|||||||
Reference in new issue
Block a user