feat(mod): help-center tickets queue with reduced PII
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
01126207dc
commit
b6b8625246
31 files changed
+502
-82
No files matched your search
@@ -25,12 +25,16 @@ function revalidateHelpCenterTicketPaths(ticketId: bigint) {
|
|||||||
const id = String(ticketId);
|
const id = String(ticketId);
|
||||||
revalidatePath("/admin/help-tickets");
|
revalidatePath("/admin/help-tickets");
|
||||||
revalidatePath(`/admin/help-tickets/${id}`);
|
revalidatePath(`/admin/help-tickets/${id}`);
|
||||||
|
revalidatePath("/mod/help-tickets");
|
||||||
|
revalidatePath(`/mod/help-tickets/${id}`);
|
||||||
revalidatePath("/help/tickets");
|
revalidatePath("/help/tickets");
|
||||||
revalidatePath(`/help/tickets/${id}`);
|
revalidatePath(`/help/tickets/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HELP_TICKET_EDIT = [PERMS.TICKETS_EDIT, PERMS.MOD_TICKETS_EDIT] as const;
|
||||||
|
|
||||||
export const replyHelpCenterTicket = adminAction(
|
export const replyHelpCenterTicket = adminAction(
|
||||||
{ permission: PERMS.TICKETS_EDIT, schema: replyHelpCenterTicketSchema },
|
{ permission: HELP_TICKET_EDIT, schema: replyHelpCenterTicketSchema },
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const ticketId = ctx.data.ticketId;
|
const ticketId = ctx.data.ticketId;
|
||||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||||
@@ -72,7 +76,7 @@ export const replyHelpCenterTicket = adminAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const closeHelpCenterTicket = adminAction(
|
export const closeHelpCenterTicket = adminAction(
|
||||||
{ permission: PERMS.TICKETS_EDIT, schema: helpCenterTicketIdSchema },
|
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const ticketId = ctx.data.ticketId;
|
const ticketId = ctx.data.ticketId;
|
||||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||||
@@ -104,7 +108,7 @@ export const closeHelpCenterTicket = adminAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const reopenHelpCenterTicket = adminAction(
|
export const reopenHelpCenterTicket = adminAction(
|
||||||
{ permission: PERMS.TICKETS_EDIT, schema: helpCenterTicketIdSchema },
|
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const ticketId = ctx.data.ticketId;
|
const ticketId = ctx.data.ticketId;
|
||||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||||
|
|||||||
@@ -42,10 +42,15 @@ export function AdminHelpTicketDetail({
|
|||||||
ticket,
|
ticket,
|
||||||
messages: initialMessages,
|
messages: initialMessages,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
listHref = "/admin/help-tickets",
|
||||||
|
hideRequesterContact = false,
|
||||||
}: {
|
}: {
|
||||||
ticket: HelpTicketInfo;
|
ticket: HelpTicketInfo;
|
||||||
messages: ThreadMessage[];
|
messages: ThreadMessage[];
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
listHref?: string;
|
||||||
|
/** Hide email/IP-style contact for mod-lite surfaces. */
|
||||||
|
hideRequesterContact?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("pages.admin.helpTickets");
|
const t = useTranslations("pages.admin.helpTickets");
|
||||||
const [messages, setMessages] = useState(initialMessages);
|
const [messages, setMessages] = useState(initialMessages);
|
||||||
@@ -92,7 +97,7 @@ export function AdminHelpTicketDetail({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<Link href="/admin/help-tickets">
|
<Link href={listHref}>
|
||||||
<Button variant="ghost" size="icon" aria-label={t("backToList")}>
|
<Button variant="ghost" size="icon" aria-label={t("backToList")}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -241,7 +246,11 @@ export function AdminHelpTicketDetail({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/users/show/${ticket.creator.id}`}
|
href={
|
||||||
|
hideRequesterContact
|
||||||
|
? `/u/${encodeURIComponent(ticket.creator.username)}`
|
||||||
|
: `/admin/users/show/${ticket.creator.id}`
|
||||||
|
}
|
||||||
className="flex items-center gap-2 text-sm font-medium hover:text-primary"
|
className="flex items-center gap-2 text-sm font-medium hover:text-primary"
|
||||||
>
|
>
|
||||||
<User className="h-3.5 w-3.5" />
|
<User className="h-3.5 w-3.5" />
|
||||||
@@ -250,7 +259,7 @@ export function AdminHelpTicketDetail({
|
|||||||
{t("rankLabel", { rank: ticket.creator.rank })}
|
{t("rankLabel", { rank: ticket.creator.rank })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
{ticket.creator.mail ? (
|
{!hideRequesterContact && ticket.creator.mail ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<Mail className="h-3 w-3" />
|
<Mail className="h-3 w-3" />
|
||||||
{ticket.creator.mail}
|
{ticket.creator.mail}
|
||||||
|
|||||||
@@ -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 { positiveBigInt } from "@/lib/api";
|
import { positiveBigInt } from "@/lib/api";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@@ -6,8 +7,6 @@ import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const STAFF_RANK_THRESHOLD = 4;
|
|
||||||
|
|
||||||
export default async function AdminHelpTicketDetailPage({
|
export default async function AdminHelpTicketDetailPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
@@ -70,6 +69,7 @@ export default async function AdminHelpTicketDetailPage({
|
|||||||
|
|
||||||
const userById = new Map(users.map((u) => [u.id, u]));
|
const userById = new Map(users.map((u) => [u.id, u]));
|
||||||
const openerId = ticket.userId;
|
const openerId = ticket.userId;
|
||||||
|
const minStaffRank = await getMinStaffRank();
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ export default async function AdminHelpTicketDetailPage({
|
|||||||
...replies.map((r) => {
|
...replies.map((r) => {
|
||||||
const user = userById.get(r.userId);
|
const user = userById.get(r.userId);
|
||||||
const isStaff =
|
const isStaff =
|
||||||
r.userId !== openerId && (user?.rank ?? 0) >= STAFF_RANK_THRESHOLD;
|
r.userId !== openerId && (user?.rank ?? 0) >= minStaffRank;
|
||||||
return {
|
return {
|
||||||
key: String(r.id),
|
key: String(r.id),
|
||||||
userId: r.userId,
|
userId: r.userId,
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export interface HelpTicketRow {
|
|||||||
|
|
||||||
export function HelpTicketsTable({
|
export function HelpTicketsTable({
|
||||||
data,
|
data,
|
||||||
|
basePath = "/admin/help-tickets",
|
||||||
}: {
|
}: {
|
||||||
data: PaginatedResult<HelpTicketRow>;
|
data: PaginatedResult<HelpTicketRow>;
|
||||||
|
basePath?: string;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("pages.admin.helpTickets");
|
const t = useTranslations("pages.admin.helpTickets");
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ export function HelpTicketsTable({
|
|||||||
#{row.id}
|
#{row.id}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/help-tickets/${row.id}`}
|
href={`${basePath}/${row.id}`}
|
||||||
className="font-medium truncate hover:underline"
|
className="font-medium truncate hover:underline"
|
||||||
>
|
>
|
||||||
{row.title}
|
{row.title}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { AdminHelpTicketDetail } from "@/app/admin/help-tickets/[id]/admin-help-ticket-detail";
|
||||||
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
|
import { positiveBigInt } from "@/lib/api";
|
||||||
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function ModHelpTicketDetailPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]);
|
||||||
|
|
||||||
|
const { session, permissions } = await getAdminContext();
|
||||||
|
const { id } = await params;
|
||||||
|
const ticketId = positiveBigInt(id);
|
||||||
|
if (!ticketId) notFound();
|
||||||
|
|
||||||
|
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||||
|
where: { id: ticketId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
title: true,
|
||||||
|
content: true,
|
||||||
|
open: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ticket) notFound();
|
||||||
|
|
||||||
|
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
|
||||||
|
where: { ticketId: ticket.id },
|
||||||
|
orderBy: { id: "asc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
content: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorIds = [
|
||||||
|
...new Set([
|
||||||
|
...(ticket.userId != null ? [ticket.userId] : []),
|
||||||
|
...replies.map((r) => r.userId),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
const users =
|
||||||
|
authorIds.length > 0
|
||||||
|
? await prisma.user.findMany({
|
||||||
|
where: { id: { in: authorIds } },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
rank: true,
|
||||||
|
mail: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const userById = new Map(users.map((u) => [u.id, u]));
|
||||||
|
const openerId = ticket.userId;
|
||||||
|
const minStaffRank = await getMinStaffRank();
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
key: "opening",
|
||||||
|
userId: openerId ?? 0,
|
||||||
|
username:
|
||||||
|
openerId != null
|
||||||
|
? (userById.get(openerId)?.username ?? `#${openerId}`)
|
||||||
|
: "—",
|
||||||
|
isStaff: false,
|
||||||
|
content: ticket.content,
|
||||||
|
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||||
|
},
|
||||||
|
...replies.map((r) => {
|
||||||
|
const user = userById.get(r.userId);
|
||||||
|
const isStaff =
|
||||||
|
r.userId !== openerId && (user?.rank ?? 0) >= minStaffRank;
|
||||||
|
return {
|
||||||
|
key: String(r.id),
|
||||||
|
userId: r.userId,
|
||||||
|
username: user?.username ?? `#${r.userId}`,
|
||||||
|
isStaff,
|
||||||
|
content: r.content,
|
||||||
|
createdAt: (r.createdAt ?? new Date()).toISOString(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const creator = openerId != null ? userById.get(openerId) : undefined;
|
||||||
|
|
||||||
|
const canEdit =
|
||||||
|
canAccess(permissions, PERMS.MOD_TICKETS_EDIT, session.user.rank) ||
|
||||||
|
canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminHelpTicketDetail
|
||||||
|
ticket={{
|
||||||
|
id: String(ticket.id),
|
||||||
|
title: ticket.title,
|
||||||
|
open: ticket.open,
|
||||||
|
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||||
|
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
|
||||||
|
creator: creator
|
||||||
|
? {
|
||||||
|
id: creator.id,
|
||||||
|
username: creator.username,
|
||||||
|
rank: creator.rank,
|
||||||
|
mail: creator.mail ?? "",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}}
|
||||||
|
messages={messages}
|
||||||
|
canEdit={canEdit}
|
||||||
|
listHref="/mod/help-tickets"
|
||||||
|
hideRequesterContact
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import {
|
||||||
|
type HelpTicketRow,
|
||||||
|
HelpTicketsTable,
|
||||||
|
} from "@/app/admin/help-tickets/help-tickets-table";
|
||||||
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
|
import type { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { requireModPermission } from "@/lib/admin/guard";
|
||||||
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function ModHelpTicketsPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
|
}) {
|
||||||
|
await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]);
|
||||||
|
|
||||||
|
const t = await getTranslations("pages.admin.helpTickets");
|
||||||
|
const raw = await searchParams;
|
||||||
|
const parsed = parseListParams(new URLSearchParams(raw));
|
||||||
|
const statusFilter = raw.filter_status || "open";
|
||||||
|
|
||||||
|
const conditions: Prisma.WebsiteHelpCenterTicketsWhereInput[] = [];
|
||||||
|
|
||||||
|
if (statusFilter === "open") {
|
||||||
|
conditions.push({ open: true });
|
||||||
|
} else if (statusFilter === "closed") {
|
||||||
|
conditions.push({ open: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.search.trim()) {
|
||||||
|
const q = parsed.search.trim();
|
||||||
|
const asId = /^\d+$/.test(q) ? BigInt(q) : null;
|
||||||
|
const matchingUsers = await prisma.user
|
||||||
|
.findMany({
|
||||||
|
where: { username: { contains: q } },
|
||||||
|
select: { id: true },
|
||||||
|
take: 50,
|
||||||
|
})
|
||||||
|
.catch(() => []);
|
||||||
|
|
||||||
|
conditions.push({
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: q } },
|
||||||
|
{ content: { contains: q } },
|
||||||
|
...(matchingUsers.length > 0
|
||||||
|
? [{ userId: { in: matchingUsers.map((u) => u.id) } }]
|
||||||
|
: []),
|
||||||
|
...(asId !== null ? [{ id: asId }] : []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const where: Prisma.WebsiteHelpCenterTicketsWhereInput =
|
||||||
|
conditions.length === 0
|
||||||
|
? {}
|
||||||
|
: conditions.length === 1
|
||||||
|
? conditions[0]
|
||||||
|
: { AND: conditions };
|
||||||
|
|
||||||
|
const SORT_MAP: Record<
|
||||||
|
string,
|
||||||
|
Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput
|
||||||
|
> = {
|
||||||
|
title: { title: "asc" },
|
||||||
|
open: { open: "desc" },
|
||||||
|
user: { userId: "asc" },
|
||||||
|
date: { createdAt: "desc" },
|
||||||
|
updated: { updatedAt: "desc" },
|
||||||
|
id: { id: "desc" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" };
|
||||||
|
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||||
|
const orderBy = {
|
||||||
|
[orderField]: parsed.order,
|
||||||
|
} as Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput;
|
||||||
|
|
||||||
|
const [total, openCount, closedCount] = await Promise.all([
|
||||||
|
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
|
||||||
|
prisma.websiteHelpCenterTickets
|
||||||
|
.count({ where: { open: true } })
|
||||||
|
.catch(() => 0),
|
||||||
|
prisma.websiteHelpCenterTickets
|
||||||
|
.count({ where: { open: false } })
|
||||||
|
.catch(() => 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||||
|
|
||||||
|
const tickets = await prisma.websiteHelpCenterTickets
|
||||||
|
.findMany({
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
skip: pagination.offset,
|
||||||
|
take: pagination.perPage,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
title: true,
|
||||||
|
open: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => []);
|
||||||
|
|
||||||
|
const ticketIds = tickets.map((ticket) => ticket.id);
|
||||||
|
const userIds = [
|
||||||
|
...new Set(
|
||||||
|
tickets
|
||||||
|
.map((ticket) => ticket.userId)
|
||||||
|
.filter((id): id is number => id != null),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [replyGroups, users] = await Promise.all([
|
||||||
|
ticketIds.length > 0
|
||||||
|
? prisma.websiteHelpCenterTicketReplies
|
||||||
|
.groupBy({
|
||||||
|
by: ["ticketId"],
|
||||||
|
where: { ticketId: { in: ticketIds } },
|
||||||
|
_count: true,
|
||||||
|
})
|
||||||
|
.catch(() => [])
|
||||||
|
: Promise.resolve([]),
|
||||||
|
userIds.length > 0
|
||||||
|
? prisma.user
|
||||||
|
.findMany({
|
||||||
|
where: { id: { in: userIds } },
|
||||||
|
select: { id: true, username: true },
|
||||||
|
})
|
||||||
|
.catch(() => [])
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const replyCountByTicket = new Map(
|
||||||
|
replyGroups.map((g) => [String(g.ticketId), g._count]),
|
||||||
|
);
|
||||||
|
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||||
|
|
||||||
|
const rows: HelpTicketRow[] = tickets.map((ticket) => ({
|
||||||
|
id: String(ticket.id),
|
||||||
|
title: ticket.title,
|
||||||
|
user:
|
||||||
|
ticket.userId != null
|
||||||
|
? (usernameById.get(ticket.userId) ?? `#${ticket.userId}`)
|
||||||
|
: "—",
|
||||||
|
userId: ticket.userId,
|
||||||
|
replies: replyCountByTicket.get(String(ticket.id)) ?? 0,
|
||||||
|
open: ticket.open,
|
||||||
|
date: formatDate(ticket.createdAt, "date"),
|
||||||
|
updated: formatDate(ticket.updatedAt, "date"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
|
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{total === 0 && !parsed.search && statusFilter === "open" ? (
|
||||||
|
<div className="admin-empty">{t("noTickets")}</div>
|
||||||
|
) : (
|
||||||
|
<HelpTicketsTable
|
||||||
|
basePath="/mod/help-tickets"
|
||||||
|
data={{
|
||||||
|
rows,
|
||||||
|
total,
|
||||||
|
page: pagination.page,
|
||||||
|
perPage: pagination.perPage,
|
||||||
|
lastPage: pagination.lastPage,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-1
@@ -1,4 +1,13 @@
|
|||||||
import { Ban, Gavel, LogOut, Radio, Shield, Ticket, Users } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
Gavel,
|
||||||
|
LifeBuoy,
|
||||||
|
LogOut,
|
||||||
|
Radio,
|
||||||
|
Shield,
|
||||||
|
Ticket,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -59,6 +68,12 @@ export default async function ModLayout({ children }: { children: ReactNode }) {
|
|||||||
icon: Ticket,
|
icon: Ticket,
|
||||||
show: showTickets,
|
show: showTickets,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/mod/help-tickets",
|
||||||
|
label: t("nav.helpTickets"),
|
||||||
|
icon: LifeBuoy,
|
||||||
|
show: showTickets,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/mod/actions",
|
href: "/mod/actions",
|
||||||
label: t("nav.actions"),
|
label: t("nav.actions"),
|
||||||
|
|||||||
+59
-27
@@ -1,4 +1,12 @@
|
|||||||
import { Ban, Gavel, Radio, Shield, Ticket, Users } from "lucide-react";
|
import {
|
||||||
|
Ban,
|
||||||
|
Gavel,
|
||||||
|
LifeBuoy,
|
||||||
|
Radio,
|
||||||
|
Shield,
|
||||||
|
Ticket,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
@@ -30,24 +38,30 @@ export default async function ModDashboardPage() {
|
|||||||
canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) ||
|
canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) ||
|
||||||
canAccess(permissions, PERMS.MODERATION_VIEW, rank);
|
canAccess(permissions, PERMS.MODERATION_VIEW, rank);
|
||||||
|
|
||||||
const [openCfh, todayBans, modsOnline, openTickets] = await Promise.all([
|
const [openCfh, todayBans, modsOnline, openTickets, openHelpTickets] =
|
||||||
showCfh
|
await Promise.all([
|
||||||
? prisma.supportTickets.count({ where: { state: 0 } }).catch(() => 0)
|
showCfh
|
||||||
: Promise.resolve(0),
|
? prisma.supportTickets.count({ where: { state: 0 } }).catch(() => 0)
|
||||||
showBans
|
: Promise.resolve(0),
|
||||||
? prisma.ban
|
showBans
|
||||||
.count({ where: { timestamp: { gte: todayStart } } })
|
? prisma.ban
|
||||||
.catch(() => 0)
|
.count({ where: { timestamp: { gte: todayStart } } })
|
||||||
: Promise.resolve(0),
|
.catch(() => 0)
|
||||||
prisma.user
|
: Promise.resolve(0),
|
||||||
.count({ where: { online: "1", rank: { gte: minStaffRank } } })
|
prisma.user
|
||||||
.catch(() => 0),
|
.count({ where: { online: "1", rank: { gte: minStaffRank } } })
|
||||||
showTickets
|
.catch(() => 0),
|
||||||
? prisma.websiteTicket
|
showTickets
|
||||||
.count({ where: { status: { not: "closed" } } })
|
? prisma.websiteTicket
|
||||||
.catch(() => 0)
|
.count({ where: { status: { not: "closed" } } })
|
||||||
: Promise.resolve(0),
|
.catch(() => 0)
|
||||||
]);
|
: Promise.resolve(0),
|
||||||
|
showTickets
|
||||||
|
? prisma.websiteHelpCenterTickets
|
||||||
|
.count({ where: { open: true } })
|
||||||
|
.catch(() => 0)
|
||||||
|
: Promise.resolve(0),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -75,6 +89,14 @@ export default async function ModDashboardPage() {
|
|||||||
href="/mod/tickets"
|
href="/mod/tickets"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showTickets ? (
|
||||||
|
<StatsCard
|
||||||
|
title={t("stats.openHelpTickets")}
|
||||||
|
value={openHelpTickets}
|
||||||
|
icon={<LifeBuoy className="h-4 w-4" />}
|
||||||
|
href="/mod/help-tickets"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showBans ? (
|
{showBans ? (
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title={t("stats.bansToday")}
|
title={t("stats.bansToday")}
|
||||||
@@ -120,14 +142,24 @@ export default async function ModDashboardPage() {
|
|||||||
</li>
|
</li>
|
||||||
) : null}
|
) : null}
|
||||||
{showTickets ? (
|
{showTickets ? (
|
||||||
<li>
|
<>
|
||||||
<Link
|
<li>
|
||||||
href="/mod/tickets"
|
<Link
|
||||||
className="text-primary hover:underline"
|
href="/mod/tickets"
|
||||||
>
|
className="text-primary hover:underline"
|
||||||
{t("nav.tickets")}
|
>
|
||||||
</Link>
|
{t("nav.tickets")}
|
||||||
</li>
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/mod/help-tickets"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{t("nav.helpTickets")}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{showActions ? (
|
{showActions ? (
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const MOD_ROUTES: Array<[string, string]> = [
|
|||||||
["actions", "PERMS.MOD_ACTIONS"],
|
["actions", "PERMS.MOD_ACTIONS"],
|
||||||
["bans", "PERMS.MOD_BANS_VIEW"],
|
["bans", "PERMS.MOD_BANS_VIEW"],
|
||||||
["tickets", "PERMS.MOD_TICKETS_VIEW"],
|
["tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||||
|
["help-tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||||
["team", "PERMS.MOD_TEAM_VIEW"],
|
["team", "PERMS.MOD_TEAM_VIEW"],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3008,7 +3008,8 @@
|
|||||||
"bans": "Bans",
|
"bans": "Bans",
|
||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "CMS tickets",
|
||||||
|
"helpTickets": "Help-center tickets",
|
||||||
"team": "Team"
|
"team": "Team"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"modsOnline": "Mods online",
|
"modsOnline": "Mods online",
|
||||||
"quickActions": "Quick actions",
|
"quickActions": "Quick actions",
|
||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open CMS tickets",
|
||||||
|
"openHelpTickets": "Open help tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3015,7 +3015,8 @@
|
|||||||
"bans": "Ban",
|
"bans": "Ban",
|
||||||
"fullAdmin": "Admin completo",
|
"fullAdmin": "Admin completo",
|
||||||
"backToSite": "Torna al sito",
|
"backToSite": "Torna al sito",
|
||||||
"tickets": "Ticket",
|
"tickets": "Ticket CMS",
|
||||||
|
"helpTickets": "Ticket help center",
|
||||||
"team": "Team"
|
"team": "Team"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -3024,7 +3025,8 @@
|
|||||||
"modsOnline": "Mod online",
|
"modsOnline": "Mod online",
|
||||||
"quickActions": "Azioni rapide",
|
"quickActions": "Azioni rapide",
|
||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Ticket aperti",
|
"openTickets": "Ticket CMS aperti",
|
||||||
|
"openHelpTickets": "Help ticket aperti",
|
||||||
"team": "Team mod",
|
"team": "Team mod",
|
||||||
"teamHint": "Staff online e carico"
|
"teamHint": "Staff online e carico"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3013,7 +3013,8 @@
|
|||||||
"bans": "Bans",
|
"bans": "Bans",
|
||||||
"fullAdmin": "Volledige admin",
|
"fullAdmin": "Volledige admin",
|
||||||
"backToSite": "Terug naar site",
|
"backToSite": "Terug naar site",
|
||||||
"tickets": "Tickets",
|
"tickets": "CMS-tickets",
|
||||||
|
"helpTickets": "Helpcenter-tickets",
|
||||||
"team": "Team"
|
"team": "Team"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -3022,7 +3023,8 @@
|
|||||||
"modsOnline": "Mods online",
|
"modsOnline": "Mods online",
|
||||||
"quickActions": "Snelle acties",
|
"quickActions": "Snelle acties",
|
||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open CMS-tickets",
|
||||||
|
"openHelpTickets": "Open help-tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & werklast"
|
"teamHint": "Online staff & werklast"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3005,7 +3005,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3015,7 +3016,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3007,7 +3007,8 @@
|
|||||||
"fullAdmin": "Full admin",
|
"fullAdmin": "Full admin",
|
||||||
"backToSite": "Back to site",
|
"backToSite": "Back to site",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"team": "Team"
|
"team": "Team",
|
||||||
|
"helpTickets": "Help-center tickets"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"openCfh": "Open CFH",
|
"openCfh": "Open CFH",
|
||||||
@@ -3017,7 +3018,8 @@
|
|||||||
"quickActionsHint": "Kick, mute, alert",
|
"quickActionsHint": "Kick, mute, alert",
|
||||||
"openTickets": "Open tickets",
|
"openTickets": "Open tickets",
|
||||||
"team": "Mod team",
|
"team": "Mod team",
|
||||||
"teamHint": "Online staff & workload"
|
"teamHint": "Online staff & workload",
|
||||||
|
"openHelpTickets": "Open help tickets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user