feat(admin): P1 — clearer tickets labels, pagination, min_staff_rank, analytics errors
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
75cdfdebe4
commit
f150aea8b9
13 files changed
+311
-101
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,9 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ export default async function AdminCalendar() {
|
|||||||
const t = await getTranslations("pages.admin.calendar");
|
const t = await getTranslations("pages.admin.calendar");
|
||||||
|
|
||||||
let campaigns: Campaign[] = [];
|
let campaigns: Campaign[] = [];
|
||||||
|
let loadError = false;
|
||||||
try {
|
try {
|
||||||
campaigns = await prisma.calendarCampaigns.findMany({
|
campaigns = await prisma.calendarCampaigns.findMany({
|
||||||
select: {
|
select: {
|
||||||
@@ -43,27 +44,37 @@ export default async function AdminCalendar() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
orderBy: { id: "desc" },
|
orderBy: { id: "desc" },
|
||||||
|
take: 200,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
campaigns = [];
|
campaigns = [];
|
||||||
|
loadError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewardCounts = new Map<number, number>();
|
const rewardCounts = new Map<number, number>();
|
||||||
let totalRewards = 0;
|
let totalRewards = 0;
|
||||||
try {
|
try {
|
||||||
const rewards = await prisma.calendarRewards.findMany({
|
const grouped = await prisma.calendarRewards.groupBy({
|
||||||
select: { campaignId: true },
|
by: ["campaignId"],
|
||||||
|
_count: { _all: true },
|
||||||
});
|
});
|
||||||
totalRewards = rewards.length;
|
for (const g of grouped) {
|
||||||
for (const r of rewards) {
|
rewardCounts.set(g.campaignId, g._count._all);
|
||||||
rewardCounts.set(r.campaignId, (rewardCounts.get(r.campaignId) ?? 0) + 1);
|
totalRewards += g._count._all;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
loadError = true;
|
||||||
|
}
|
||||||
|
|
||||||
const enabledCount = campaigns.filter((c) => c.enabled === "1").length;
|
const enabledCount = campaigns.filter((c) => c.enabled === "1").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
|
{loadError ? (
|
||||||
|
<div className="mb-4 rounded-lg bg-[var(--admin-error-subtle)] p-3 text-xs text-[var(--admin-error)]">
|
||||||
|
Could not load some calendar data (database unavailable).
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
<StatusCard label={t("campaigns")} value={campaigns.length} icon="📅" />
|
<StatusCard label={t("campaigns")} value={campaigns.length} icon="📅" />
|
||||||
<StatusCard
|
<StatusCard
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export default async function ModerationDashboard() {
|
|||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const todayStart = now - (now % 86400);
|
const todayStart = now - (now % 86400);
|
||||||
|
const minStaffRank = await getMinStaffRank();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
openCfh,
|
openCfh,
|
||||||
@@ -46,7 +48,9 @@ export default async function ModerationDashboard() {
|
|||||||
prisma.supportTickets.count({ where: { state: 0 } }),
|
prisma.supportTickets.count({ where: { state: 0 } }),
|
||||||
prisma.supportTickets.count({ where: { state: 1 } }),
|
prisma.supportTickets.count({ where: { state: 1 } }),
|
||||||
prisma.ban.count({ where: { timestamp: { gte: todayStart } } }),
|
prisma.ban.count({ where: { timestamp: { gte: todayStart } } }),
|
||||||
prisma.user.count({ where: { online: "1", rank: { gte: 4 } } }),
|
prisma.user.count({
|
||||||
|
where: { online: "1", rank: { gte: minStaffRank } },
|
||||||
|
}),
|
||||||
prisma.supportTickets.findMany({
|
prisma.supportTickets.findMany({
|
||||||
orderBy: { id: "desc" },
|
orderBy: { id: "desc" },
|
||||||
take: 10,
|
take: 10,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { AvatarImage } from "@/components/shared/avatar-image";
|
import { AvatarImage } from "@/components/shared/avatar-image";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
@@ -11,9 +12,9 @@ export default async function ModTeamPage() {
|
|||||||
if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank))
|
if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank))
|
||||||
redirect("/admin");
|
redirect("/admin");
|
||||||
|
|
||||||
// Get all staff (rank >= 4)
|
const minStaffRank = await getMinStaffRank();
|
||||||
const staff = await prisma.user.findMany({
|
const staff = await prisma.user.findMany({
|
||||||
where: { rank: { gte: 4 } },
|
where: { rank: { gte: minStaffRank } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -77,9 +78,9 @@ export default async function ModTeamPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-4 text-center">
|
<CardContent className="pt-4 text-center">
|
||||||
<p className="text-2xl font-bold text-primary">
|
<p className="text-2xl font-bold text-primary">
|
||||||
{staff.filter((s) => s.rank >= 7).length}
|
{staff.filter((s) => s.rank > minStaffRank).length}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">Super Admins</p>
|
<p className="text-xs text-muted-foreground">Above min rank</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HABBO_GAMEDATA_HOTELS } from "@/lib/habbo-gamedata-hotel";
|
|
||||||
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
||||||
|
import { HABBO_GAMEDATA_HOTELS } from "@/lib/habbo-gamedata-hotel";
|
||||||
|
|
||||||
export type FieldType =
|
export type FieldType =
|
||||||
| "text"
|
| "text"
|
||||||
@@ -82,7 +82,8 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
{
|
{
|
||||||
key: "min_staff_rank",
|
key: "min_staff_rank",
|
||||||
label: "Minimum staff rank",
|
label: "Minimum staff rank",
|
||||||
description: "Rank shown on the public staff page.",
|
description:
|
||||||
|
"Minimum rank treated as staff (admin lists and public staff page).",
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: "7",
|
defaultValue: "7",
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
|||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
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 { AdminTicketDetail } from "./admin-ticket-detail";
|
import { AdminTicketDetail } from "./admin-ticket-detail";
|
||||||
@@ -45,9 +46,10 @@ export default async function AdminTicketDetailPage({
|
|||||||
|
|
||||||
if (!ticket) notFound();
|
if (!ticket) notFound();
|
||||||
|
|
||||||
// Get staff members for assignment
|
// Staff assignees: min_staff_rank and above
|
||||||
|
const minStaffRank = await getMinStaffRank();
|
||||||
const staffMembers = await prisma.user.findMany({
|
const staffMembers = await prisma.user.findMany({
|
||||||
where: { rank: { gte: 4 } },
|
where: { rank: { gte: minStaffRank } },
|
||||||
select: { id: true, username: true },
|
select: { id: true, username: true },
|
||||||
orderBy: { username: "asc" },
|
orderBy: { username: "asc" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
/** Minimal prev/next pager for admin list pages without DataTable. */
|
||||||
|
export function AdminSimplePager({
|
||||||
|
page,
|
||||||
|
lastPage,
|
||||||
|
total,
|
||||||
|
basePath,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
lastPage: number;
|
||||||
|
total: number;
|
||||||
|
basePath: string;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
|
if (lastPage <= 1) return null;
|
||||||
|
|
||||||
|
const href = (p: number) => {
|
||||||
|
const u = new URLSearchParams();
|
||||||
|
u.set("page", String(p));
|
||||||
|
return `${basePath}?${u.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-2 text-sm text-[var(--admin-text-muted)]">
|
||||||
|
<span>
|
||||||
|
{label ? `${label} · ` : null}
|
||||||
|
{total} total · page {page}/{lastPage}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{page > 1 ? (
|
||||||
|
<Link className="btn btn-ghost btn-sm" href={href(page - 1)}>
|
||||||
|
Previous
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="opacity-40">Previous</span>
|
||||||
|
)}
|
||||||
|
{page < lastPage ? (
|
||||||
|
<Link className="btn btn-ghost btn-sm" href={href(page + 1)}>
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="opacity-40">Next</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum hotel rank treated as staff (admin lists + public staff page).
|
||||||
|
* Setting: `min_staff_rank` (default 7).
|
||||||
|
*/
|
||||||
|
export async function getMinStaffRank(fallback = 7): Promise<number> {
|
||||||
|
const raw = await siteSettings.get("min_staff_rank", String(fallback));
|
||||||
|
const n = Number(raw);
|
||||||
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
||||||
|
}
|
||||||
+10
-7
@@ -1018,8 +1018,8 @@
|
|||||||
"advertisements": "Advertisements",
|
"advertisements": "Advertisements",
|
||||||
"writeableBoxes": "Writeable boxes",
|
"writeableBoxes": "Writeable boxes",
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"tickets": "Tickets",
|
"tickets": "Support desk",
|
||||||
"helpTickets": "Help center tickets",
|
"helpTickets": "Help-center tickets",
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"polls": "Polls",
|
"polls": "Polls",
|
||||||
"banners": "Banners",
|
"banners": "Banners",
|
||||||
@@ -1735,7 +1735,9 @@
|
|||||||
"orders": "Orders",
|
"orders": "Orders",
|
||||||
"recentOrders": "Recent orders",
|
"recentOrders": "Recent orders",
|
||||||
"paidOrders": "Paid orders",
|
"paidOrders": "Paid orders",
|
||||||
"ordersHint": "Latest PayPal transactions",
|
"ordersHint": "Latest PayPal transactions (most recent 25)",
|
||||||
|
"pageOf": "Page {page} of {lastPage}",
|
||||||
|
"packagesTotal": "{count} packages",
|
||||||
"newPackage": "+ New package",
|
"newPackage": "+ New package",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
@@ -2448,8 +2450,8 @@
|
|||||||
"events": "Events",
|
"events": "Events",
|
||||||
"eventTypes": "Event types",
|
"eventTypes": "Event types",
|
||||||
"polls": "Polls",
|
"polls": "Polls",
|
||||||
"tickets": "Tickets",
|
"tickets": "CMS tickets",
|
||||||
"helpTickets": "Help center tickets",
|
"helpTickets": "Help-center tickets",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"directory": "Directory",
|
"directory": "Directory",
|
||||||
"multiAccounts": "Multi-accounts",
|
"multiAccounts": "Multi-accounts",
|
||||||
@@ -2510,8 +2512,8 @@
|
|||||||
},
|
},
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"eventsSubtitle": "Hotel events and event types",
|
"eventsSubtitle": "Hotel events and event types",
|
||||||
"tickets": "Tickets",
|
"tickets": "Support desk",
|
||||||
"ticketsSubtitle": "Support tickets and reply templates",
|
"ticketsSubtitle": "CMS tickets, help-center tickets, and reply templates",
|
||||||
"staffAccess": "Ranks & permissions",
|
"staffAccess": "Ranks & permissions",
|
||||||
"staffAccessSubtitle": "Staff ranks, ACL permissions and housekeeping access",
|
"staffAccessSubtitle": "Staff ranks, ACL permissions and housekeeping access",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
@@ -2889,6 +2891,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
|
"loadError": "Some analytics queries failed. Numbers below may be incomplete — check the database connection and emulator tables.",
|
||||||
"totalUsers": "Total users",
|
"totalUsers": "Total users",
|
||||||
"onlineNow": "{count} online now",
|
"onlineNow": "{count} online now",
|
||||||
"weekRegistrations": "Week registrations",
|
"weekRegistrations": "Week registrations",
|
||||||
|
|||||||
@@ -952,7 +952,7 @@
|
|||||||
"advertisements": "Pubblicità",
|
"advertisements": "Pubblicità",
|
||||||
"writeableBoxes": "Box modificabili",
|
"writeableBoxes": "Box modificabili",
|
||||||
"photos": "Foto",
|
"photos": "Foto",
|
||||||
"tickets": "Ticket",
|
"tickets": "Assistenza",
|
||||||
"helpTickets": "Ticket centro assistenza",
|
"helpTickets": "Ticket centro assistenza",
|
||||||
"events": "Eventi",
|
"events": "Eventi",
|
||||||
"polls": "Sondaggi",
|
"polls": "Sondaggi",
|
||||||
@@ -1673,7 +1673,9 @@
|
|||||||
"orders": "Ordini",
|
"orders": "Ordini",
|
||||||
"recentOrders": "Ordini recenti",
|
"recentOrders": "Ordini recenti",
|
||||||
"paidOrders": "Ordini pagati",
|
"paidOrders": "Ordini pagati",
|
||||||
"ordersHint": "Ultime transazioni PayPal",
|
"ordersHint": "Ultime transazioni PayPal (ultime 25)",
|
||||||
|
"pageOf": "Pagina {page} di {lastPage}",
|
||||||
|
"packagesTotal": "{count} pacchetti",
|
||||||
"newPackage": "+ Nuovo pacchetto",
|
"newPackage": "+ Nuovo pacchetto",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
@@ -2665,7 +2667,7 @@
|
|||||||
"events": "Eventi",
|
"events": "Eventi",
|
||||||
"eventTypes": "Tipi evento",
|
"eventTypes": "Tipi evento",
|
||||||
"polls": "Sondaggi",
|
"polls": "Sondaggi",
|
||||||
"tickets": "Ticket",
|
"tickets": "Ticket CMS",
|
||||||
"helpTickets": "Ticket centro assistenza",
|
"helpTickets": "Ticket centro assistenza",
|
||||||
"templates": "Template",
|
"templates": "Template",
|
||||||
"directory": "Directory",
|
"directory": "Directory",
|
||||||
@@ -2727,8 +2729,8 @@
|
|||||||
},
|
},
|
||||||
"events": "Eventi",
|
"events": "Eventi",
|
||||||
"eventsSubtitle": "Eventi hotel e tipi evento",
|
"eventsSubtitle": "Eventi hotel e tipi evento",
|
||||||
"tickets": "Ticket",
|
"tickets": "Assistenza",
|
||||||
"ticketsSubtitle": "Ticket di supporto e template risposte",
|
"ticketsSubtitle": "Ticket CMS, ticket centro assistenza e template risposte",
|
||||||
"staffAccess": "Gradi e permessi",
|
"staffAccess": "Gradi e permessi",
|
||||||
"staffAccessSubtitle": "Gradi staff, permessi ACL e accesso housekeeping",
|
"staffAccessSubtitle": "Gradi staff, permessi ACL e accesso housekeeping",
|
||||||
"analytics": "Analitiche",
|
"analytics": "Analitiche",
|
||||||
@@ -2829,6 +2831,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
|
"loadError": "Alcune query analytics sono fallite. I numeri sotto possono essere incompleti — controlla DB e tabelle emulator.",
|
||||||
"totalUsers": "Utenti totali",
|
"totalUsers": "Utenti totali",
|
||||||
"onlineNow": "{count} online ora",
|
"onlineNow": "{count} online ora",
|
||||||
"weekRegistrations": "Registrazioni della settimana",
|
"weekRegistrations": "Registrazioni della settimana",
|
||||||
|
|||||||
@@ -997,7 +997,7 @@
|
|||||||
"advertisements": "Advertenties",
|
"advertisements": "Advertenties",
|
||||||
"writeableBoxes": "Bewerkbare boxes",
|
"writeableBoxes": "Bewerkbare boxes",
|
||||||
"photos": "Foto's",
|
"photos": "Foto's",
|
||||||
"tickets": "Tickets",
|
"tickets": "Supportbalie",
|
||||||
"helpTickets": "Helpcentrum-tickets",
|
"helpTickets": "Helpcentrum-tickets",
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"polls": "Polls",
|
"polls": "Polls",
|
||||||
@@ -1649,7 +1649,9 @@
|
|||||||
"orders": "Bestellingen",
|
"orders": "Bestellingen",
|
||||||
"recentOrders": "Recente bestellingen",
|
"recentOrders": "Recente bestellingen",
|
||||||
"paidOrders": "Betaalde bestellingen",
|
"paidOrders": "Betaalde bestellingen",
|
||||||
"ordersHint": "Laatste PayPal-transacties",
|
"ordersHint": "Laatste PayPal-transacties (meest recente 25)",
|
||||||
|
"pageOf": "Pagina {page} van {lastPage}",
|
||||||
|
"packagesTotal": "{count} pakketten",
|
||||||
"newPackage": "+ Nieuw pakket",
|
"newPackage": "+ Nieuw pakket",
|
||||||
"edit": "Bewerken",
|
"edit": "Bewerken",
|
||||||
"delete": "Verwijderen",
|
"delete": "Verwijderen",
|
||||||
@@ -2731,7 +2733,7 @@
|
|||||||
"events": "Evenementen",
|
"events": "Evenementen",
|
||||||
"eventTypes": "Evenementtypen",
|
"eventTypes": "Evenementtypen",
|
||||||
"polls": "Polls",
|
"polls": "Polls",
|
||||||
"tickets": "Tickets",
|
"tickets": "CMS-tickets",
|
||||||
"helpTickets": "Helpcentrum-tickets",
|
"helpTickets": "Helpcentrum-tickets",
|
||||||
"templates": "Sjablonen",
|
"templates": "Sjablonen",
|
||||||
"directory": "Adresboek",
|
"directory": "Adresboek",
|
||||||
@@ -2793,8 +2795,8 @@
|
|||||||
},
|
},
|
||||||
"events": "Evenementen",
|
"events": "Evenementen",
|
||||||
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
|
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
|
||||||
"tickets": "Tickets",
|
"tickets": "Supportbalie",
|
||||||
"ticketsSubtitle": "Supporttickets en antwoordtemplates",
|
"ticketsSubtitle": "CMS-tickets, helpcentrum-tickets en antwoordtemplates",
|
||||||
"staffAccess": "Rangen & rechten",
|
"staffAccess": "Rangen & rechten",
|
||||||
"staffAccessSubtitle": "Staffrangen, ACL-rechten en housekeeping-toegang",
|
"staffAccessSubtitle": "Staffrangen, ACL-rechten en housekeeping-toegang",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
@@ -2872,6 +2874,7 @@
|
|||||||
"colStatus": "Status"
|
"colStatus": "Status"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
|
"loadError": "Sommige analytics-queries zijn mislukt. Onderstaande cijfers kunnen onvolledig zijn — controleer de database en emulator-tabellen.",
|
||||||
"totalUsers": "Totaal gebruikers",
|
"totalUsers": "Totaal gebruikers",
|
||||||
"onlineNow": "{count} nu online",
|
"onlineNow": "{count} nu online",
|
||||||
"weekRegistrations": "Registraties deze week",
|
"weekRegistrations": "Registraties deze week",
|
||||||
|
|||||||
Reference in new issue
Block a user