mirror of
https://github.com/mosh-hamedani/helpdesk.git
synced 2026-05-21 11:58:19 +02:00
Add sorting
This commit is contained in:
parent
00d45441b5
commit
c384470575
5
bun.lock
5
bun.lock
|
|
@ -16,6 +16,7 @@
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"better-auth": "^1.4.18",
|
"better-auth": "^1.4.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
@ -579,6 +580,10 @@
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
||||||
|
|
||||||
|
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||||
|
|
||||||
|
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||||
|
|
||||||
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
|
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"better-auth": "^1.4.18",
|
"better-auth": "^1.4.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { screen, waitFor } from "@testing-library/react";
|
import { screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { renderWithQuery } from "@/test/render";
|
import { renderWithQuery } from "@/test/render";
|
||||||
|
|
@ -47,11 +48,13 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
expect(screen.getByText("Tickets")).toBeInTheDocument();
|
expect(screen.getByText("Tickets")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Subject")).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /Subject/ })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Sender")).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /Sender/ })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Status")).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /Status/ })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Category")).toBeInTheDocument();
|
expect(
|
||||||
expect(screen.getByText("Created")).toBeInTheDocument();
|
screen.getByRole("button", { name: /Category/ })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /Created/ })).toBeInTheDocument();
|
||||||
expect(document.querySelector("[data-slot='skeleton']")).toBeInTheDocument();
|
expect(document.querySelector("[data-slot='skeleton']")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -60,12 +63,18 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Cannot login to my account")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText("Cannot login to my account")
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Refund for order #123")).toBeInTheDocument();
|
expect(screen.getByText("Refund for order #123")).toBeInTheDocument();
|
||||||
expect(screen.getByText("How do I reset my password?")).toBeInTheDocument();
|
expect(
|
||||||
expect(document.querySelector("[data-slot='skeleton']")).not.toBeInTheDocument();
|
screen.getByText("How do I reset my password?")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
document.querySelector("[data-slot='skeleton']")
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display sender name and email", async () => {
|
it("should display sender name and email", async () => {
|
||||||
|
|
@ -109,7 +118,9 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("How do I reset my password?")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText("How do I reset my password?")
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("—")).toBeInTheDocument();
|
expect(screen.getByText("—")).toBeInTheDocument();
|
||||||
|
|
@ -119,7 +130,9 @@ describe("TicketsPage", () => {
|
||||||
mockedAxios.get.mockResolvedValue({ data: { tickets: [mockTickets[0]] } });
|
mockedAxios.get.mockResolvedValue({ data: { tickets: [mockTickets[0]] } });
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
const expectedDate = new Date("2025-03-01T10:00:00.000Z").toLocaleDateString();
|
const expectedDate = new Date(
|
||||||
|
"2025-03-01T10:00:00.000Z"
|
||||||
|
).toLocaleDateString();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
@ -130,7 +143,9 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Failed to fetch tickets")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText("Failed to fetch tickets")
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -139,7 +154,9 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Failed to fetch tickets")).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText("Failed to fetch tickets")
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
||||||
|
|
@ -150,19 +167,82 @@ describe("TicketsPage", () => {
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(document.querySelector("[data-slot='skeleton']")).not.toBeInTheDocument();
|
expect(
|
||||||
|
document.querySelector("[data-slot='skeleton']")
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByRole("table")).toBeInTheDocument();
|
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||||
expect(screen.getAllByRole("row")).toHaveLength(1); // header row only
|
expect(screen.getAllByRole("row")).toHaveLength(1); // header row only
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call axios.get with /api/tickets", async () => {
|
it("should call axios.get with /api/tickets and default sort params", async () => {
|
||||||
mockedAxios.get.mockResolvedValue({ data: { tickets: [] } });
|
mockedAxios.get.mockResolvedValue({ data: { tickets: [] } });
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedAxios.get).toHaveBeenCalledWith("/api/tickets");
|
expect(mockedAxios.get).toHaveBeenCalledWith("/api/tickets", {
|
||||||
|
params: { sortBy: "createdAt", sortOrder: "desc" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sort by column when clicking a column header", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockedAxios.get.mockResolvedValue({ data: { tickets: mockTickets } });
|
||||||
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Cannot login to my account")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedAxios.get.mockClear();
|
||||||
|
mockedAxios.get.mockResolvedValue({ data: { tickets: mockTickets } });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /Subject/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith("/api/tickets", {
|
||||||
|
params: { sortBy: "subject", sortOrder: "asc" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle sort order when clicking the same column header twice", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockedAxios.get.mockResolvedValue({ data: { tickets: mockTickets } });
|
||||||
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("Cannot login to my account")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedAxios.get.mockClear();
|
||||||
|
mockedAxios.get.mockResolvedValue({ data: { tickets: mockTickets } });
|
||||||
|
|
||||||
|
// First click: sort by Subject ascending
|
||||||
|
await user.click(screen.getByRole("button", { name: /Subject/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith("/api/tickets", {
|
||||||
|
params: { sortBy: "subject", sortOrder: "asc" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedAxios.get.mockClear();
|
||||||
|
mockedAxios.get.mockResolvedValue({ data: { tickets: mockTickets } });
|
||||||
|
|
||||||
|
// Second click: sort by Subject descending
|
||||||
|
await user.click(screen.getByRole("button", { name: /Subject/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith("/api/tickets", {
|
||||||
|
params: { sortBy: "subject", sortOrder: "desc" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type SortingState,
|
||||||
|
useReactTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
flexRender,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import { type TicketStatus } from "core/constants/ticket-status.ts";
|
import { type TicketStatus } from "core/constants/ticket-status.ts";
|
||||||
import { type TicketCategory } from "core/constants/ticket-category.ts";
|
import { type TicketCategory } from "core/constants/ticket-category.ts";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -13,7 +22,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle, ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
|
||||||
|
|
||||||
interface Ticket {
|
interface Ticket {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -25,25 +34,91 @@ interface Ticket {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusVariant: Record<TicketStatus, "default" | "secondary" | "outline"> = {
|
const statusVariant: Record<TicketStatus, "default" | "secondary" | "outline"> =
|
||||||
open: "default",
|
{
|
||||||
resolved: "secondary",
|
open: "default",
|
||||||
closed: "outline",
|
resolved: "secondary",
|
||||||
};
|
closed: "outline",
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<Ticket>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "subject",
|
||||||
|
header: "Subject",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "senderName",
|
||||||
|
header: "Sender",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div>{row.original.senderName}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{row.original.senderEmail}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant={statusVariant[row.original.status]}>
|
||||||
|
{row.original.status}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "category",
|
||||||
|
header: "Category",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.category ? (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{row.original.category.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: "Created",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
new Date(row.original.createdAt).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function TicketsTable() {
|
export default function TicketsTable() {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{ id: "createdAt", desc: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sortBy = sorting[0]?.id ?? "createdAt";
|
||||||
|
const sortOrder = sorting[0]?.desc ?? true ? "desc" : "asc";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: tickets,
|
data: tickets,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["tickets"],
|
queryKey: ["tickets", sortBy, sortOrder],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await axios.get<{ tickets: Ticket[] }>("/api/tickets");
|
const { data } = await axios.get<{ tickets: Ticket[] }>("/api/tickets", {
|
||||||
|
params: { sortBy, sortOrder },
|
||||||
|
});
|
||||||
return data.tickets;
|
return data.tickets;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: tickets ?? [],
|
||||||
|
columns,
|
||||||
|
state: { sorting },
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
manualSorting: true,
|
||||||
|
enableMultiSort: false,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
|
|
@ -56,13 +131,32 @@ export default function TicketsTable() {
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableHead>Subject</TableHead>
|
<TableRow key={headerGroup.id}>
|
||||||
<TableHead>Sender</TableHead>
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead>Status</TableHead>
|
<TableHead key={header.id}>
|
||||||
<TableHead>Category</TableHead>
|
<Button
|
||||||
<TableHead>Created</TableHead>
|
variant="ghost"
|
||||||
</TableRow>
|
size="sm"
|
||||||
|
className="-ml-3"
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{header.column.getIsSorted() === "asc" ? (
|
||||||
|
<ArrowUp className="ml-2 h-4 w-4" />
|
||||||
|
) : header.column.getIsSorted() === "desc" ? (
|
||||||
|
<ArrowDown className="ml-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading
|
{isLoading
|
||||||
|
|
@ -85,32 +179,13 @@ export default function TicketsTable() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
: tickets?.map((ticket) => (
|
: table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={ticket.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell>{ticket.subject}</TableCell>
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell>
|
<TableCell key={cell.id}>
|
||||||
<div>{ticket.senderName}</div>
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
<div className="text-sm text-muted-foreground">
|
</TableCell>
|
||||||
{ticket.senderEmail}
|
))}
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={statusVariant[ticket.status]}>
|
|
||||||
{ticket.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{ticket.category ? (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
{ticket.category.replace(/_/g, " ")}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
"—"
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{new Date(ticket.createdAt).toLocaleDateString()}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
"react": path.resolve(__dirname, "./node_modules/react"),
|
||||||
|
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
|
@ -24,5 +26,10 @@ export default defineConfig({
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: "./src/test/setup.ts",
|
setupFiles: "./src/test/setup.ts",
|
||||||
|
server: {
|
||||||
|
deps: {
|
||||||
|
inline: ["@tanstack/react-table"],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,18 @@ export const inboundEmailSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export type InboundEmailInput = z.infer<typeof inboundEmailSchema>;
|
export type InboundEmailInput = z.infer<typeof inboundEmailSchema>;
|
||||||
|
|
||||||
|
const sortableColumns = [
|
||||||
|
"subject",
|
||||||
|
"senderName",
|
||||||
|
"status",
|
||||||
|
"category",
|
||||||
|
"createdAt",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type TicketSortField = (typeof sortableColumns)[number];
|
||||||
|
|
||||||
|
export const ticketListQuerySchema = z.object({
|
||||||
|
sortBy: z.enum(sortableColumns).default("createdAt"),
|
||||||
|
sortOrder: z.enum(["asc", "desc"]).default("desc"),
|
||||||
|
});
|
||||||
|
|
|
||||||
953
server/prisma/seed-tickets.ts
Normal file
953
server/prisma/seed-tickets.ts
Normal file
|
|
@ -0,0 +1,953 @@
|
||||||
|
import "dotenv/config";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import {
|
||||||
|
PrismaClient,
|
||||||
|
TicketStatus,
|
||||||
|
TicketCategory,
|
||||||
|
} from "../src/generated/prisma/client";
|
||||||
|
|
||||||
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
const tickets: {
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
status: TicketStatus;
|
||||||
|
category: TicketCategory | null;
|
||||||
|
senderName: string;
|
||||||
|
senderEmail: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}[] = [
|
||||||
|
// --- OPEN / technical_question ---
|
||||||
|
{
|
||||||
|
subject: "App crashes when uploading large files",
|
||||||
|
body: "Whenever I try to upload a file larger than 50 MB the app crashes with an out-of-memory error.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Alice Johnson",
|
||||||
|
senderEmail: "alice.johnson@example.com",
|
||||||
|
createdAt: new Date("2026-02-23T09:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Two-factor authentication not sending SMS",
|
||||||
|
body: "I enabled 2FA but I never receive the SMS code. I've tried multiple times.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Marcus Chen",
|
||||||
|
senderEmail: "marcus.chen@techcorp.io",
|
||||||
|
createdAt: new Date("2026-02-22T14:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "API rate limit exceeded unexpectedly",
|
||||||
|
body: "We're hitting 429 errors even though our usage is well below the documented limits.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Priya Patel",
|
||||||
|
senderEmail: "priya@devstudio.com",
|
||||||
|
createdAt: new Date("2026-02-21T11:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Cannot connect to database after migration",
|
||||||
|
body: "After running the latest migration, our staging environment can no longer connect to the database.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "James O'Brien",
|
||||||
|
senderEmail: "james.obrien@startup.co",
|
||||||
|
createdAt: new Date("2026-02-20T16:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Webhook payloads missing timestamps",
|
||||||
|
body: "Since the last update, webhook payloads no longer include the created_at and updated_at fields.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Sofia Rodriguez",
|
||||||
|
senderEmail: "sofia@webworks.net",
|
||||||
|
createdAt: new Date("2026-02-19T08:20:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "SSL certificate renewal failing",
|
||||||
|
body: "Our auto-renewal for SSL certificates has been failing for the past week with a DNS validation error.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Daniel Kim",
|
||||||
|
senderEmail: "dkim@securehost.com",
|
||||||
|
createdAt: new Date("2026-02-18T13:10:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "GraphQL subscriptions dropping after 30 seconds",
|
||||||
|
body: "WebSocket connections for GraphQL subscriptions are being terminated after exactly 30 seconds every time.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Emma Watson",
|
||||||
|
senderEmail: "emma.w@appbuilders.io",
|
||||||
|
createdAt: new Date("2026-02-17T10:05:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Docker container keeps restarting in production",
|
||||||
|
body: "Our main API container restarts every 10 minutes. Logs show an OOMKilled error.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Liam Thompson",
|
||||||
|
senderEmail: "liam@cloudops.dev",
|
||||||
|
createdAt: new Date("2026-02-16T17:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Search index not updating for new records",
|
||||||
|
body: "New records added after Feb 15 are not appearing in search results. Reindexing manually doesn't help.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Aisha Mohammed",
|
||||||
|
senderEmail: "aisha@dataco.com",
|
||||||
|
createdAt: new Date("2026-02-15T12:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "OAuth callback URL mismatch error",
|
||||||
|
body: "Getting a redirect_uri_mismatch error when trying to log in with Google OAuth. Worked fine last week.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Carlos Mendez",
|
||||||
|
senderEmail: "carlos@freelance.dev",
|
||||||
|
createdAt: new Date("2026-02-14T09:45:00Z"),
|
||||||
|
},
|
||||||
|
// --- OPEN / general_question ---
|
||||||
|
{
|
||||||
|
subject: "How do I export my data to CSV?",
|
||||||
|
body: "I need to export all my project data to CSV for a quarterly report. Where is this option?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Rachel Green",
|
||||||
|
senderEmail: "rachel.green@marketing.co",
|
||||||
|
createdAt: new Date("2026-02-23T08:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "What are the differences between plan tiers?",
|
||||||
|
body: "Can you explain the differences between the Starter, Pro, and Enterprise plans?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Tom Baker",
|
||||||
|
senderEmail: "tom.baker@smallbiz.com",
|
||||||
|
createdAt: new Date("2026-02-22T10:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Can I add more team members to my account?",
|
||||||
|
body: "We just hired 5 new people. How do I add them to our team workspace?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Nina Kowalski",
|
||||||
|
senderEmail: "nina@agencyplus.com",
|
||||||
|
createdAt: new Date("2026-02-21T15:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Is there a mobile app available?",
|
||||||
|
body: "I'd like to manage my account on the go. Do you have iOS or Android apps?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "David Park",
|
||||||
|
senderEmail: "david.park@gmail.com",
|
||||||
|
createdAt: new Date("2026-02-20T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I change my account email address?",
|
||||||
|
body: "I recently changed companies and need to update my email address on file.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Laura Martinez",
|
||||||
|
senderEmail: "laura.m@oldcompany.com",
|
||||||
|
createdAt: new Date("2026-02-19T14:20:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Do you offer nonprofit discounts?",
|
||||||
|
body: "We're a registered 501(c)(3) nonprofit. Do you offer any special pricing?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Michael Brown",
|
||||||
|
senderEmail: "michael@helpinghandsfoundation.org",
|
||||||
|
createdAt: new Date("2026-02-18T11:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Where can I find the API documentation?",
|
||||||
|
body: "I'm a developer and I'd like to integrate your service into our app. Where are the API docs?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Yuki Tanaka",
|
||||||
|
senderEmail: "yuki@codelab.jp",
|
||||||
|
createdAt: new Date("2026-02-17T07:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "What payment methods do you accept?",
|
||||||
|
body: "Can I pay with a wire transfer instead of credit card?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Hans Mueller",
|
||||||
|
senderEmail: "hans@mueller-gmbh.de",
|
||||||
|
createdAt: new Date("2026-02-16T13:45:00Z"),
|
||||||
|
},
|
||||||
|
// --- OPEN / refund_request ---
|
||||||
|
{
|
||||||
|
subject: "Charged twice for February subscription",
|
||||||
|
body: "I was charged $49.99 twice on Feb 1st. Please refund the duplicate charge.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Jennifer Lee",
|
||||||
|
senderEmail: "jennifer.lee@inbox.com",
|
||||||
|
createdAt: new Date("2026-02-23T07:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Service outage caused lost revenue — requesting credit",
|
||||||
|
body: "The 6-hour outage on Feb 10 caused us to lose approximately $2,000 in sales. We'd like a service credit.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Robert Chang",
|
||||||
|
senderEmail: "robert@ecommerce-store.com",
|
||||||
|
createdAt: new Date("2026-02-22T16:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Accidentally upgraded to Enterprise plan",
|
||||||
|
body: "I clicked the wrong button and got upgraded to Enterprise ($299/mo). Please revert and refund.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Emily Nguyen",
|
||||||
|
senderEmail: "emily.nguyen@solopreneur.com",
|
||||||
|
createdAt: new Date("2026-02-21T09:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Cancellation processed but still being charged",
|
||||||
|
body: "I cancelled my subscription on Jan 15th but was still charged on Feb 1st.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Patrick O'Sullivan",
|
||||||
|
senderEmail: "patrick@osullivan.me",
|
||||||
|
createdAt: new Date("2026-02-20T11:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Refund for unused annual plan months",
|
||||||
|
body: "I paid annually but need to cancel after 4 months. Can I get a prorated refund for the remaining 8 months?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Sara Johansson",
|
||||||
|
senderEmail: "sara@nordicdesign.se",
|
||||||
|
createdAt: new Date("2026-02-19T10:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- OPEN / null category ---
|
||||||
|
{
|
||||||
|
subject: "Urgent: Account compromised",
|
||||||
|
body: "I think someone has accessed my account. I'm seeing login attempts from IP addresses I don't recognize.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Angela Foster",
|
||||||
|
senderEmail: "angela.foster@securemail.com",
|
||||||
|
createdAt: new Date("2026-02-23T06:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Partnership inquiry",
|
||||||
|
body: "We're a consulting firm interested in becoming a certified partner. Who should I speak with?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Brian Hayes",
|
||||||
|
senderEmail: "brian@consultpro.com",
|
||||||
|
createdAt: new Date("2026-02-22T12:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Feature request: Dark mode",
|
||||||
|
body: "Would love to see a dark mode option for the dashboard. It would really help with late-night work sessions.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Zoe Mitchell",
|
||||||
|
senderEmail: "zoe.mitchell@designstudio.co",
|
||||||
|
createdAt: new Date("2026-02-21T20:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Feedback on the new onboarding flow",
|
||||||
|
body: "The new onboarding wizard is confusing. Step 3 asks for information that wasn't mentioned in step 2.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Kevin Wright",
|
||||||
|
senderEmail: "kevin@wrightco.com",
|
||||||
|
createdAt: new Date("2026-02-20T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Request for SOC 2 compliance report",
|
||||||
|
body: "Our legal team requires your SOC 2 Type II report before we can proceed with the contract.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Catherine Davis",
|
||||||
|
senderEmail: "cdavis@enterprise-corp.com",
|
||||||
|
createdAt: new Date("2026-02-18T09:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- RESOLVED / technical_question ---
|
||||||
|
{
|
||||||
|
subject: "Login page returns 500 error",
|
||||||
|
body: "Every time I try to log in I get a 500 Internal Server Error. Started happening this morning.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Chris Evans",
|
||||||
|
senderEmail: "chris.evans@webmail.com",
|
||||||
|
createdAt: new Date("2026-02-10T08:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "File upload stuck at 99%",
|
||||||
|
body: "Uploads get to 99% and then just hang indefinitely. Happens with all file types.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Maria Garcia",
|
||||||
|
senderEmail: "maria@creativeco.com",
|
||||||
|
createdAt: new Date("2026-02-09T15:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Email notifications not being delivered",
|
||||||
|
body: "I haven't received any email notifications for the past 3 days despite having them enabled.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Ahmad Hassan",
|
||||||
|
senderEmail: "ahmad@techventures.ae",
|
||||||
|
createdAt: new Date("2026-02-08T11:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Broken image thumbnails in gallery view",
|
||||||
|
body: "All image thumbnails show as broken icons. Full-size images load fine.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Lisa Wong",
|
||||||
|
senderEmail: "lisa.wong@photopro.com",
|
||||||
|
createdAt: new Date("2026-02-07T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "CSV import failing with encoding error",
|
||||||
|
body: "Trying to import a UTF-8 CSV file but getting a 'character encoding mismatch' error.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Henrik Larsson",
|
||||||
|
senderEmail: "henrik@dataflow.se",
|
||||||
|
createdAt: new Date("2026-02-06T09:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Password reset link expired immediately",
|
||||||
|
body: "The password reset email I received had an already-expired link. Tried 3 times.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Olivia Taylor",
|
||||||
|
senderEmail: "olivia.t@personalmail.com",
|
||||||
|
createdAt: new Date("2026-02-05T16:20:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Dashboard widgets not loading on Firefox",
|
||||||
|
body: "All dashboard widgets show 'Loading...' indefinitely on Firefox 124. Works fine on Chrome.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Jake Morrison",
|
||||||
|
senderEmail: "jake@morrisontech.com",
|
||||||
|
createdAt: new Date("2026-02-04T10:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Timezone showing incorrectly in reports",
|
||||||
|
body: "All report timestamps show in UTC instead of my local timezone (PST), despite my settings being correct.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Amanda Clark",
|
||||||
|
senderEmail: "amanda.clark@westcoastbiz.com",
|
||||||
|
createdAt: new Date("2026-02-03T13:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "CORS error when embedding widget",
|
||||||
|
body: "Getting CORS errors when trying to embed the chat widget on our website at customdomain.com.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Ryan Patel",
|
||||||
|
senderEmail: "ryan@webagency.co",
|
||||||
|
createdAt: new Date("2026-02-02T08:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Bulk delete not working for archived items",
|
||||||
|
body: "The bulk delete option does nothing when I select archived items. No error message either.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Natasha Ivanova",
|
||||||
|
senderEmail: "natasha@rustech.ru",
|
||||||
|
createdAt: new Date("2026-02-01T17:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Mobile layout broken on iPad Pro",
|
||||||
|
body: "The responsive layout is completely broken on iPad Pro in landscape mode. Elements overlap.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "George Hamilton",
|
||||||
|
senderEmail: "george@mobilefirst.io",
|
||||||
|
createdAt: new Date("2026-01-31T11:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "PDF export cutting off right side of tables",
|
||||||
|
body: "When exporting reports to PDF, the rightmost columns are cut off on every page.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Diana Prince",
|
||||||
|
senderEmail: "diana@analytics-hub.com",
|
||||||
|
createdAt: new Date("2026-01-30T14:45:00Z"),
|
||||||
|
},
|
||||||
|
// --- RESOLVED / general_question ---
|
||||||
|
{
|
||||||
|
subject: "How to set up single sign-on (SSO)?",
|
||||||
|
body: "We'd like to configure SAML-based SSO for our organization. What are the steps?",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Victor Okonkwo",
|
||||||
|
senderEmail: "victor@nigeriabiz.ng",
|
||||||
|
createdAt: new Date("2026-02-08T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "What's the maximum file upload size?",
|
||||||
|
body: "We need to upload training videos (up to 2 GB). What's the file size limit?",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Sandra Miller",
|
||||||
|
senderEmail: "sandra@trainingco.com",
|
||||||
|
createdAt: new Date("2026-02-07T10:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I transfer project ownership?",
|
||||||
|
body: "I'm leaving the company and need to transfer all my projects to my colleague.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Paul Anderson",
|
||||||
|
senderEmail: "paul.anderson@corp.com",
|
||||||
|
createdAt: new Date("2026-02-06T13:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Can I customize the email notification templates?",
|
||||||
|
body: "We'd like to add our company logo and change the colors of the notification emails.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Isabelle Fontaine",
|
||||||
|
senderEmail: "isabelle@brandstudio.fr",
|
||||||
|
createdAt: new Date("2026-02-05T08:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How long is data retained after account deletion?",
|
||||||
|
body: "If I delete my account, how long do you keep my data? We need this info for GDPR compliance.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Klaus Schmidt",
|
||||||
|
senderEmail: "klaus@datenschutz.de",
|
||||||
|
createdAt: new Date("2026-02-04T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "What integrations are available?",
|
||||||
|
body: "Do you integrate with Slack, Jira, and Salesforce? We use all three daily.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Michelle Torres",
|
||||||
|
senderEmail: "michelle@projectmgmt.com",
|
||||||
|
createdAt: new Date("2026-02-03T10:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I enable audit logging?",
|
||||||
|
body: "Our compliance team requires full audit logs. How do I enable this feature?",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Anthony Robinson",
|
||||||
|
senderEmail: "anthony@fintechsolutions.com",
|
||||||
|
createdAt: new Date("2026-02-02T11:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- RESOLVED / refund_request ---
|
||||||
|
{
|
||||||
|
subject: "Overcharged on last invoice",
|
||||||
|
body: "My invoice shows $199 but my plan is $99/month. I was charged for the wrong tier.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Jessica White",
|
||||||
|
senderEmail: "jessica.white@budgetbiz.com",
|
||||||
|
createdAt: new Date("2026-02-06T07:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Trial converted to paid without warning",
|
||||||
|
body: "My free trial converted to a paid plan without any notification. I'd like a refund.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Benjamin Cole",
|
||||||
|
senderEmail: "ben.cole@testmail.com",
|
||||||
|
createdAt: new Date("2026-02-05T12:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Refund for downtime on January 28th",
|
||||||
|
body: "Your service was down for 4 hours on Jan 28. Per your SLA, we're entitled to a credit.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Stephanie Cruz",
|
||||||
|
senderEmail: "stephanie@legaltech.com",
|
||||||
|
createdAt: new Date("2026-02-04T15:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Wrong currency charged on invoice",
|
||||||
|
body: "I was charged in USD instead of EUR. The exchange rate difference cost me an extra €12.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Lucien Dubois",
|
||||||
|
senderEmail: "lucien@parisoffice.fr",
|
||||||
|
createdAt: new Date("2026-02-03T09:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Double charge for add-on feature",
|
||||||
|
body: "I was charged twice for the analytics add-on in January. Please refund one of the charges.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Karen Mitchell",
|
||||||
|
senderEmail: "karen@retailhq.com",
|
||||||
|
createdAt: new Date("2026-02-02T14:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- RESOLVED / null category ---
|
||||||
|
{
|
||||||
|
subject: "Account locked after too many failed attempts",
|
||||||
|
body: "My account is locked. I forgot my password and tried too many times.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: null,
|
||||||
|
senderName: "Trevor Barnes",
|
||||||
|
senderEmail: "trevor.barnes@outlook.com",
|
||||||
|
createdAt: new Date("2026-02-07T16:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Request to update company name on invoice",
|
||||||
|
body: "Our company name changed from 'Old Corp' to 'New Corp LLC'. Please update future invoices.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: null,
|
||||||
|
senderName: "Megan Foster",
|
||||||
|
senderEmail: "megan@newcorpllc.com",
|
||||||
|
createdAt: new Date("2026-02-05T10:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Suggestion: Add keyboard shortcuts",
|
||||||
|
body: "It would be great to have keyboard shortcuts for common actions like save (Ctrl+S) and search (Ctrl+K).",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: null,
|
||||||
|
senderName: "Alex Rivera",
|
||||||
|
senderEmail: "alex@productivepro.com",
|
||||||
|
createdAt: new Date("2026-02-03T07:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Need a copy of my invoice from December",
|
||||||
|
body: "Can you resend or provide a download link for my December 2025 invoice? I need it for tax filing.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: null,
|
||||||
|
senderName: "Sharon Lee",
|
||||||
|
senderEmail: "sharon.lee@accounting.com",
|
||||||
|
createdAt: new Date("2026-02-01T09:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- CLOSED / technical_question ---
|
||||||
|
{
|
||||||
|
subject: "Error 403 when accessing admin panel",
|
||||||
|
body: "I'm getting a 403 Forbidden error when trying to access the admin panel. I am an admin user.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Frank Murphy",
|
||||||
|
senderEmail: "frank@murphyindustries.com",
|
||||||
|
createdAt: new Date("2026-01-28T10:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Data sync between mobile and desktop not working",
|
||||||
|
body: "Changes made on mobile aren't syncing to desktop. Last sync was 3 days ago.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Ruby Chen",
|
||||||
|
senderEmail: "ruby.chen@syncapp.com",
|
||||||
|
createdAt: new Date("2026-01-27T14:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Custom domain showing SSL warning",
|
||||||
|
body: "Our custom domain helpdesk.ourcompany.com is showing an SSL certificate warning to visitors.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Nathan Brooks",
|
||||||
|
senderEmail: "nathan@ourcompany.com",
|
||||||
|
createdAt: new Date("2026-01-26T09:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Calendar integration showing wrong event times",
|
||||||
|
body: "Events synced from Google Calendar show up 5 hours off. My timezone settings are correct.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Victoria Sanchez",
|
||||||
|
senderEmail: "victoria@eventplanner.com",
|
||||||
|
createdAt: new Date("2026-01-25T11:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Cannot delete old workspace",
|
||||||
|
body: "Trying to delete a workspace from 2024 but the delete button is grayed out.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Timothy Adams",
|
||||||
|
senderEmail: "tim.adams@cleanup.io",
|
||||||
|
createdAt: new Date("2026-01-24T16:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Intermittent 504 Gateway Timeout errors",
|
||||||
|
body: "We're seeing random 504 errors during peak hours (2-4 PM EST). Affects about 10% of requests.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Samantha Phillips",
|
||||||
|
senderEmail: "sam@hightraffic.com",
|
||||||
|
createdAt: new Date("2026-01-23T15:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Markdown rendering broken in comments",
|
||||||
|
body: "Code blocks in comments no longer render correctly. They show raw markdown instead.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Oscar Nilsson",
|
||||||
|
senderEmail: "oscar@devteam.se",
|
||||||
|
createdAt: new Date("2026-01-22T08:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Export function producing corrupted Excel files",
|
||||||
|
body: "Exporting data to .xlsx format produces files that Excel says are corrupted and cannot open.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Hannah Wilson",
|
||||||
|
senderEmail: "hannah.w@finance-dept.com",
|
||||||
|
createdAt: new Date("2026-01-21T13:45:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Auto-save not working in form builder",
|
||||||
|
body: "Lost 30 minutes of work because auto-save silently stopped working in the form builder.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Derek James",
|
||||||
|
senderEmail: "derek@formdesign.co",
|
||||||
|
createdAt: new Date("2026-01-20T10:20:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Drag and drop not working in Safari",
|
||||||
|
body: "The drag-and-drop feature for reordering items doesn't work at all in Safari 18.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Fiona Campbell",
|
||||||
|
senderEmail: "fiona@macuser.co.uk",
|
||||||
|
createdAt: new Date("2026-01-19T09:30:00Z"),
|
||||||
|
},
|
||||||
|
// --- CLOSED / general_question ---
|
||||||
|
{
|
||||||
|
subject: "Do you offer training or onboarding sessions?",
|
||||||
|
body: "We're rolling out your platform to 200 employees. Do you provide training sessions?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Margaret Thompson",
|
||||||
|
senderEmail: "margaret@largecorp.com",
|
||||||
|
createdAt: new Date("2026-01-25T08:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "What browsers are officially supported?",
|
||||||
|
body: "We need to know which browsers are officially supported for our IT policy document.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Greg Patterson",
|
||||||
|
senderEmail: "greg@itdept.edu",
|
||||||
|
createdAt: new Date("2026-01-24T12:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I downgrade my plan?",
|
||||||
|
body: "I'd like to downgrade from Pro to Starter. How do I do that without losing my data?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Christina Lopez",
|
||||||
|
senderEmail: "christina@budgetsaver.com",
|
||||||
|
createdAt: new Date("2026-01-23T10:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Can I white-label the platform?",
|
||||||
|
body: "We want to offer this to our clients under our own brand. Is white-labeling available?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Douglas Reed",
|
||||||
|
senderEmail: "douglas@whitelabel.agency",
|
||||||
|
createdAt: new Date("2026-01-22T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Is there a sandbox environment for testing?",
|
||||||
|
body: "We'd like a sandbox to test our integrations before going live.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Wendy Zhang",
|
||||||
|
senderEmail: "wendy@testfirst.com",
|
||||||
|
createdAt: new Date("2026-01-21T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I set up automated reports?",
|
||||||
|
body: "I want to receive a weekly summary report every Monday morning. How do I set that up?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Roger Kim",
|
||||||
|
senderEmail: "roger@analytics-pro.com",
|
||||||
|
createdAt: new Date("2026-01-20T11:30:00Z"),
|
||||||
|
},
|
||||||
|
// --- CLOSED / refund_request ---
|
||||||
|
{
|
||||||
|
subject: "Charged after free trial expired — want refund",
|
||||||
|
body: "I forgot to cancel before the trial ended. I never used the paid features. Please refund.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Brenda Cooper",
|
||||||
|
senderEmail: "brenda.cooper@email.com",
|
||||||
|
createdAt: new Date("2026-01-26T07:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Refund for service not matching description",
|
||||||
|
body: "The feature advertised on your website doesn't work as described. I'd like a full refund.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Ivan Petrov",
|
||||||
|
senderEmail: "ivan@moscow-tech.ru",
|
||||||
|
createdAt: new Date("2026-01-24T13:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Accidental purchase of extra storage",
|
||||||
|
body: "I accidentally purchased 100 GB of extra storage. Can this be reversed?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Nancy Bell",
|
||||||
|
senderEmail: "nancy@thriftybiz.com",
|
||||||
|
createdAt: new Date("2026-01-22T10:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Request refund — migrating to competitor",
|
||||||
|
body: "We've decided to switch to another platform. Please refund the remaining days on our plan.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Peter Hoffman",
|
||||||
|
senderEmail: "peter@switching.co",
|
||||||
|
createdAt: new Date("2026-01-20T15:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Student charged full price instead of discounted rate",
|
||||||
|
body: "I signed up with my .edu email but was charged $49 instead of the $19 student rate.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Ashley Green",
|
||||||
|
senderEmail: "ashley.green@university.edu",
|
||||||
|
createdAt: new Date("2026-01-18T08:30:00Z"),
|
||||||
|
},
|
||||||
|
// --- CLOSED / null category ---
|
||||||
|
{
|
||||||
|
subject: "Thank you for the excellent support",
|
||||||
|
body: "Just wanted to say thanks to your support team. My issue was resolved within minutes!",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "William Turner",
|
||||||
|
senderEmail: "william@happycustomer.com",
|
||||||
|
createdAt: new Date("2026-01-27T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Interested in becoming a reseller",
|
||||||
|
body: "We'd like to resell your product in the Latin American market. Do you have a reseller program?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "Carmen Reyes",
|
||||||
|
senderEmail: "carmen@latamtech.mx",
|
||||||
|
createdAt: new Date("2026-01-25T14:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "GDPR data export request",
|
||||||
|
body: "Under GDPR Article 20, I'm requesting a full export of all personal data you hold about me.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "Eva Bergström",
|
||||||
|
senderEmail: "eva@privacy-first.eu",
|
||||||
|
createdAt: new Date("2026-01-23T08:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Bug bounty submission — XSS in profile page",
|
||||||
|
body: "I found a reflected XSS vulnerability on the profile edit page. Is there a bug bounty program?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "Raj Malhotra",
|
||||||
|
senderEmail: "raj@securityresearch.in",
|
||||||
|
createdAt: new Date("2026-01-21T17:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Accessibility issue: Screen reader not reading alerts",
|
||||||
|
body: "Error alerts on forms are not announced by screen readers (tested with NVDA and VoiceOver).",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "Danielle Price",
|
||||||
|
senderEmail: "danielle@a11y-matters.org",
|
||||||
|
createdAt: new Date("2026-01-19T12:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- Extra OPEN tickets for more variety ---
|
||||||
|
{
|
||||||
|
subject: "Billing page not loading",
|
||||||
|
body: "The billing page shows a white screen. Console shows a JavaScript error.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Tanya Reeves",
|
||||||
|
senderEmail: "tanya@startuphub.com",
|
||||||
|
createdAt: new Date("2026-02-13T08:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How to bulk import contacts?",
|
||||||
|
body: "We have 5,000 contacts in a spreadsheet. Is there a way to import them all at once?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Richard Stone",
|
||||||
|
senderEmail: "richard@salesforce-user.com",
|
||||||
|
createdAt: new Date("2026-02-12T10:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Charged in wrong currency (GBP instead of USD)",
|
||||||
|
body: "My last invoice was in British pounds but my account is set to USD.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Monica Hall",
|
||||||
|
senderEmail: "monica@piedpiper.com",
|
||||||
|
createdAt: new Date("2026-02-11T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Broken link in welcome email",
|
||||||
|
body: "The 'Get Started' button in the welcome email leads to a 404 page.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Steven Grant",
|
||||||
|
senderEmail: "steven.grant@newuser.com",
|
||||||
|
createdAt: new Date("2026-02-10T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Notification preferences keep resetting",
|
||||||
|
body: "Every time I disable email notifications, they re-enable themselves the next day.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Jasmine Howard",
|
||||||
|
senderEmail: "jasmine@quietplease.com",
|
||||||
|
createdAt: new Date("2026-02-09T11:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Request for volume pricing",
|
||||||
|
body: "We're looking to purchase 500 licenses. Do you offer volume discounts?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Walter Briggs",
|
||||||
|
senderEmail: "walter@megacorp.com",
|
||||||
|
createdAt: new Date("2026-02-08T13:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Unexpected charge of $9.99 for premium feature",
|
||||||
|
body: "I see a charge for 'Premium Analytics' that I never signed up for.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Diane Fletcher",
|
||||||
|
senderEmail: "diane@carefulspender.com",
|
||||||
|
createdAt: new Date("2026-02-07T08:15:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Can I schedule demo for my team?",
|
||||||
|
body: "We're evaluating tools and would love a live demo for our team of 12.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: null,
|
||||||
|
senderName: "Martin Cross",
|
||||||
|
senderEmail: "martin@evaluation-team.com",
|
||||||
|
createdAt: new Date("2026-02-06T15:00:00Z"),
|
||||||
|
},
|
||||||
|
// --- Extra RESOLVED ---
|
||||||
|
{
|
||||||
|
subject: "Print function cutting off headers",
|
||||||
|
body: "When I print a report, the header row is cut off on every page after the first.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Pamela York",
|
||||||
|
senderEmail: "pamela@printshop.com",
|
||||||
|
createdAt: new Date("2026-01-30T10:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "How do I archive old projects?",
|
||||||
|
body: "We have 200+ completed projects cluttering the dashboard. How do I archive them?",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Stanley Cooper",
|
||||||
|
senderEmail: "stanley@organizedpm.com",
|
||||||
|
createdAt: new Date("2026-01-29T13:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Charged for cancelled add-on",
|
||||||
|
body: "I cancelled the 'Priority Support' add-on last month but was charged again.",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Vanessa Brooks",
|
||||||
|
senderEmail: "vanessa@careful-billing.com",
|
||||||
|
createdAt: new Date("2026-01-28T11:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Request to merge duplicate accounts",
|
||||||
|
body: "I accidentally created two accounts with different emails. Can you merge them?",
|
||||||
|
status: TicketStatus.resolved,
|
||||||
|
category: null,
|
||||||
|
senderName: "Leonard Walsh",
|
||||||
|
senderEmail: "leonard@dupaccounts.com",
|
||||||
|
createdAt: new Date("2026-01-27T15:30:00Z"),
|
||||||
|
},
|
||||||
|
// --- Extra CLOSED ---
|
||||||
|
{
|
||||||
|
subject: "Slow page load times on dashboard",
|
||||||
|
body: "The main dashboard takes over 10 seconds to load. It used to load in under 2 seconds.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Gloria Simmons",
|
||||||
|
senderEmail: "gloria@speedmatters.com",
|
||||||
|
createdAt: new Date("2026-01-17T10:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Can I change my subdomain?",
|
||||||
|
body: "We rebranded and need to change our subdomain from oldname.app.com to newname.app.com.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Alberto Rossi",
|
||||||
|
senderEmail: "alberto@rebrand.it",
|
||||||
|
createdAt: new Date("2026-01-16T14:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Refund for duplicate annual subscription",
|
||||||
|
body: "I was charged for two annual subscriptions. Only one account is active.",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: TicketCategory.refund_request,
|
||||||
|
senderName: "Beth Hawkins",
|
||||||
|
senderEmail: "beth@oops-billing.com",
|
||||||
|
createdAt: new Date("2026-01-15T09:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Media inquiry about your company",
|
||||||
|
body: "I'm a journalist writing about SaaS platforms. Could I interview your CEO?",
|
||||||
|
status: TicketStatus.closed,
|
||||||
|
category: null,
|
||||||
|
senderName: "Charlotte Edwards",
|
||||||
|
senderEmail: "charlotte@techpress.com",
|
||||||
|
createdAt: new Date("2026-01-14T11:30:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "API returning stale cached data",
|
||||||
|
body: "The GET /projects endpoint returns data from yesterday even after I create new projects.",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.technical_question,
|
||||||
|
senderName: "Felix Hartmann",
|
||||||
|
senderEmail: "felix@cachebuster.dev",
|
||||||
|
createdAt: new Date("2026-02-12T07:00:00Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Need invoice with VAT number for tax purposes",
|
||||||
|
body: "Our accountant requires invoices with our EU VAT number (DE123456789). How do I add it?",
|
||||||
|
status: TicketStatus.open,
|
||||||
|
category: TicketCategory.general_question,
|
||||||
|
senderName: "Ingrid Bauer",
|
||||||
|
senderEmail: "ingrid@eufinance.de",
|
||||||
|
createdAt: new Date("2026-02-11T10:00:00Z"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Clear existing tickets
|
||||||
|
await prisma.ticket.deleteMany();
|
||||||
|
|
||||||
|
// Insert all 100 tickets
|
||||||
|
await prisma.ticket.createMany({ data: tickets });
|
||||||
|
|
||||||
|
console.log(`Seeded ${tickets.length} tickets successfully.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { requireAuth } from "../middleware/require-auth";
|
import { requireAuth } from "../middleware/require-auth";
|
||||||
|
import { validate } from "../lib/validate";
|
||||||
|
import { ticketListQuerySchema } from "core/schemas/tickets.ts";
|
||||||
import prisma from "../db";
|
import prisma from "../db";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", requireAuth, async (req, res) => {
|
router.get("/", requireAuth, async (req, res) => {
|
||||||
|
const query = validate(ticketListQuerySchema, req.query, res);
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
const tickets = await prisma.ticket.findMany({
|
const tickets = await prisma.ticket.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
@ -15,7 +20,7 @@ router.get("/", requireAuth, async (req, res) => {
|
||||||
senderEmail: true,
|
senderEmail: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { [query.sortBy]: query.sortOrder },
|
||||||
});
|
});
|
||||||
res.json({ tickets });
|
res.json({ tickets });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue