feat(support): filter waiting tickets and staff assignments
This commit is contained in:
1 parent
5d18af932d
commit
25d0a13050
7 files changed
+173
-6
No files matched your search
@@ -15,7 +15,14 @@ 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 {
|
||||
awaitingStaffCondition,
|
||||
latestTicketReplyAt,
|
||||
latestTicketReplyIsStaff,
|
||||
ticketAssignmentCondition,
|
||||
} from "@/lib/admin/ticket-desk-query";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { ticketReplyAge } from "@/lib/admin/ticket-reply-age";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
@@ -41,8 +48,15 @@ export default async function AdminTicketsDeskPage({
|
||||
const Assignee = alias(User, "ticket_assignee");
|
||||
|
||||
const conditions: SQL[] = [];
|
||||
const assignment = ticketAssignmentCondition(
|
||||
raw.filter_assignee,
|
||||
session.user.id,
|
||||
);
|
||||
if (assignment) conditions.push(assignment);
|
||||
|
||||
if (statusFilter === "active") {
|
||||
if (statusFilter === "awaiting_staff") {
|
||||
if (awaitingStaffCondition) conditions.push(awaitingStaffCondition);
|
||||
} else if (statusFilter === "active") {
|
||||
conditions.push(ne(WebsiteTicket.status, "closed"));
|
||||
} else if (statusFilter && statusFilter !== "all") {
|
||||
conditions.push(eq(WebsiteTicket.status, statusFilter));
|
||||
@@ -130,6 +144,8 @@ export default async function AdminTicketsDeskPage({
|
||||
createdAt: WebsiteTicket.createdAt,
|
||||
creatorUsername: Creator.username,
|
||||
assigneeUsername: Assignee.username,
|
||||
replyAt: latestTicketReplyAt,
|
||||
replyIsStaff: latestTicketReplyIsStaff,
|
||||
messageCount: sql<number>`(
|
||||
SELECT COUNT(*) FROM website_ticket_messages m
|
||||
WHERE m.ticket_id = ${WebsiteTicket.id}
|
||||
@@ -152,6 +168,9 @@ export default async function AdminTicketsDeskPage({
|
||||
category: ticket.category,
|
||||
messages: Number(ticket.messageCount ?? 0),
|
||||
status: ticket.status,
|
||||
awaitingStaff:
|
||||
ticket.status !== "closed" && Number(ticket.replyIsStaff) === 0,
|
||||
replyAgeMinutes: ticketReplyAge(ticket.replyAt),
|
||||
priority: ticket.priority,
|
||||
assignee: ticket.assigneeUsername ?? "—",
|
||||
date: formatDate(ticket.createdAt, "date"),
|
||||
@@ -185,7 +204,10 @@ export default async function AdminTicketsDeskPage({
|
||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||
</div>
|
||||
|
||||
{total === 0 && !parsed.search && statusFilter === "active" ? (
|
||||
{total === 0 &&
|
||||
!parsed.search &&
|
||||
statusFilter === "active" &&
|
||||
!raw.filter_assignee ? (
|
||||
<div className="admin-empty">{t("noTickets")}</div>
|
||||
) : (
|
||||
<TicketsTable
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface TicketRow {
|
||||
priority: string;
|
||||
assignee: string;
|
||||
date: string;
|
||||
awaitingStaff: boolean;
|
||||
replyAgeMinutes: number | null;
|
||||
}
|
||||
|
||||
const STATUS_VARIANT: Record<
|
||||
@@ -79,6 +81,7 @@ export function TicketsTable({
|
||||
sortable: true,
|
||||
filterKey: "filter_status",
|
||||
filterOptions: [
|
||||
{ label: t("awaitingStaff"), value: "awaiting_staff" },
|
||||
{ label: t("statusOpen"), value: "open" },
|
||||
{ label: t("statusInProgress"), value: "in_progress" },
|
||||
{ label: t("statusWaiting"), value: "waiting" },
|
||||
@@ -113,7 +116,41 @@ export function TicketsTable({
|
||||
return <span className={`text-xs capitalize ${className}`}>{p}</span>;
|
||||
},
|
||||
},
|
||||
{ key: "assignee", label: t("colAssigned"), sortable: true },
|
||||
{
|
||||
key: "assignee",
|
||||
label: t("colAssigned"),
|
||||
sortable: true,
|
||||
filterKey: "filter_assignee",
|
||||
filterOptions: [
|
||||
{ label: t("assignedMine"), value: "mine" },
|
||||
{ label: t("unassigned"), value: "unassigned" },
|
||||
{ label: t("assignedAny"), value: "assigned" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "replyAgeMinutes",
|
||||
label: t("lastReplyAge"),
|
||||
render: (_value, row) => (
|
||||
<div>
|
||||
{row.awaitingStaff && (
|
||||
<Badge variant="outline">{t("awaitingStaff")}</Badge>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{row.replyAgeMinutes === null
|
||||
? "—"
|
||||
: row.replyAgeMinutes < 60
|
||||
? t("ageMinutes", { count: row.replyAgeMinutes })
|
||||
: row.replyAgeMinutes < 1440
|
||||
? t("ageHours", {
|
||||
count: Math.floor(row.replyAgeMinutes / 60),
|
||||
})
|
||||
: t("ageDays", {
|
||||
count: Math.floor(row.replyAgeMinutes / 1440),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: "date", label: t("colDate"), sortable: true },
|
||||
];
|
||||
|
||||
@@ -123,6 +160,14 @@ export function TicketsTable({
|
||||
columns={columns}
|
||||
searchPlaceholder={t("searchPlaceholder")}
|
||||
presets={[
|
||||
{
|
||||
label: t("awaitingStaff"),
|
||||
params: { filter_status: "awaiting_staff", filter_assignee: "" },
|
||||
},
|
||||
{
|
||||
label: t("assignedMine"),
|
||||
params: { filter_status: "active", filter_assignee: "mine" },
|
||||
},
|
||||
{ label: t("presetActive"), params: { filter_status: "active" } },
|
||||
{ label: t("statusOpen"), params: { filter_status: "open" } },
|
||||
{
|
||||
|
||||
@@ -19,7 +19,14 @@ import {
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import { requireModPermission } from "@/lib/admin/guard";
|
||||
import {
|
||||
awaitingStaffCondition,
|
||||
latestTicketReplyAt,
|
||||
latestTicketReplyIsStaff,
|
||||
ticketAssignmentCondition,
|
||||
} from "@/lib/admin/ticket-desk-query";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { ticketReplyAge } from "@/lib/admin/ticket-reply-age";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, User, WebsiteTicket, WebsiteTicketMessage } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
@@ -42,7 +49,10 @@ export default async function ModTicketsDeskPage({
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) {
|
||||
await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]);
|
||||
const staff = await requireModPermission([
|
||||
PERMS.MOD_TICKETS_VIEW,
|
||||
PERMS.TICKETS_VIEW,
|
||||
]);
|
||||
|
||||
const t = await getTranslations("pages.admin.tickets");
|
||||
const raw = await searchParams;
|
||||
@@ -50,8 +60,12 @@ export default async function ModTicketsDeskPage({
|
||||
const statusFilter = raw.filter_status || raw.status || "active";
|
||||
|
||||
const conditions: SQL[] = [];
|
||||
const assignment = ticketAssignmentCondition(raw.filter_assignee, staff.id);
|
||||
if (assignment) conditions.push(assignment);
|
||||
|
||||
if (statusFilter === "active") {
|
||||
if (statusFilter === "awaiting_staff") {
|
||||
if (awaitingStaffCondition) conditions.push(awaitingStaffCondition);
|
||||
} else if (statusFilter === "active") {
|
||||
conditions.push(ne(WebsiteTicket.status, "closed"));
|
||||
} else if (statusFilter && statusFilter !== "all") {
|
||||
conditions.push(eq(WebsiteTicket.status, statusFilter));
|
||||
@@ -137,6 +151,8 @@ export default async function ModTicketsDeskPage({
|
||||
createdAt: WebsiteTicket.createdAt,
|
||||
creatorUsername: Creator.username,
|
||||
assigneeUsername: Assignee.username,
|
||||
replyAt: latestTicketReplyAt,
|
||||
replyIsStaff: latestTicketReplyIsStaff,
|
||||
})
|
||||
.from(WebsiteTicket)
|
||||
.leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id))
|
||||
@@ -170,6 +186,9 @@ export default async function ModTicketsDeskPage({
|
||||
category: ticket.category,
|
||||
messages: msgMap.get(ticket.id) ?? 0,
|
||||
status: ticket.status,
|
||||
awaitingStaff:
|
||||
ticket.status !== "closed" && Number(ticket.replyIsStaff) === 0,
|
||||
replyAgeMinutes: ticketReplyAge(ticket.replyAt),
|
||||
priority: ticket.priority,
|
||||
assignee: ticket.assigneeUsername ?? "—",
|
||||
date: formatDate(ticket.createdAt, "date"),
|
||||
@@ -203,7 +222,10 @@ export default async function ModTicketsDeskPage({
|
||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||
</div>
|
||||
|
||||
{total === 0 && !parsed.search && statusFilter === "active" ? (
|
||||
{total === 0 &&
|
||||
!parsed.search &&
|
||||
statusFilter === "active" &&
|
||||
!raw.filter_assignee ? (
|
||||
<div className="admin-empty">{t("noTickets")}</div>
|
||||
) : (
|
||||
<TicketsTable
|
||||
|
||||
Reference in new issue
Block a user