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
@@ -0,0 +1,30 @@
|
||||
import { MySqlDialect } from "drizzle-orm/mysql-core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/db", async () => import("@/db/schema"));
|
||||
|
||||
import {
|
||||
awaitingStaffCondition,
|
||||
latestTicketReplyAt,
|
||||
ticketAssignmentCondition,
|
||||
} from "./ticket-desk-query";
|
||||
|
||||
const dialect = new MySqlDialect();
|
||||
describe("support desk query", () => {
|
||||
it("binds the signed-in staff ID and distinguishes unassigned tickets", () => {
|
||||
const mine = ticketAssignmentCondition("mine", 123);
|
||||
expect(mine && dialect.sqlToQuery(mine).params).toEqual([123]);
|
||||
const none = ticketAssignmentCondition("unassigned", 123);
|
||||
expect(none && dialect.sqlToQuery(none).sql).toContain("is null");
|
||||
expect(ticketAssignmentCondition("123 OR 1=1", 123)).toBeUndefined();
|
||||
});
|
||||
it("requires an active ticket and resolves equal timestamps by message id", () => {
|
||||
expect(awaitingStaffCondition).toBeDefined();
|
||||
if (!awaitingStaffCondition) return;
|
||||
const query = dialect.sqlToQuery(awaitingStaffCondition);
|
||||
expect(query.params).toEqual(["closed", 0]);
|
||||
expect(query.sql).toContain("m.is_staff");
|
||||
expect(query.sql).toContain("m.created_at DESC, m.id DESC LIMIT 1");
|
||||
expect(dialect.sqlToQuery(latestTicketReplyAt).sql).toContain("COALESCE");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { and, eq, isNotNull, isNull, ne, type SQL, sql } from "drizzle-orm";
|
||||
import { WebsiteTicket } from "@/lib/db";
|
||||
|
||||
/** Messages persist their staff role at send time; later rank changes do not alter the queue. */
|
||||
export const latestTicketReplyIsStaff = sql<number>`COALESCE((SELECT m.is_staff FROM website_ticket_messages m WHERE m.ticket_id = ${WebsiteTicket.id} ORDER BY m.created_at DESC, m.id DESC LIMIT 1), 0)`;
|
||||
export const latestTicketReplyAt = sql<string>`COALESCE((SELECT m.created_at FROM website_ticket_messages m WHERE m.ticket_id = ${WebsiteTicket.id} ORDER BY m.created_at DESC, m.id DESC LIMIT 1), ${WebsiteTicket.createdAt})`;
|
||||
export const awaitingStaffCondition = and(
|
||||
ne(WebsiteTicket.status, "closed"),
|
||||
eq(latestTicketReplyIsStaff, 0),
|
||||
);
|
||||
|
||||
export function ticketAssignmentCondition(
|
||||
value: string | undefined,
|
||||
userId: number,
|
||||
): SQL | undefined {
|
||||
if (value === "mine") return eq(WebsiteTicket.assigneeId, userId);
|
||||
if (value === "unassigned") return isNull(WebsiteTicket.assigneeId);
|
||||
if (value === "assigned") return isNotNull(WebsiteTicket.assigneeId);
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ticketReplyAge } from "./ticket-reply-age";
|
||||
|
||||
describe("ticket reply age", () => {
|
||||
it("measures elapsed minutes, including day boundaries", () => {
|
||||
expect(
|
||||
ticketReplyAge(
|
||||
"2026-09-10T12:00:00Z",
|
||||
Date.parse("2026-09-11T13:30:00Z"),
|
||||
),
|
||||
).toBe(1530);
|
||||
});
|
||||
it("does not report negative or invalid ages", () => {
|
||||
expect(ticketReplyAge(new Date(2000), 1000)).toBe(0);
|
||||
expect(ticketReplyAge("invalid")).toBeNull();
|
||||
expect(ticketReplyAge(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function ticketReplyAge(
|
||||
value: Date | string | null,
|
||||
now = Date.now(),
|
||||
): number | null {
|
||||
if (!value) return null;
|
||||
const time = value instanceof Date ? value.getTime() : Date.parse(value);
|
||||
return Number.isFinite(time)
|
||||
? Math.max(0, Math.floor((now - time) / 60000))
|
||||
: null;
|
||||
}
|
||||
Reference in new issue
Block a user