diff --git a/src/app/admin/tickets/desk/page.tsx b/src/app/admin/tickets/desk/page.tsx new file mode 100644 index 00000000..8e80a3ce --- /dev/null +++ b/src/app/admin/tickets/desk/page.tsx @@ -0,0 +1,204 @@ +import { + and, + asc, + count, + desc, + eq, + like, + ne, + or, + type SQL, + sql, +} from "drizzle-orm"; +import { alias } from "drizzle-orm/mysql-core"; +import { redirect } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { StatusCard } from "@/components/admin/dashboard"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; +import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, User, WebsiteTicket } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; +import { type TicketRow, TicketsTable } from "../tickets-table"; + +export default async function AdminTicketsDeskPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const { session, permissions } = await getAdminContext(); + if (!canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank)) { + redirect("/admin"); + } + + 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 Creator = alias(User, "ticket_creator"); + const Assignee = alias(User, "ticket_assignee"); + + const conditions: SQL[] = []; + + if (statusFilter === "active") { + conditions.push(ne(WebsiteTicket.status, "closed")); + } else if (statusFilter && statusFilter !== "all") { + conditions.push(eq(WebsiteTicket.status, statusFilter)); + } + + if (parsed.search.trim()) { + const q = parsed.search.trim(); + const asId = Number(q); + const searchOr = [ + like(WebsiteTicket.subject, `%${q}%`), + like(WebsiteTicket.category, `%${q}%`), + like(Creator.username, `%${q}%`), + like(Assignee.username, `%${q}%`), + ...(Number.isFinite(asId) && asId > 0 + ? [eq(WebsiteTicket.id, asId)] + : []), + ]; + conditions.push(or(...searchOr)!); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const orderCol = + parsed.sort === "subject" + ? WebsiteTicket.subject + : parsed.sort === "status" + ? WebsiteTicket.status + : parsed.sort === "priority" + ? WebsiteTicket.priority + : parsed.sort === "assignee" + ? WebsiteTicket.assigneeId + : parsed.sort === "id" + ? WebsiteTicket.id + : WebsiteTicket.createdAt; + const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol); + + const [totalRows, openRows, inProgressRows, waitingRows, closedRows, queues] = + await Promise.all([ + db + .select({ total: count() }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) + .where(where) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "open")) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "in_progress")) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "waiting")) + .catch(() => [{ total: 0 }]), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "closed")) + .catch(() => [{ total: 0 }]), + fetchTicketQueueOpenCounts(), + ]); + + const total = Number(totalRows[0]?.total ?? 0); + const openCount = Number(openRows[0]?.total ?? 0); + const inProgressCount = Number(inProgressRows[0]?.total ?? 0); + const waitingCount = Number(waitingRows[0]?.total ?? 0); + const closedCount = Number(closedRows[0]?.total ?? 0); + + const pagination = calcPagination(total, parsed.page, parsed.perPage); + + const tickets = await db + .select({ + id: WebsiteTicket.id, + subject: WebsiteTicket.subject, + creatorId: WebsiteTicket.creatorId, + category: WebsiteTicket.category, + status: WebsiteTicket.status, + priority: WebsiteTicket.priority, + createdAt: WebsiteTicket.createdAt, + creatorUsername: Creator.username, + assigneeUsername: Assignee.username, + messageCount: sql`( + SELECT COUNT(*) FROM website_ticket_messages m + WHERE m.ticket_id = ${WebsiteTicket.id} + )`.mapWith(Number), + }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) + .where(where) + .orderBy(orderBy) + .limit(pagination.perPage) + .offset(pagination.offset) + .catch(() => []); + + const rows: TicketRow[] = tickets.map((ticket) => ({ + id: ticket.id, + subject: ticket.subject, + creator: ticket.creatorUsername ?? "—", + creatorId: ticket.creatorId, + category: ticket.category, + messages: Number(ticket.messageCount ?? 0), + status: ticket.status, + priority: ticket.priority, + assignee: ticket.assigneeUsername ?? "—", + date: formatDate(ticket.createdAt, "date"), + })); + + return ( +
+ +
+ + 0 ? "warn" : "neutral"} + icon="⏳" + /> + 0 ? "warn" : "neutral"} + icon="🕒" + /> + +
+ + {total === 0 && !parsed.search && statusFilter === "active" ? ( +
{t("noTickets")}
+ ) : ( + + )} +
+ ); +} diff --git a/src/app/admin/tickets/page.tsx b/src/app/admin/tickets/page.tsx index 287be477..b0f6b407 100644 --- a/src/app/admin/tickets/page.tsx +++ b/src/app/admin/tickets/page.tsx @@ -1,28 +1,16 @@ -import { - and, - asc, - count, - desc, - eq, - like, - ne, - or, - type SQL, - sql, -} from "drizzle-orm"; -import { alias } from "drizzle-orm/mysql-core"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; -import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; +import { UnifiedTicketsTable } from "@/components/admin/unified-tickets-table"; +import { + fetchUnifiedTicketInbox, + type TicketInboxTypeFilter, +} from "@/lib/admin/ticket-inbox"; import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; -import { calcPagination, parseListParams } from "@/lib/admin-helpers"; -import { db, User, WebsiteTicket } from "@/lib/db"; -import { formatDate } from "@/lib/format-date"; +import { parseListParams } from "@/lib/admin-helpers"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; -import { type TicketRow, TicketsTable } from "./tickets-table"; -export default async function AdminTicketsPage({ +export default async function AdminTicketInboxPage({ searchParams, }: { searchParams: Promise>; @@ -32,169 +20,55 @@ export default async function AdminTicketsPage({ redirect("/admin"); } - const t = await getTranslations("pages.admin.tickets"); + const t = await getTranslations("pages.admin.ticketInbox"); const raw = await searchParams; const parsed = parseListParams(new URLSearchParams(raw)); - const statusFilter = raw.filter_status || raw.status || "active"; + const typeRaw = raw.filter_type || "all"; + const type: TicketInboxTypeFilter = + typeRaw === "cms" || typeRaw === "help" ? typeRaw : "all"; + const statusFilter = raw.filter_status || "open"; + const openOnly = statusFilter !== "all"; - const Creator = alias(User, "ticket_creator"); - const Assignee = alias(User, "ticket_assignee"); - - const conditions: SQL[] = []; - - if (statusFilter === "active") { - conditions.push(ne(WebsiteTicket.status, "closed")); - } else if (statusFilter && statusFilter !== "all") { - conditions.push(eq(WebsiteTicket.status, statusFilter)); - } - - if (parsed.search.trim()) { - const q = parsed.search.trim(); - const asId = Number(q); - const searchOr = [ - like(WebsiteTicket.subject, `%${q}%`), - like(WebsiteTicket.category, `%${q}%`), - like(Creator.username, `%${q}%`), - like(Assignee.username, `%${q}%`), - ...(Number.isFinite(asId) && asId > 0 - ? [eq(WebsiteTicket.id, asId)] - : []), - ]; - conditions.push(or(...searchOr)!); - } - - const where = conditions.length > 0 ? and(...conditions) : undefined; - - const orderCol = - parsed.sort === "subject" - ? WebsiteTicket.subject - : parsed.sort === "status" - ? WebsiteTicket.status - : parsed.sort === "priority" - ? WebsiteTicket.priority - : parsed.sort === "assignee" - ? WebsiteTicket.assigneeId - : parsed.sort === "id" - ? WebsiteTicket.id - : WebsiteTicket.createdAt; - const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol); - - const [totalRows, openRows, inProgressRows, waitingRows, closedRows, queues] = - await Promise.all([ - db - .select({ total: count() }) - .from(WebsiteTicket) - .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) - .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) - .where(where) - .catch(() => [{ total: 0 }]), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "open")) - .catch(() => [{ total: 0 }]), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "in_progress")) - .catch(() => [{ total: 0 }]), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "waiting")) - .catch(() => [{ total: 0 }]), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "closed")) - .catch(() => [{ total: 0 }]), - fetchTicketQueueOpenCounts(), - ]); - - const total = Number(totalRows[0]?.total ?? 0); - const openCount = Number(openRows[0]?.total ?? 0); - const inProgressCount = Number(inProgressRows[0]?.total ?? 0); - const waitingCount = Number(waitingRows[0]?.total ?? 0); - const closedCount = Number(closedRows[0]?.total ?? 0); - - const pagination = calcPagination(total, parsed.page, parsed.perPage); - - const tickets = await db - .select({ - id: WebsiteTicket.id, - subject: WebsiteTicket.subject, - creatorId: WebsiteTicket.creatorId, - category: WebsiteTicket.category, - status: WebsiteTicket.status, - priority: WebsiteTicket.priority, - createdAt: WebsiteTicket.createdAt, - creatorUsername: Creator.username, - assigneeUsername: Assignee.username, - messageCount: sql`( - SELECT COUNT(*) FROM website_ticket_messages m - WHERE m.ticket_id = ${WebsiteTicket.id} - )`.mapWith(Number), - }) - .from(WebsiteTicket) - .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) - .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) - .where(where) - .orderBy(orderBy) - .limit(pagination.perPage) - .offset(pagination.offset) - .catch(() => []); - - const rows: TicketRow[] = tickets.map((ticket) => ({ - id: ticket.id, - subject: ticket.subject, - creator: ticket.creatorUsername ?? "—", - creatorId: ticket.creatorId, - category: ticket.category, - messages: Number(ticket.messageCount ?? 0), - status: ticket.status, - priority: ticket.priority, - assignee: ticket.assigneeUsername ?? "—", - date: formatDate(ticket.createdAt, "date"), - })); + const [inbox, queues] = await Promise.all([ + fetchUnifiedTicketInbox({ + page: parsed.page, + perPage: parsed.perPage, + search: parsed.search, + type, + openOnly, + base: "admin", + }), + fetchTicketQueueOpenCounts(), + ]); return (
- +

{t("hint")}

- + 0 ? "warn" : "neutral"} - icon="⏳" + label={t("statHelpOpen")} + value={queues.helpOpen} + state={queues.helpOpen > 0 ? "warn" : "neutral"} + icon="🛟" /> 0 ? "warn" : "neutral"} - icon="🕒" + label={t("statTotal")} + value={queues.cmsOpen + queues.helpOpen} + icon="📥" /> -
- {total === 0 && !parsed.search && statusFilter === "active" ? ( + {inbox.total === 0 && !parsed.search ? (
{t("noTickets")}
) : ( - )} diff --git a/src/app/mod/layout.tsx b/src/app/mod/layout.tsx index 07628de1..6215536e 100644 --- a/src/app/mod/layout.tsx +++ b/src/app/mod/layout.tsx @@ -2,7 +2,6 @@ import { eq } from "drizzle-orm"; import { Ban, Gavel, - LifeBuoy, LogOut, Radio, Shield, @@ -73,12 +72,6 @@ export default async function ModLayout({ children }: { children: ReactNode }) { icon: Ticket, show: showTickets, }, - { - href: "/mod/help-tickets", - label: t("nav.helpTickets"), - icon: LifeBuoy, - show: showTickets, - }, { href: "/mod/actions", label: t("nav.actions"), diff --git a/src/app/mod/tickets/desk/page.tsx b/src/app/mod/tickets/desk/page.tsx new file mode 100644 index 00000000..5ebb2bdf --- /dev/null +++ b/src/app/mod/tickets/desk/page.tsx @@ -0,0 +1,221 @@ +import { + and, + asc, + count, + desc, + eq, + inArray, + like, + ne, + or, + type SQL, +} from "drizzle-orm"; +import { alias } from "drizzle-orm/mysql-core"; +import { getTranslations } from "next-intl/server"; +import { + type TicketRow, + TicketsTable, +} from "@/app/admin/tickets/tickets-table"; +import { StatusCard } from "@/components/admin/dashboard"; +import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; +import { requireModPermission } from "@/lib/admin/guard"; +import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; +import { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; +import { PERMS } from "@/lib/permissions"; + +const Creator = alias(User, "ticket_creator"); +const Assignee = alias(User, "ticket_assignee"); + +const SORT_COLS = { + subject: WebsiteTicket.subject, + status: WebsiteTicket.status, + priority: WebsiteTicket.priority, + assignee: WebsiteTicket.assigneeId, + date: WebsiteTicket.createdAt, + id: WebsiteTicket.id, +} as const; + +export default async function ModTicketsDeskPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]); + + 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 conditions: SQL[] = []; + + if (statusFilter === "active") { + conditions.push(ne(WebsiteTicket.status, "closed")); + } else if (statusFilter && statusFilter !== "all") { + conditions.push(eq(WebsiteTicket.status, statusFilter)); + } + + if (parsed.search.trim()) { + const q = parsed.search.trim(); + const asId = Number(q); + const ors: SQL[] = [ + like(WebsiteTicket.subject, `%${q}%`), + like(WebsiteTicket.category, `%${q}%`), + like(Creator.username, `%${q}%`), + like(Assignee.username, `%${q}%`), + ]; + if (Number.isFinite(asId) && asId > 0) { + ors.push(eq(WebsiteTicket.id, asId)); + } + conditions.push(or(...ors)!); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const sortKey = + parsed.sort && parsed.sort in SORT_COLS + ? (parsed.sort as keyof typeof SORT_COLS) + : "date"; + const sortCol = SORT_COLS[sortKey]; + const finalOrder = + parsed.sort && parsed.sort in SORT_COLS + ? parsed.order === "asc" + ? asc(sortCol) + : desc(sortCol) + : desc(WebsiteTicket.createdAt); + + const [total, openCount, inProgressCount, waitingCount, closedCount, queues] = + await Promise.all([ + db + .select({ total: count() }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) + .where(where) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "open")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "in_progress")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "waiting")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + db + .select({ total: count() }) + .from(WebsiteTicket) + .where(eq(WebsiteTicket.status, "closed")) + .then((rows) => rows[0]?.total ?? 0) + .catch(() => 0), + fetchTicketQueueOpenCounts(), + ]); + + const pagination = calcPagination(total, parsed.page, parsed.perPage); + + const tickets = await db + .select({ + id: WebsiteTicket.id, + subject: WebsiteTicket.subject, + category: WebsiteTicket.category, + status: WebsiteTicket.status, + priority: WebsiteTicket.priority, + creatorId: WebsiteTicket.creatorId, + createdAt: WebsiteTicket.createdAt, + creatorUsername: Creator.username, + assigneeUsername: Assignee.username, + }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) + .where(where) + .orderBy(finalOrder) + .offset(pagination.offset) + .limit(pagination.perPage) + .catch(() => []); + + const ticketIds = tickets.map((ticket) => ticket.id); + const msgRows = + ticketIds.length > 0 + ? await db + .select({ + ticketId: WebsiteTicketMessage.ticketId, + total: count(), + }) + .from(WebsiteTicketMessage) + .where(inArray(WebsiteTicketMessage.ticketId, ticketIds)) + .groupBy(WebsiteTicketMessage.ticketId) + .catch(() => []) + : []; + const msgMap = new Map(msgRows.map((m) => [m.ticketId, m.total])); + + const rows: TicketRow[] = tickets.map((ticket) => ({ + id: ticket.id, + subject: ticket.subject, + creator: ticket.creatorUsername ?? "—", + creatorId: ticket.creatorId, + category: ticket.category, + messages: msgMap.get(ticket.id) ?? 0, + status: ticket.status, + priority: ticket.priority, + assignee: ticket.assigneeUsername ?? "—", + date: formatDate(ticket.createdAt, "date"), + })); + + return ( +
+ +
+ + 0 ? "warn" : "neutral"} + icon="⏳" + /> + 0 ? "warn" : "neutral"} + icon="🕒" + /> + +
+ + {total === 0 && !parsed.search && statusFilter === "active" ? ( +
{t("noTickets")}
+ ) : ( + + )} +
+ ); +} diff --git a/src/app/mod/tickets/page.tsx b/src/app/mod/tickets/page.tsx index 821b7c0a..f470b471 100644 --- a/src/app/mod/tickets/page.tsx +++ b/src/app/mod/tickets/page.tsx @@ -1,218 +1,95 @@ -import { - and, - asc, - count, - desc, - eq, - inArray, - like, - ne, - or, - type SQL, -} from "drizzle-orm"; -import { alias } from "drizzle-orm/mysql-core"; +import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { - type TicketRow, - TicketsTable, -} from "@/app/admin/tickets/tickets-table"; import { StatusCard } from "@/components/admin/dashboard"; -import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner"; +import { UnifiedTicketsTable } from "@/components/admin/unified-tickets-table"; import { requireModPermission } from "@/lib/admin/guard"; +import { + fetchUnifiedTicketInbox, + type TicketInboxTypeFilter, +} from "@/lib/admin/ticket-inbox"; import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts"; -import { calcPagination, parseListParams } from "@/lib/admin-helpers"; -import { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db"; -import { formatDate } from "@/lib/format-date"; +import { parseListParams } from "@/lib/admin-helpers"; import { PERMS } from "@/lib/permissions"; -const Creator = alias(User, "ticket_creator"); -const Assignee = alias(User, "ticket_assignee"); - -const SORT_COLS = { - subject: WebsiteTicket.subject, - status: WebsiteTicket.status, - priority: WebsiteTicket.priority, - assignee: WebsiteTicket.assigneeId, - date: WebsiteTicket.createdAt, - id: WebsiteTicket.id, -} as const; - -export default async function ModTicketsPage({ +export default async function ModTicketInboxPage({ searchParams, }: { searchParams: Promise>; }) { await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]); - const t = await getTranslations("pages.admin.tickets"); + const t = await getTranslations("pages.admin.ticketInbox"); const raw = await searchParams; const parsed = parseListParams(new URLSearchParams(raw)); - const statusFilter = raw.filter_status || raw.status || "active"; + const typeRaw = raw.filter_type || "all"; + const type: TicketInboxTypeFilter = + typeRaw === "cms" || typeRaw === "help" ? typeRaw : "all"; + const statusFilter = raw.filter_status || "open"; + const openOnly = statusFilter !== "all"; - const conditions: SQL[] = []; - - if (statusFilter === "active") { - conditions.push(ne(WebsiteTicket.status, "closed")); - } else if (statusFilter && statusFilter !== "all") { - conditions.push(eq(WebsiteTicket.status, statusFilter)); - } - - if (parsed.search.trim()) { - const q = parsed.search.trim(); - const asId = Number(q); - const ors: SQL[] = [ - like(WebsiteTicket.subject, `%${q}%`), - like(WebsiteTicket.category, `%${q}%`), - like(Creator.username, `%${q}%`), - like(Assignee.username, `%${q}%`), - ]; - if (Number.isFinite(asId) && asId > 0) { - ors.push(eq(WebsiteTicket.id, asId)); - } - conditions.push(or(...ors)!); - } - - const where = conditions.length > 0 ? and(...conditions) : undefined; - - const sortKey = - parsed.sort && parsed.sort in SORT_COLS - ? (parsed.sort as keyof typeof SORT_COLS) - : "date"; - const sortCol = SORT_COLS[sortKey]; - const finalOrder = - parsed.sort && parsed.sort in SORT_COLS - ? parsed.order === "asc" - ? asc(sortCol) - : desc(sortCol) - : desc(WebsiteTicket.createdAt); - - const [total, openCount, inProgressCount, waitingCount, closedCount, queues] = - await Promise.all([ - db - .select({ total: count() }) - .from(WebsiteTicket) - .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) - .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) - .where(where) - .then((rows) => rows[0]?.total ?? 0) - .catch(() => 0), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "open")) - .then((rows) => rows[0]?.total ?? 0) - .catch(() => 0), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "in_progress")) - .then((rows) => rows[0]?.total ?? 0) - .catch(() => 0), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "waiting")) - .then((rows) => rows[0]?.total ?? 0) - .catch(() => 0), - db - .select({ total: count() }) - .from(WebsiteTicket) - .where(eq(WebsiteTicket.status, "closed")) - .then((rows) => rows[0]?.total ?? 0) - .catch(() => 0), - fetchTicketQueueOpenCounts(), - ]); - - const pagination = calcPagination(total, parsed.page, parsed.perPage); - - const tickets = await db - .select({ - id: WebsiteTicket.id, - subject: WebsiteTicket.subject, - category: WebsiteTicket.category, - status: WebsiteTicket.status, - priority: WebsiteTicket.priority, - creatorId: WebsiteTicket.creatorId, - createdAt: WebsiteTicket.createdAt, - creatorUsername: Creator.username, - assigneeUsername: Assignee.username, - }) - .from(WebsiteTicket) - .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) - .leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id)) - .where(where) - .orderBy(finalOrder) - .offset(pagination.offset) - .limit(pagination.perPage) - .catch(() => []); - - const ticketIds = tickets.map((ticket) => ticket.id); - const msgRows = - ticketIds.length > 0 - ? await db - .select({ - ticketId: WebsiteTicketMessage.ticketId, - total: count(), - }) - .from(WebsiteTicketMessage) - .where(inArray(WebsiteTicketMessage.ticketId, ticketIds)) - .groupBy(WebsiteTicketMessage.ticketId) - .catch(() => []) - : []; - const msgMap = new Map(msgRows.map((m) => [m.ticketId, m.total])); - - const rows: TicketRow[] = tickets.map((ticket) => ({ - id: ticket.id, - subject: ticket.subject, - creator: ticket.creatorUsername ?? "—", - creatorId: ticket.creatorId, - category: ticket.category, - messages: msgMap.get(ticket.id) ?? 0, - status: ticket.status, - priority: ticket.priority, - assignee: ticket.assigneeUsername ?? "—", - date: formatDate(ticket.createdAt, "date"), - })); + const [inbox, queues] = await Promise.all([ + fetchUnifiedTicketInbox({ + page: parsed.page, + perPage: parsed.perPage, + search: parsed.search, + type, + openOnly, + base: "mod", + }), + fetchTicketQueueOpenCounts(), + ]); return (
- +
+ + {t("modNavLabel")} + + + {t("modNavInbox")} + + + {t("modNavDesk")} + + + {t("modNavHelp")} + +
+

{t("hint")}

- + 0 ? "warn" : "neutral"} - icon="⏳" + label={t("statHelpOpen")} + value={queues.helpOpen} + state={queues.helpOpen > 0 ? "warn" : "neutral"} + icon="🛟" /> 0 ? "warn" : "neutral"} - icon="🕒" + label={t("statTotal")} + value={queues.cmsOpen + queues.helpOpen} + icon="📥" /> -
- {total === 0 && !parsed.search && statusFilter === "active" ? ( + {inbox.total === 0 && !parsed.search ? (
{t("noTickets")}
) : ( - )} diff --git a/src/components/admin/unified-tickets-table.tsx b/src/components/admin/unified-tickets-table.tsx new file mode 100644 index 00000000..dfd16fd3 --- /dev/null +++ b/src/components/admin/unified-tickets-table.tsx @@ -0,0 +1,116 @@ +"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 { TicketInboxRow } from "@/lib/admin/ticket-inbox"; +import type { DataTableColumn, PaginatedResult } from "@/types/common"; + +export function UnifiedTicketsTable({ + data, +}: { + data: PaginatedResult; +}) { + const t = useTranslations("pages.admin.ticketInbox"); + + const columns: DataTableColumn[] = [ + { + key: "title", + label: t("colTicket"), + sortable: false, + render: (_value, row) => ( +
+
+ + {row.kind === "cms" ? t("typeCms") : t("typeHelp")} + + + #{row.id} + + + {row.title} + +
+
+ {t("meta", { user: row.user })} +
+
+ ), + }, + { + key: "status", + label: t("colStatus"), + sortable: false, + filterKey: "filter_status", + filterOptions: [ + { label: t("statusOpen"), value: "open" }, + { label: t("statusAll"), value: "all" }, + ], + render: (_value, row) => ( + + {row.kind === "cms" + ? row.status.replace("_", " ") + : row.open + ? t("statusOpen") + : t("statusClosed")} + + ), + }, + { + key: "kind", + label: t("colType"), + sortable: false, + filterKey: "filter_type", + filterOptions: [ + { label: t("typeAll"), value: "all" }, + { label: t("typeCms"), value: "cms" }, + { label: t("typeHelp"), value: "help" }, + ], + render: (value) => { + const kind = String(value); + return ( + + {kind === "cms" ? t("typeCms") : t("typeHelp")} + + ); + }, + }, + { key: "date", label: t("colUpdated"), sortable: false }, + ]; + + return ( + + ); +} diff --git a/src/lib/admin-nav.ts b/src/lib/admin-nav.ts index 478e53ff..56c4bfc9 100644 --- a/src/lib/admin-nav.ts +++ b/src/lib/admin-nav.ts @@ -109,9 +109,14 @@ export const ADMIN_HUBS: AdminHubDefinition[] = [ tabs: [ { href: "/admin/tickets", - labelKey: "tickets", + labelKey: "ticketInbox", match: ["/admin/tickets"], }, + { + href: "/admin/tickets/desk", + labelKey: "tickets", + match: ["/admin/tickets/desk"], + }, { href: "/admin/help-tickets", labelKey: "helpTickets", diff --git a/src/lib/admin/ticket-inbox.ts b/src/lib/admin/ticket-inbox.ts new file mode 100644 index 00000000..ed6dae2b --- /dev/null +++ b/src/lib/admin/ticket-inbox.ts @@ -0,0 +1,327 @@ +import "server-only"; + +import { + and, + count, + desc, + eq, + inArray, + like, + ne, + or, + type SQL, +} from "drizzle-orm"; +import { alias } from "drizzle-orm/mysql-core"; +import { calcPagination } from "@/lib/admin-helpers"; +import { db, User, WebsiteHelpCenterTickets, WebsiteTicket } from "@/lib/db"; +import { formatDate } from "@/lib/format-date"; + +export type TicketInboxKind = "cms" | "help"; +export type TicketInboxTypeFilter = "all" | TicketInboxKind; + +export interface TicketInboxRow { + key: string; + kind: TicketInboxKind; + id: string; + title: string; + user: string; + userId: number | null; + status: string; + open: boolean; + sortAt: number; + date: string; + href: string; +} + +export interface FetchTicketInboxOptions { + page: number; + perPage: number; + search?: string; + type?: TicketInboxTypeFilter; + /** Default true: only non-closed CMS + open help-center. */ + openOnly?: boolean; + base?: "admin" | "mod"; +} + +function toSortMs(value: Date | string | null | undefined): number { + if (!value) return 0; + const ms = + value instanceof Date ? value.getTime() : Date.parse(String(value)); + return Number.isFinite(ms) ? ms : 0; +} + +async function fetchCmsCandidates( + limit: number, + search: string, + openOnly: boolean, + base: "admin" | "mod", +): Promise { + const Creator = alias(User, "inbox_ticket_creator"); + const conditions: SQL[] = []; + + if (openOnly) { + conditions.push(ne(WebsiteTicket.status, "closed")); + } + + if (search) { + const asId = Number(search); + const searchOr = [ + like(WebsiteTicket.subject, `%${search}%`), + like(WebsiteTicket.category, `%${search}%`), + like(Creator.username, `%${search}%`), + ...(Number.isFinite(asId) && asId > 0 + ? [eq(WebsiteTicket.id, asId)] + : []), + ]; + conditions.push(or(...searchOr)!); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const rows = await db + .select({ + id: WebsiteTicket.id, + subject: WebsiteTicket.subject, + status: WebsiteTicket.status, + creatorId: WebsiteTicket.creatorId, + updatedAt: WebsiteTicket.updatedAt, + createdAt: WebsiteTicket.createdAt, + creatorUsername: Creator.username, + }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .where(where) + .orderBy(desc(WebsiteTicket.updatedAt), desc(WebsiteTicket.id)) + .limit(limit) + .catch(() => []); + + const prefix = base === "mod" ? "/mod/tickets" : "/admin/tickets"; + + return rows.map((row) => ({ + key: `cms-${row.id}`, + kind: "cms" as const, + id: String(row.id), + title: row.subject, + user: row.creatorUsername ?? "—", + userId: row.creatorId, + status: row.status, + open: row.status !== "closed", + sortAt: toSortMs(row.updatedAt ?? row.createdAt), + date: formatDate(row.updatedAt ?? row.createdAt, "date"), + href: `${prefix}/${row.id}`, + })); +} + +async function fetchHelpCandidates( + limit: number, + search: string, + openOnly: boolean, + base: "admin" | "mod", +): Promise { + const conditions: SQL[] = []; + + if (openOnly) { + conditions.push(eq(WebsiteHelpCenterTickets.open, true)); + } + + if (search) { + const asId = /^\d+$/.test(search) ? BigInt(search) : null; + const matchingUsers = await db + .select({ id: User.id }) + .from(User) + .where(like(User.username, `%${search}%`)) + .limit(50) + .catch(() => []); + + const searchOr = [ + like(WebsiteHelpCenterTickets.title, `%${search}%`), + like(WebsiteHelpCenterTickets.content, `%${search}%`), + ...(matchingUsers.length > 0 + ? [ + inArray( + WebsiteHelpCenterTickets.userId, + matchingUsers.map((u) => u.id), + ), + ] + : []), + ...(asId !== null ? [eq(WebsiteHelpCenterTickets.id, asId)] : []), + ]; + conditions.push(or(...searchOr)!); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const rows = await db + .select({ + id: WebsiteHelpCenterTickets.id, + title: WebsiteHelpCenterTickets.title, + open: WebsiteHelpCenterTickets.open, + userId: WebsiteHelpCenterTickets.userId, + updatedAt: WebsiteHelpCenterTickets.updatedAt, + createdAt: WebsiteHelpCenterTickets.createdAt, + }) + .from(WebsiteHelpCenterTickets) + .where(where) + .orderBy( + desc(WebsiteHelpCenterTickets.updatedAt), + desc(WebsiteHelpCenterTickets.id), + ) + .limit(limit) + .catch(() => []); + + const userIds = [ + ...new Set( + rows.map((row) => row.userId).filter((id): id is number => id != null), + ), + ]; + const users = + userIds.length > 0 + ? await db + .select({ id: User.id, username: User.username }) + .from(User) + .where(inArray(User.id, userIds)) + .catch(() => []) + : []; + const usernameById = new Map(users.map((u) => [u.id, u.username])); + + const prefix = base === "mod" ? "/mod/help-tickets" : "/admin/help-tickets"; + + return rows.map((row) => ({ + key: `help-${row.id}`, + kind: "help" as const, + id: String(row.id), + title: row.title, + user: + row.userId != null + ? (usernameById.get(row.userId) ?? `#${row.userId}`) + : "—", + userId: row.userId, + status: row.open ? "open" : "closed", + open: row.open, + sortAt: toSortMs(row.updatedAt ?? row.createdAt), + date: formatDate(row.updatedAt ?? row.createdAt, "date"), + href: `${prefix}/${row.id}`, + })); +} + +async function countCms(search: string, openOnly: boolean): Promise { + const Creator = alias(User, "inbox_ticket_creator_count"); + const conditions: SQL[] = []; + if (openOnly) conditions.push(ne(WebsiteTicket.status, "closed")); + if (search) { + const asId = Number(search); + conditions.push( + or( + like(WebsiteTicket.subject, `%${search}%`), + like(WebsiteTicket.category, `%${search}%`), + like(Creator.username, `%${search}%`), + ...(Number.isFinite(asId) && asId > 0 + ? [eq(WebsiteTicket.id, asId)] + : []), + )!, + ); + } + const where = conditions.length > 0 ? and(...conditions) : undefined; + const rows = await db + .select({ total: count() }) + .from(WebsiteTicket) + .leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id)) + .where(where) + .catch(() => [{ total: 0 }]); + return Number(rows[0]?.total ?? 0); +} + +async function countHelp(search: string, openOnly: boolean): Promise { + const conditions: SQL[] = []; + if (openOnly) conditions.push(eq(WebsiteHelpCenterTickets.open, true)); + if (search) { + const asId = /^\d+$/.test(search) ? BigInt(search) : null; + const matchingUsers = await db + .select({ id: User.id }) + .from(User) + .where(like(User.username, `%${search}%`)) + .limit(50) + .catch(() => []); + conditions.push( + or( + like(WebsiteHelpCenterTickets.title, `%${search}%`), + like(WebsiteHelpCenterTickets.content, `%${search}%`), + ...(matchingUsers.length > 0 + ? [ + inArray( + WebsiteHelpCenterTickets.userId, + matchingUsers.map((u) => u.id), + ), + ] + : []), + ...(asId !== null ? [eq(WebsiteHelpCenterTickets.id, asId)] : []), + )!, + ); + } + const where = conditions.length > 0 ? and(...conditions) : undefined; + const rows = await db + .select({ total: count() }) + .from(WebsiteHelpCenterTickets) + .where(where) + .catch(() => [{ total: 0 }]); + return Number(rows[0]?.total ?? 0); +} + +/** Merged read-model over CMS desk + help-center queues (no DB merge). */ +export async function fetchUnifiedTicketInbox( + options: FetchTicketInboxOptions, +): Promise<{ + rows: TicketInboxRow[]; + total: number; + page: number; + perPage: number; + lastPage: number; + cmsTotal: number; + helpTotal: number; +}> { + const type = options.type ?? "all"; + const openOnly = options.openOnly !== false; + const search = options.search?.trim() ?? ""; + const base = options.base ?? "admin"; + + const includeCms = type === "all" || type === "cms"; + const includeHelp = type === "all" || type === "help"; + + const [cmsTotal, helpTotal] = await Promise.all([ + includeCms ? countCms(search, openOnly) : Promise.resolve(0), + includeHelp ? countHelp(search, openOnly) : Promise.resolve(0), + ]); + + const total = cmsTotal + helpTotal; + const pagination = calcPagination(total, options.page, options.perPage); + const window = pagination.offset + pagination.perPage; + + const [cmsRows, helpRows] = await Promise.all([ + includeCms + ? fetchCmsCandidates(window, search, openOnly, base) + : Promise.resolve([]), + includeHelp + ? fetchHelpCandidates(window, search, openOnly, base) + : Promise.resolve([]), + ]); + + const merged = [...cmsRows, ...helpRows].sort((a, b) => { + if (b.sortAt !== a.sortAt) return b.sortAt - a.sortAt; + return a.key < b.key ? 1 : -1; + }); + + const rows = merged.slice( + pagination.offset, + pagination.offset + pagination.perPage, + ); + + return { + rows, + total, + page: pagination.page, + perPage: pagination.perPage, + lastPage: pagination.lastPage, + cmsTotal, + helpTotal, + }; +} diff --git a/src/lib/staff-smoke-contract.test.ts b/src/lib/staff-smoke-contract.test.ts index 4d2a7ea4..f9123f73 100644 --- a/src/lib/staff-smoke-contract.test.ts +++ b/src/lib/staff-smoke-contract.test.ts @@ -125,9 +125,9 @@ describe("staff smoke contract", () => { it("guards dual ticket queues on admin and mod", () => { for (const path of [ - "src/app/admin/tickets/page.tsx", + "src/app/admin/tickets/desk/page.tsx", "src/app/admin/help-tickets/page.tsx", - "src/app/mod/tickets/page.tsx", + "src/app/mod/tickets/desk/page.tsx", "src/app/mod/help-tickets/page.tsx", ]) { expect(existsSync(path), path).toBe(true); @@ -135,6 +135,25 @@ describe("staff smoke contract", () => { } }); + it("ships unified ticket inbox over both queues", () => { + expect(existsSync("src/lib/admin/ticket-inbox.ts")).toBe(true); + expect(readFileSync("src/lib/admin/ticket-inbox.ts", "utf8")).toContain( + "fetchUnifiedTicketInbox", + ); + expect(existsSync("src/components/admin/unified-tickets-table.tsx")).toBe( + true, + ); + expect(readFileSync("src/app/admin/tickets/page.tsx", "utf8")).toContain( + "fetchUnifiedTicketInbox", + ); + expect(readFileSync("src/app/mod/tickets/page.tsx", "utf8")).toContain( + "fetchUnifiedTicketInbox", + ); + expect(readFileSync("src/lib/admin-nav.ts", "utf8")).toContain( + "ticketInbox", + ); + }); + it("ships items_base browser", () => { expect(existsSync("src/app/admin/items/page.tsx")).toBe(true); expect(existsSync("src/app/admin/items/[id]/page.tsx")).toBe(true); diff --git a/src/messages/bg.json b/src/messages/bg.json index 84576ad0..7f3de04f 100644 --- a/src/messages/bg.json +++ b/src/messages/bg.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/cs.json b/src/messages/cs.json index eec8a426..63148658 100644 --- a/src/messages/cs.json +++ b/src/messages/cs.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/da.json b/src/messages/da.json index 8039c1c5..1cab804a 100644 --- a/src/messages/da.json +++ b/src/messages/da.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/de.json b/src/messages/de.json index 24e76de9..e8180587 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -2565,7 +2565,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2870,6 +2871,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "radioRequests": { diff --git a/src/messages/el.json b/src/messages/el.json index 79c23041..4bf77e2b 100644 --- a/src/messages/el.json +++ b/src/messages/el.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/en.json b/src/messages/en.json index 5708c55c..46133704 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -981,7 +981,7 @@ "advertisements": "Advertisements", "writeableBoxes": "Writeable boxes", "photos": "Photos", - "tickets": "Support desk", + "tickets": "Tickets", "helpTickets": "Help-center tickets", "events": "Events", "polls": "Polls", @@ -1510,7 +1510,7 @@ "tickets": { "title": "CMS support desk", "subtitle": "Internal CMS tickets (assignee, priority, staff workflow). Not the same as help-center tickets from /help.", - "queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.", + "queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab. Use Inbox for both queues.", "queueOtherCta": "Open help-center tickets", "queueCounts": "Open here: {here} · Help center: {other}", "noTickets": "No active tickets", @@ -1527,11 +1527,35 @@ "colDate": "Date", "meta": "by {user} · {count} msg · {category}" }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product’s own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" + }, "helpTickets": { "title": "Help center tickets", "subtitle": "Tickets players open at /help/tickets. Separate from the CMS support desk.", - "queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.", - "queueOtherCta": "Open CMS support desk", + "queueHint": "This queue is the public help center. Use Inbox for both queues, or Support desk for CMS-only.", + "queueOtherCta": "Open unified inbox", "queueCounts": "Open here: {here} · Support desk: {other}", "noTickets": "No open help center tickets", "searchPlaceholder": "Search title, user or ID…", @@ -2516,6 +2540,7 @@ "eventTypes": "Event types", "polls": "Polls", "tickets": "Support desk", + "ticketInbox": "Inbox", "helpTickets": "Help center", "templates": "Templates", "directory": "Directory", @@ -2577,8 +2602,8 @@ }, "events": "Events", "eventsSubtitle": "Hotel events and event types", - "tickets": "Support desk", - "ticketsSubtitle": "Two separate queues: CMS support desk and public help-center tickets", + "tickets": "Tickets", + "ticketsSubtitle": "Unified inbox over CMS support desk and help-center queues", "staffAccess": "Ranks & permissions", "staffAccessSubtitle": "Staff ranks and live access. Archive is compare/export only.", "analytics": "Analytics", @@ -3080,7 +3105,7 @@ "bans": "Bans", "fullAdmin": "Full admin", "backToSite": "Back to site", - "tickets": "Support desk", + "tickets": "Tickets", "helpTickets": "Help-center tickets", "users": "Users", "team": "Team" diff --git a/src/messages/es.json b/src/messages/es.json index 07e5b748..eeb12a13 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -2565,7 +2565,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2870,6 +2871,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "radioRequests": { diff --git a/src/messages/fr.json b/src/messages/fr.json index 49bf16c4..2bdef077 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -2565,7 +2565,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2870,6 +2871,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "radioRequests": { diff --git a/src/messages/hr.json b/src/messages/hr.json index cacb1bad..8a5f7e50 100644 --- a/src/messages/hr.json +++ b/src/messages/hr.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/hu.json b/src/messages/hu.json index a4fd8cbf..b14ac3e3 100644 --- a/src/messages/hu.json +++ b/src/messages/hu.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/it.json b/src/messages/it.json index 483bc1c4..18a2585e 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -952,7 +952,7 @@ "advertisements": "Pubblicità", "writeableBoxes": "Box modificabili", "photos": "Foto", - "tickets": "Assistenza", + "tickets": "Ticket", "helpTickets": "Ticket centro assistenza", "events": "Eventi", "polls": "Sondaggi", @@ -2812,12 +2812,13 @@ "devops": "DevOps", "errors": "Errori", "alerts": "Alert", - "toolsGroup": "Strumenti" + "toolsGroup": "Strumenti", + "ticketInbox": "Inbox" }, "events": "Eventi", "eventsSubtitle": "Eventi hotel e tipi evento", - "tickets": "Assistenza", - "ticketsSubtitle": "Due code separate: supporto CMS e ticket help center pubblici", + "tickets": "Ticket", + "ticketsSubtitle": "Inbox unificata su supporto CMS e help center", "staffAccess": "Gradi e permessi", "staffAccessSubtitle": "Gradi staff e accesso live. L'archivio e solo confronto/export.", "analytics": "Analitiche", @@ -3042,6 +3043,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Vista unificata dei ticket aperti CMS e help center. I dettagli restano sulle route di ciascun prodotto.", + "noTickets": "Nessun ticket aperto in nessuna coda", + "searchPlaceholder": "Cerca oggetto, titolo, utente o ID…", + "statCmsOpen": "CMS aperti", + "statHelpOpen": "Help aperti", + "statTotal": "Totale aperti", + "colTicket": "Ticket", + "colStatus": "Stato", + "colType": "Coda", + "colUpdated": "Aggiornato", + "meta": "di {user}", + "typeAll": "Tutte le code", + "typeCms": "Supporto CMS", + "typeHelp": "Help center", + "statusOpen": "Aperti", + "statusClosed": "Chiusi", + "statusAll": "Tutti gli stati", + "presetOpen": "Solo aperti", + "modNavLabel": "Code:", + "modNavInbox": "Inbox", + "modNavDesk": "Supporto CMS", + "modNavHelp": "Help center" } }, "radioRequests": { diff --git a/src/messages/nl.json b/src/messages/nl.json index 4a4dfcd1..2c691730 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -2853,7 +2853,8 @@ "devops": "DevOps", "errors": "Fouten", "alerts": "Meldingen", - "toolsGroup": "Tools" + "toolsGroup": "Tools", + "ticketInbox": "Inbox" }, "events": "Evenementen", "eventsSubtitle": "Hotel-evenementen en evenementtypes", @@ -3051,6 +3052,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "mod": { diff --git a/src/messages/no.json b/src/messages/no.json index a23f50ce..abb02079 100644 --- a/src/messages/no.json +++ b/src/messages/no.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/pl.json b/src/messages/pl.json index fee62c8d..169d084b 100644 --- a/src/messages/pl.json +++ b/src/messages/pl.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/pt.json b/src/messages/pt.json index 22558d8f..77a12250 100644 --- a/src/messages/pt.json +++ b/src/messages/pt.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/ro.json b/src/messages/ro.json index d9f0827d..b02feb95 100644 --- a/src/messages/ro.json +++ b/src/messages/ro.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/ru.json b/src/messages/ru.json index 4b73a456..af8ac0a6 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -2593,7 +2593,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2898,6 +2899,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/sk.json b/src/messages/sk.json index bdbc9d2f..543ea557 100644 --- a/src/messages/sk.json +++ b/src/messages/sk.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/sr.json b/src/messages/sr.json index bf300e82..4b671e7c 100644 --- a/src/messages/sr.json +++ b/src/messages/sr.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/sv.json b/src/messages/sv.json index 566cbbed..40754301 100644 --- a/src/messages/sv.json +++ b/src/messages/sv.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/tr.json b/src/messages/tr.json index 0c3697c2..a502647e 100644 --- a/src/messages/tr.json +++ b/src/messages/tr.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": { diff --git a/src/messages/uk.json b/src/messages/uk.json index 5c5eb001..20964700 100644 --- a/src/messages/uk.json +++ b/src/messages/uk.json @@ -2595,7 +2595,8 @@ "errors": "Errors", "alerts": "Alerts", "toolsGroup": "Tools", - "helpTickets": "Help-center tickets" + "helpTickets": "Help-center tickets", + "ticketInbox": "Inbox" }, "events": "Events", "eventsSubtitle": "Hotel events and event types", @@ -2900,6 +2901,30 @@ "title": "Online users", "subtitle": "Players currently connected", "connected": "{count, plural, one {# player connected} other {# players connected}}" + }, + "ticketInbox": { + "hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product own route.", + "noTickets": "No open tickets in either queue", + "searchPlaceholder": "Search subject, title, user or ID…", + "statCmsOpen": "CMS open", + "statHelpOpen": "Help open", + "statTotal": "Open total", + "colTicket": "Ticket", + "colStatus": "Status", + "colType": "Queue", + "colUpdated": "Updated", + "meta": "by {user}", + "typeAll": "All queues", + "typeCms": "CMS desk", + "typeHelp": "Help center", + "statusOpen": "Open", + "statusClosed": "Closed", + "statusAll": "All statuses", + "presetOpen": "Open only", + "modNavLabel": "Queues:", + "modNavInbox": "Inbox", + "modNavDesk": "CMS desk", + "modNavHelp": "Help center" } }, "error": {