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");
|
||||
}
|
||||
|
||||
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 { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
applyPreset,
|
||||
bulkDeletePermissions,
|
||||
bulkImportPermissions,
|
||||
clearAllPermissions,
|
||||
deletePermission,
|
||||
@@ -31,6 +32,61 @@ interface SharedProps {
|
||||
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) {
|
||||
const t = useTranslations("pages.admin.housekeeping");
|
||||
const router = useRouter();
|
||||
@@ -39,6 +95,11 @@ export function ManageSection(props: SharedProps) {
|
||||
const [groupFilter, setGroupFilter] = useState(sp.get("group") ?? "");
|
||||
const [page, setPage] = useState(1);
|
||||
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(() => {
|
||||
return props.permissions.filter((p) => {
|
||||
@@ -70,11 +131,26 @@ export function ManageSection(props: SharedProps) {
|
||||
[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(
|
||||
async (formData: FormData) => {
|
||||
try {
|
||||
await upsertPermission(formData);
|
||||
toast.success(t("saved"));
|
||||
if (formRef.current) formRef.current.reset();
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast.error(t("errorOccurred"));
|
||||
@@ -85,7 +161,6 @@ export function ManageSection(props: SharedProps) {
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
if (!confirm(t("confirmDelete"))) return;
|
||||
setPendingDel(id);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
@@ -97,12 +172,62 @@ export function ManageSection(props: SharedProps) {
|
||||
toast.error(t("errorOccurred"));
|
||||
}
|
||||
setPendingDel(null);
|
||||
setConfirmDel(null);
|
||||
},
|
||||
[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 (
|
||||
<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="flex gap-2 flex-1 flex-wrap">
|
||||
<input
|
||||
@@ -111,7 +236,7 @@ export function ManageSection(props: SharedProps) {
|
||||
const v = e.target.value;
|
||||
setSearch(v);
|
||||
setPage(1);
|
||||
syncUrl(v, groupFilter);
|
||||
debouncedSync(v, groupFilter);
|
||||
}}
|
||||
placeholder={t("searchPermission")}
|
||||
className="flex-1 min-w-[200px]"
|
||||
@@ -122,7 +247,7 @@ export function ManageSection(props: SharedProps) {
|
||||
const v = e.target.value;
|
||||
setGroupFilter(v);
|
||||
setPage(1);
|
||||
syncUrl(search, v);
|
||||
debouncedSync(search, v);
|
||||
}}
|
||||
className="w-[180px]"
|
||||
>
|
||||
@@ -141,6 +266,7 @@ export function ManageSection(props: SharedProps) {
|
||||
{t("addPermission")}
|
||||
</h2>
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleUpsert(new FormData(e.currentTarget));
|
||||
@@ -188,12 +314,24 @@ export function ManageSection(props: SharedProps) {
|
||||
</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>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="admin-section-title mb-0">
|
||||
{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>
|
||||
{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 ? (
|
||||
<div className="admin-empty">{t("noPermissions")}</div>
|
||||
) : (
|
||||
@@ -201,6 +339,16 @@ export function ManageSection(props: SharedProps) {
|
||||
<table>
|
||||
<thead>
|
||||
<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>{t("colPermission")}</th>
|
||||
<th>{t("colDescription")}</th>
|
||||
@@ -211,100 +359,110 @@ export function ManageSection(props: SharedProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginated.map((p, i) => (
|
||||
<tr key={String(p.id)}>
|
||||
<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" }}
|
||||
>
|
||||
{paginated.map((p, i) => {
|
||||
const sid = String(p.id);
|
||||
return (
|
||||
<tr key={sid}>
|
||||
<td>
|
||||
<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 }}
|
||||
type="checkbox"
|
||||
checked={selected.has(sid)}
|
||||
onChange={() => toggleSelect(sid)}
|
||||
/>
|
||||
</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
|
||||
type="submit"
|
||||
className="btn btn-outline btn-sm"
|
||||
type="button"
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => setConfirmDel(sid)}
|
||||
disabled={pendingDel === sid}
|
||||
>
|
||||
{t("save")}
|
||||
{pendingDel === sid ? "..." : "\u2715"}
|
||||
</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
|
||||
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>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -337,10 +495,13 @@ export function ManageSection(props: SharedProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ────────── Presets Section ────────── */
|
||||
|
||||
export function PresetsSection() {
|
||||
const t = useTranslations("pages.admin.housekeeping");
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
|
||||
const handleApplyPreset = useCallback(
|
||||
async (preset: string) => {
|
||||
@@ -360,7 +521,6 @@ export function PresetsSection() {
|
||||
);
|
||||
|
||||
const handleClearAll = useCallback(async () => {
|
||||
if (!confirm(t("confirmClearAll"))) return;
|
||||
setBusy("__clear__");
|
||||
try {
|
||||
await clearAllPermissions();
|
||||
@@ -370,10 +530,19 @@ export function PresetsSection() {
|
||||
toast.error(t("errorOccurred"));
|
||||
}
|
||||
setBusy(null);
|
||||
setConfirmClear(false);
|
||||
}, [router, t]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ConfirmModal
|
||||
open={confirmClear}
|
||||
title={t("confirmClearAll")}
|
||||
message={t("confirmClearAllDesc")}
|
||||
onConfirm={handleClearAll}
|
||||
onCancel={() => setConfirmClear(false)}
|
||||
/>
|
||||
|
||||
<h2 className="admin-section-title">{t("presets")}</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] -mt-2 mb-3">
|
||||
{t("presetsHint")}
|
||||
@@ -410,7 +579,7 @@ export function PresetsSection() {
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAll}
|
||||
onClick={() => setConfirmClear(true)}
|
||||
disabled={busy !== null}
|
||||
className="btn btn-danger"
|
||||
>
|
||||
@@ -421,6 +590,8 @@ export function PresetsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ────────── Test Section ────────── */
|
||||
|
||||
export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
||||
const t = useTranslations("pages.admin.housekeeping");
|
||||
const [testRank, setTestRank] = useState("");
|
||||
@@ -480,6 +651,8 @@ export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ────────── Import Section ────────── */
|
||||
|
||||
export function ImportSection() {
|
||||
const t = useTranslations("pages.admin.housekeeping");
|
||||
const router = useRouter();
|
||||
@@ -551,6 +724,8 @@ export function ImportSection() {
|
||||
);
|
||||
}
|
||||
|
||||
/* ────────── Export Section ────────── */
|
||||
|
||||
export function ExportSection() {
|
||||
const t = useTranslations("pages.admin.housekeeping");
|
||||
const [data, setData] = useState<string | null>(null);
|
||||
@@ -611,3 +786,99 @@ export function ExportSection() {
|
||||
</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 { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getAuditLogs } from "@/lib/services/audit";
|
||||
import {
|
||||
AuditSection,
|
||||
ExportSection,
|
||||
ImportSection,
|
||||
ManageSection,
|
||||
@@ -77,6 +79,11 @@ export default async function AdminHousekeeping({
|
||||
|
||||
const allGroups = [...new Set([...GROUPS, ...Object.keys(groupCounts)])];
|
||||
|
||||
const auditLogs = await getAuditLogs({
|
||||
search: "housekeeping",
|
||||
perPage: 25,
|
||||
}).catch(() => null);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="admin-tabs mb-4">
|
||||
@@ -308,20 +315,7 @@ export default async function AdminHousekeeping({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tab === "audit" && (
|
||||
<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>
|
||||
)}
|
||||
{tab === "audit" && <AuditSection initialLogs={auditLogs} />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Стаи",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Pokoje",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Værelser",
|
||||
|
||||
+11
-1
@@ -1922,7 +1922,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Räume",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Δωμάτια",
|
||||
|
||||
+11
-1
@@ -1895,6 +1895,12 @@
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"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",
|
||||
"colDepends": "Depends on",
|
||||
"colPermission": "Permission",
|
||||
@@ -1957,7 +1963,11 @@
|
||||
"auditTitle": "Permission audit log",
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"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": {
|
||||
"title": "Rooms",
|
||||
|
||||
+11
-1
@@ -1922,7 +1922,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Salas",
|
||||
|
||||
+11
-1
@@ -1922,7 +1922,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Salons",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Sobe",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Szobák",
|
||||
|
||||
+11
-1
@@ -1922,7 +1922,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Stanze",
|
||||
|
||||
+11
-1
@@ -1895,6 +1895,12 @@
|
||||
"cancel": "Annuleren"
|
||||
},
|
||||
"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",
|
||||
"colDepends": "Afhankelijk van",
|
||||
"colPermission": "Permissie",
|
||||
@@ -1957,7 +1963,11 @@
|
||||
"auditTitle": "Permissie auditlog",
|
||||
"auditHint": "Alle wijzigingen aan housekeeping permissies worden gelogd.",
|
||||
"auditLogsAt": "Auditlogs zijn beschikbaar op de Auditlog-pagina.",
|
||||
"viewAuditLogs": "Bekijk auditlogs"
|
||||
"viewAuditLogs": "Bekijk auditlogs",
|
||||
"auditTime": "Tijd",
|
||||
"auditUser": "Gebruiker",
|
||||
"auditAction": "Actie",
|
||||
"auditTarget": "Doel"
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Kamers",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Rom",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Pokoje",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Quartos",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Camerele",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Rooms",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Izby",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Собе",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Rum",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Odalar",
|
||||
|
||||
+11
-1
@@ -1958,7 +1958,17 @@
|
||||
"auditHint": "All changes to housekeeping permissions are logged.",
|
||||
"auditLogsAt": "Audit logs are available on the Audit log page.",
|
||||
"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": {
|
||||
"title": "Кімнати",
|
||||
|
||||
Reference in new issue
Block a user