diff --git a/src/app/admin/moderation/actions/mod-actions-client.tsx b/src/app/admin/moderation/actions/mod-actions-client.tsx index 27ae40b5..323425a4 100644 --- a/src/app/admin/moderation/actions/mod-actions-client.tsx +++ b/src/app/admin/moderation/actions/mod-actions-client.tsx @@ -19,6 +19,7 @@ import { quickRoomKick, quickUnmute, } from "@/actions/moderation"; +import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -27,24 +28,65 @@ import { useServerAction } from "@/hooks/use-server-action"; export function ModActionsClient() { const { run, isPending } = useServerAction(); + const { confirm, dialog: confirmDialog } = useConfirmDialog(); - // User actions const [userId, setUserId] = useState(""); const [muteDuration, setMuteDuration] = useState("10"); const [alertMessage, setAlertMessage] = useState(""); - - // Room actions const [roomId, setRoomId] = useState(""); - - // Broadcast const [broadcastMsg, setBroadcastMsg] = useState(""); const uid = parseInt(userId, 10); const rid = parseInt(roomId, 10); + async function handleKick() { + const ok = await confirm({ + title: "Kick user", + description: `Disconnect user #${uid}?`, + confirmLabel: "Kick", + }); + if (!ok) return; + run(() => quickKick({ userId: uid }), { + successMessage: "User disconnected", + }); + } + + async function handleRoomKick() { + const ok = await confirm({ + title: "Kick all from room", + description: `Kick every user from room #${rid}?`, + confirmLabel: "Kick all", + }); + if (!ok) return; + run(() => quickRoomKick({ roomId: rid }), { + successMessage: "All users kicked from room", + }); + } + + async function handleHotelBroadcast() { + const ok = await confirm({ + title: "Hotel-wide alert", + description: "Send this alert to everyone in the hotel?", + confirmLabel: "Send", + variant: "default", + }); + if (!ok) return; + run( + () => + broadcastAlert({ + message: broadcastMsg.trim(), + type: "hotel", + }), + { + successMessage: "Hotel alert sent!", + onSuccess: () => setBroadcastMsg(""), + }, + ); + } + return (
- {/* User Actions */} + {confirmDialog} @@ -70,11 +112,7 @@ export function ModActionsClient() { variant="destructive" size="sm" disabled={isPending || !uid} - onClick={() => - run(() => quickKick({ userId: uid }), { - successMessage: "User disconnected", - }) - } + onClick={handleKick} > {isPending ? ( @@ -159,7 +197,6 @@ export function ModActionsClient() { - {/* Room Actions */}
@@ -185,18 +222,13 @@ export function ModActionsClient() { size="sm" className="w-full" disabled={isPending || !rid} - onClick={() => - run(() => quickRoomKick({ roomId: rid }), { - successMessage: "All users kicked from room", - }) - } + onClick={handleRoomKick} > Kick All From Room - {/* Broadcast */} @@ -216,19 +248,7 @@ export function ModActionsClient() { diff --git a/src/app/admin/moderation/cfh/cfh-table.tsx b/src/app/admin/moderation/cfh/cfh-table.tsx new file mode 100644 index 00000000..f5790cfa --- /dev/null +++ b/src/app/admin/moderation/cfh/cfh-table.tsx @@ -0,0 +1,117 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { DataTable } from "@/components/admin/data-table"; +import { Badge } from "@/components/ui/badge"; +import type { DataTableColumn, PaginatedResult } from "@/types"; + +export interface CfhRow { + id: number; + issue: string; + roomId: number; + when: string; + sender: string; + senderId: number; + reported: string; + reportedId: number; + mod: string; + state: number; + stateLabel: string; +} + +export function CfhTable({ data }: { data: PaginatedResult }) { + const t = useTranslations("pages.admin.cfh"); + + const columns: DataTableColumn[] = [ + { key: "id", label: t("colId"), sortable: true }, + { + key: "issue", + label: t("colIssue"), + sortable: true, + render: (_value, row) => ( +
+ + {row.issue || t("noDescription")} + +

+ {t("roomMeta", { room: row.roomId, when: row.when })} +

+
+ ), + }, + { + key: "sender", + label: t("colSender"), + sortable: true, + render: (_value, row) => + row.senderId ? ( + + {row.sender} + + ) : ( + "—" + ), + }, + { + key: "reported", + label: t("colReported"), + sortable: true, + render: (_value, row) => + row.reportedId ? ( + + {row.reported} + + ) : ( + "—" + ), + }, + { key: "mod", label: t("colMod"), sortable: true }, + { + key: "state", + label: t("colStatus"), + sortable: true, + filterKey: "filter_state", + filterOptions: [ + { label: t("stateOpen"), value: "0" }, + { label: t("statePicked"), value: "1" }, + { label: t("stateClosed"), value: "2" }, + ], + render: (_value, row) => { + const variant = + row.state === 0 + ? "destructive" + : row.state === 1 + ? "default" + : "secondary"; + return ( + + {row.stateLabel} + + ); + }, + }, + ]; + + return ( + + ); +} diff --git a/src/app/admin/moderation/cfh/page.tsx b/src/app/admin/moderation/cfh/page.tsx index a345be83..e2ed52a1 100644 --- a/src/app/admin/moderation/cfh/page.tsx +++ b/src/app/admin/moderation/cfh/page.tsx @@ -1,186 +1,157 @@ -import { AlertTriangle, CheckCircle2, Clock, Radio } from "lucide-react"; -import Link from "next/link"; import { redirect } from "next/navigation"; -import { Badge } from "@/components/ui/badge"; +import { getTranslations } from "next-intl/server"; +import type { Prisma } from "@/generated/prisma/client"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; +import { CfhTable, type CfhRow } from "./cfh-table"; -const CFH_STATES: Record< - number, - { label: string; class: string; icon: typeof AlertTriangle } -> = { - 0: { - label: "Open", - class: "bg-[var(--admin-error-subtle)] text-[var(--admin-error)]", - icon: AlertTriangle, - }, - 1: { - label: "Picked", - class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", - icon: Clock, - }, - 2: { - label: "Closed", - class: "bg-muted text-muted-foreground", - icon: CheckCircle2, - }, -}; +export const dynamic = "force-dynamic"; -interface PageProps { - searchParams: Promise<{ state?: string; page?: string }>; -} - -export default async function CfhListPage({ searchParams }: PageProps) { +export default async function CfhListPage({ + searchParams, +}: { + searchParams: Promise>; +}) { const { session, permissions } = await getAdminContext(); - if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank)) + if (!canAccess(permissions, PERMS.MODERATION_VIEW, session.user.rank)) { redirect("/admin"); + } - const sp = await searchParams; - const stateFilter = sp.state !== undefined ? Number(sp.state) : -1; - const page = Math.max(1, Number(sp.page) || 1); - const perPage = 25; + const t = await getTranslations("pages.admin.cfh"); + const raw = await searchParams; + const parsed = parseListParams(new URLSearchParams(raw)); + const stateRaw = raw.filter_state ?? raw.state; + const stateFilter = + stateRaw !== undefined && stateRaw !== "" ? Number(stateRaw) : -1; - const where = stateFilter >= 0 ? { state: stateFilter } : {}; + const conditions: Prisma.SupportTicketsWhereInput[] = []; + if (stateFilter >= 0) { + conditions.push({ state: stateFilter }); + } - const [tickets, total] = await Promise.all([ - prisma.supportTickets.findMany({ + if (parsed.search.trim()) { + const q = parsed.search.trim(); + const asId = Number(q); + let userIds: number[] = []; + try { + const users = await prisma.user.findMany({ + where: { username: { contains: q } }, + select: { id: true }, + take: 50, + }); + userIds = users.map((u) => u.id); + } catch { + userIds = []; + } + + conditions.push({ + OR: [ + { issue: { contains: q } }, + ...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []), + ...(userIds.length + ? [ + { senderId: { in: userIds } }, + { reportedId: { in: userIds } }, + { modId: { in: userIds } }, + ] + : []), + ], + }); + } + + const where: Prisma.SupportTicketsWhereInput = + conditions.length === 0 + ? {} + : conditions.length === 1 + ? conditions[0]! + : { AND: conditions }; + + const SORT_MAP: Record = + { + id: { id: "desc" }, + issue: { issue: "asc" }, + sender: { senderId: "asc" }, + reported: { reportedId: "asc" }, + mod: { modId: "asc" }, + state: { state: "asc" }, + }; + + const baseOrder = SORT_MAP[parsed.sort ?? "id"] ?? { id: "desc" }; + const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; + const orderBy = { + [orderField]: parsed.order, + } as Prisma.SupportTicketsOrderByWithRelationInput; + + const total = await prisma.supportTickets.count({ where }).catch(() => 0); + const pagination = calcPagination(total, parsed.page, parsed.perPage); + + const tickets = await prisma.supportTickets + .findMany({ where, - orderBy: { id: "desc" }, - skip: (page - 1) * perPage, - take: perPage, - }), - prisma.supportTickets.count({ where }), - ]); + orderBy, + skip: pagination.offset, + take: pagination.perPage, + }) + .catch(() => []); - // Fetch all related users const userIds = [ ...new Set([ - ...tickets.map((t) => t.senderId), - ...tickets.map((t) => t.reportedId), - ...tickets.map((t) => t.modId), + ...tickets.map((x) => x.senderId), + ...tickets.map((x) => x.reportedId), + ...tickets.map((x) => x.modId), ]), ].filter(Boolean); - const users = - userIds.length > 0 - ? await prisma.user.findMany({ - where: { id: { in: userIds } }, - select: { id: true, username: true }, - }) - : []; - const userMap = new Map(users.map((u) => [u.id, u.username])); + let userMap = new Map(); + if (userIds.length > 0) { + try { + const users = await prisma.user.findMany({ + where: { id: { in: userIds } }, + select: { id: true, username: true }, + }); + userMap = new Map(users.map((u) => [u.id, u.username])); + } catch { + userMap = new Map(); + } + } - const totalPages = Math.ceil(total / perPage); + const stateLabels: Record = { + 0: t("stateOpen"), + 1: t("statePicked"), + 2: t("stateClosed"), + }; + + const rows: CfhRow[] = tickets.map((ticket) => ({ + id: ticket.id, + issue: ticket.issue, + roomId: ticket.roomId, + when: ticket.timestamp + ? new Date(ticket.timestamp * 1000).toISOString().slice(0, 16).replace("T", " ") + : "—", + sender: userMap.get(ticket.senderId) ?? "—", + senderId: ticket.senderId, + reported: userMap.get(ticket.reportedId) ?? "—", + reportedId: ticket.reportedId, + mod: userMap.get(ticket.modId) ?? "—", + state: ticket.state, + stateLabel: stateLabels[ticket.state] ?? String(ticket.state), + })); return (
- {/* Filters */} -
- {[ - { key: -1, label: "All" }, - { key: 0, label: "Open" }, - { key: 1, label: "Picked" }, - { key: 2, label: "Closed" }, - ].map(({ key, label }) => ( - = 0 ? `?state=${key}` : ""}`} - > - - {label} - - - ))} -
- - {/* Table */} -
-
- # - Issue - Sender - Reported - Mod - Status -
- - {tickets.length === 0 ? ( -
- - No tickets found. -
- ) : ( - tickets.map((ticket) => { - const state = CFH_STATES[ticket.state] ?? CFH_STATES[0]; - const StateIcon = state.icon; - - return ( - - - {ticket.id} - -
-

- {ticket.issue || "No description"} -

-

- Room #{ticket.roomId} ·{" "} - {new Date(ticket.timestamp * 1000).toLocaleString()} -

-
- - {userMap.get(ticket.senderId) ?? "—"} - - - {userMap.get(ticket.reportedId) ?? "—"} - - - {userMap.get(ticket.modId) ?? "—"} - - - - {state.label} - - - ); - }) - )} -
- - {/* Pagination */} - {totalPages > 1 && ( -
- {page > 1 && ( - - - Previous - - - )} - - Page {page} of {totalPages} - - {page < totalPages && ( - - - Next - - - )} -
+ {total === 0 && !parsed.search && stateFilter < 0 ? ( +
{t("noTickets")}
+ ) : ( + )}
); diff --git a/src/app/admin/tickets/page.tsx b/src/app/admin/tickets/page.tsx index bb6bb1d2..dba3cb0a 100644 --- a/src/app/admin/tickets/page.tsx +++ b/src/app/admin/tickets/page.tsx @@ -1,250 +1,151 @@ -import { - AlertCircle, - CheckCircle2, - Clock, - Loader2, - Ticket, -} from "lucide-react"; -import Link from "next/link"; import { redirect } from "next/navigation"; -import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getTranslations } from "next-intl/server"; +import type { Prisma } from "@/generated/prisma/client"; +import { StatusCard } from "@/components/admin/dashboard"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; +import { TicketsTable, type TicketRow } from "./tickets-table"; -const STATUS_CONFIG = { - open: { - label: "Open", - icon: AlertCircle, - class: "bg-[var(--admin-info-subtle)] text-[var(--admin-info)]", - }, - in_progress: { - label: "In Progress", - icon: Loader2, - class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", - }, - waiting: { - label: "Waiting", - icon: Clock, - class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", - }, - closed: { - label: "Closed", - icon: CheckCircle2, - class: "bg-muted text-muted-foreground", - }, -} as const; +export const dynamic = "force-dynamic"; -const PRIORITY_COLORS = { - low: "", - normal: "text-[var(--admin-info)]", - high: "text-[var(--admin-warning)]", - urgent: "text-[var(--admin-error)] font-bold", -}; - -interface PageProps { - searchParams: Promise<{ status?: string; page?: string }>; +function fromDate(d: Date | null | undefined): string { + return d ? d.toISOString().slice(0, 10) : "—"; } -export default async function AdminTicketsPage({ searchParams }: PageProps) { +export default async function AdminTicketsPage({ + searchParams, +}: { + searchParams: Promise>; +}) { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank)) { redirect("/admin"); } - const sp = await searchParams; - const statusFilter = sp.status || "active"; - const page = Math.max(1, Number(sp.page) || 1); - const perPage = 20; + const t = await getTranslations("pages.admin.tickets"); + const raw = await searchParams; + const parsed = parseListParams(new URLSearchParams(raw)); + const statusFilter = raw.filter_status || raw.status || "active"; - const where = - statusFilter === "active" - ? { status: { not: "closed" } } - : statusFilter === "all" - ? {} - : { status: statusFilter }; + const conditions: Prisma.WebsiteTicketWhereInput[] = []; - const [tickets, total, stats] = await Promise.all([ - prisma.websiteTicket.findMany({ + if (statusFilter === "active") { + conditions.push({ status: { not: "closed" } }); + } else if (statusFilter && statusFilter !== "all") { + conditions.push({ status: statusFilter }); + } + + if (parsed.search.trim()) { + const q = parsed.search.trim(); + const asId = Number(q); + conditions.push({ + OR: [ + { subject: { contains: q } }, + { category: { contains: q } }, + { creator: { username: { contains: q } } }, + { assignee: { username: { contains: q } } }, + ...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []), + ], + }); + } + + const where: Prisma.WebsiteTicketWhereInput = + conditions.length === 0 + ? {} + : conditions.length === 1 + ? conditions[0]! + : { AND: conditions }; + + const SORT_MAP: Record = + { + subject: { subject: "asc" }, + status: { status: "asc" }, + priority: { priority: "asc" }, + assignee: { assigneeId: "asc" }, + date: { createdAt: "desc" }, + id: { id: "desc" }, + }; + + const baseOrder = SORT_MAP[parsed.sort ?? "date"] ?? { createdAt: "desc" }; + const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; + const orderBy = { + [orderField]: parsed.order, + } as Prisma.WebsiteTicketOrderByWithRelationInput; + + const [total, openCount, inProgressCount, waitingCount, closedCount] = + await Promise.all([ + prisma.websiteTicket.count({ where }).catch(() => 0), + prisma.websiteTicket.count({ where: { status: "open" } }).catch(() => 0), + prisma.websiteTicket + .count({ where: { status: "in_progress" } }) + .catch(() => 0), + prisma.websiteTicket + .count({ where: { status: "waiting" } }) + .catch(() => 0), + prisma.websiteTicket.count({ where: { status: "closed" } }).catch(() => 0), + ]); + + const pagination = calcPagination(total, parsed.page, parsed.perPage); + + const tickets = await prisma.websiteTicket + .findMany({ where, - orderBy: [{ createdAt: "desc" }], - skip: (page - 1) * perPage, - take: perPage, + orderBy, + skip: pagination.offset, + take: pagination.perPage, include: { creator: { select: { id: true, username: true } }, assignee: { select: { id: true, username: true } }, _count: { select: { messages: true } }, }, - }), - prisma.websiteTicket.count({ where }), - Promise.all([ - prisma.websiteTicket.count({ where: { status: "open" } }), - prisma.websiteTicket.count({ where: { status: "in_progress" } }), - prisma.websiteTicket.count({ where: { status: "waiting" } }), - prisma.websiteTicket.count({ where: { status: "closed" } }), - ]), - ]); + }) + .catch(() => []); - const [openCount, inProgressCount, waitingCount, closedCount] = stats; - const totalPages = Math.ceil(total / perPage); + const rows: TicketRow[] = tickets.map((ticket) => ({ + id: ticket.id, + subject: ticket.subject, + creator: ticket.creator.username, + creatorId: ticket.creatorId, + category: ticket.category, + messages: ticket._count.messages, + status: ticket.status, + priority: ticket.priority, + assignee: ticket.assignee?.username ?? "—", + date: fromDate(ticket.createdAt), + })); return (
- {/* Stats */} -
- - - - Open - - - -
{openCount}
-
-
- - - - In Progress - - - -
{inProgressCount}
-
-
- - - - Waiting - - - -
{waitingCount}
-
-
- - - - Closed - - - -
{closedCount}
-
-
+
+ + 0 ? "warn" : "neutral"} + icon="⏳" + /> + 0 ? "warn" : "neutral"} + icon="🕒" + /> +
- {/* Filter tabs */} -
- {[ - { key: "active", label: "Active" }, - { key: "open", label: "Open" }, - { key: "in_progress", label: "In Progress" }, - { key: "waiting", label: "Waiting" }, - { key: "closed", label: "Closed" }, - { key: "all", label: "All" }, - ].map(({ key, label }) => ( - - - {label} - - - ))} -
- - {/* Ticket list */} -
-
- Ticket - Status - Priority - Assigned - Date -
- - {tickets.length === 0 ? ( -
- - No tickets found. -
- ) : ( - tickets.map((ticket) => { - const status = - STATUS_CONFIG[ticket.status as keyof typeof STATUS_CONFIG] ?? - STATUS_CONFIG.open; - const StatusIcon = status.icon; - - return ( - -
-
- - #{ticket.id} - - - {ticket.subject} - -
-
- by {ticket.creator.username} ·{" "} - {ticket._count.messages} msg · {ticket.category} -
-
- - - {status.label} - - - {ticket.priority} - - - {ticket.assignee?.username ?? "—"} - - - {new Date(ticket.createdAt).toLocaleDateString()} - - - ); - }) - )} -
- - {/* Pagination */} - {totalPages > 1 && ( -
- {page > 1 && ( - - - Previous - - - )} - - Page {page} of {totalPages} - - {page < totalPages && ( - - - Next - - - )} -
+ {total === 0 && !parsed.search && statusFilter === "active" ? ( +
{t("noTickets")}
+ ) : ( + )}
); diff --git a/src/app/admin/tickets/tickets-table.tsx b/src/app/admin/tickets/tickets-table.tsx new file mode 100644 index 00000000..95336302 --- /dev/null +++ b/src/app/admin/tickets/tickets-table.tsx @@ -0,0 +1,131 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { DataTable } from "@/components/admin/data-table"; +import { Badge } from "@/components/ui/badge"; +import type { DataTableColumn, PaginatedResult } from "@/types"; + +export interface TicketRow { + id: number; + subject: string; + creator: string; + creatorId: number; + category: string; + messages: number; + status: string; + priority: string; + assignee: string; + date: string; +} + +const STATUS_VARIANT: Record< + string, + "default" | "secondary" | "destructive" | "outline" +> = { + open: "default", + in_progress: "secondary", + waiting: "outline", + closed: "secondary", +}; + +export function TicketsTable({ data }: { data: PaginatedResult }) { + const t = useTranslations("pages.admin.tickets"); + + const statusLabel: Record = { + open: t("statusOpen"), + in_progress: t("statusInProgress"), + waiting: t("statusWaiting"), + closed: t("statusClosed"), + }; + + const columns: DataTableColumn[] = [ + { + key: "subject", + label: t("colTicket"), + sortable: true, + render: (_value, row) => ( +
+
+ + #{row.id} + + + {row.subject} + +
+
+ {t("meta", { + user: row.creator, + count: row.messages, + category: row.category, + })} +
+
+ ), + }, + { + key: "status", + label: t("colStatus"), + sortable: true, + filterKey: "filter_status", + filterOptions: [ + { label: t("statusOpen"), value: "open" }, + { label: t("statusInProgress"), value: "in_progress" }, + { label: t("statusWaiting"), value: "waiting" }, + { label: t("statusClosed"), value: "closed" }, + ], + render: (value) => { + const status = String(value); + return ( + + {statusLabel[status] ?? status.replace("_", " ")} + + ); + }, + }, + { + key: "priority", + label: t("colPriority"), + sortable: true, + render: (value) => { + const p = String(value); + const className = + p === "urgent" + ? "text-[var(--admin-error)] font-bold" + : p === "high" + ? "text-[var(--admin-warning)]" + : p === "normal" + ? "text-[var(--admin-info)]" + : ""; + return {p}; + }, + }, + { key: "assignee", label: t("colAssigned"), sortable: true }, + { key: "date", label: t("colDate"), sortable: true }, + ]; + + return ( + + ); +} diff --git a/src/messages/en.json b/src/messages/en.json index f718ffb5..a11aa309 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -1092,6 +1092,40 @@ "saving": "Saving…", "settingsSaved": "VPN settings saved successfully" }, + "tickets": { + "title": "Tickets", + "subtitle": "Support tickets from players", + "noTickets": "No active tickets", + "searchPlaceholder": "Search subject, user, category or ID…", + "presetActive": "Active", + "statusOpen": "Open", + "statusInProgress": "In progress", + "statusWaiting": "Waiting", + "statusClosed": "Closed", + "colTicket": "Ticket", + "colStatus": "Status", + "colPriority": "Priority", + "colAssigned": "Assigned", + "colDate": "Date", + "meta": "by {user} · {count} msg · {category}" + }, + "cfh": { + "title": "Call for Help", + "subtitle": "In-game CFH / support tickets", + "noTickets": "No CFH tickets", + "searchPlaceholder": "Search issue, user or ID…", + "noDescription": "No description", + "roomMeta": "Room #{room} · {when}", + "stateOpen": "Open", + "statePicked": "Picked", + "stateClosed": "Closed", + "colId": "ID", + "colIssue": "Issue", + "colSender": "Sender", + "colReported": "Reported", + "colMod": "Mod", + "colStatus": "Status" + }, "applications": { "title": "Staff Applications", "subtitle": "Pending and reviewed staff applications", diff --git a/src/messages/it.json b/src/messages/it.json index aa0b1e79..cd82261f 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -1056,6 +1056,40 @@ "saving": "Salvataggio…", "settingsSaved": "Impostazioni VPN salvate con successo" }, + "tickets": { + "title": "Ticket", + "subtitle": "Ticket di supporto dai giocatori", + "noTickets": "Nessun ticket attivo", + "searchPlaceholder": "Cerca oggetto, utente, categoria o ID…", + "presetActive": "Attivi", + "statusOpen": "Aperti", + "statusInProgress": "In corso", + "statusWaiting": "In attesa", + "statusClosed": "Chiusi", + "colTicket": "Ticket", + "colStatus": "Stato", + "colPriority": "Priorità", + "colAssigned": "Assegnato", + "colDate": "Data", + "meta": "di {user} · {count} msg · {category}" + }, + "cfh": { + "title": "Call for Help", + "subtitle": "Ticket CFH in-game", + "noTickets": "Nessun ticket CFH", + "searchPlaceholder": "Cerca problema, utente o ID…", + "noDescription": "Nessuna descrizione", + "roomMeta": "Stanza #{room} · {when}", + "stateOpen": "Aperti", + "statePicked": "Presi in carico", + "stateClosed": "Chiusi", + "colId": "ID", + "colIssue": "Problema", + "colSender": "Mittente", + "colReported": "Segnalato", + "colMod": "Mod", + "colStatus": "Stato" + }, "applications": { "title": "Candidature staff", "subtitle": "Candidature allo staff in sospeso e revisionate",