refactor(housekeeping): modal confirm, bulk delete, search debounce, audit inline, form reset
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:
openhands committed 2026-07-17 19:53:53 +02:00
1 parent d693d169dd
commit f1e4e32a1e
25 files changed
+645 -137

No files matched your search

+23
View File
@@ -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 };
}
+372 -101
View File
@@ -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>
);
}
+8 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "Кімнати",