Skip to content

Commit 3178149

Browse files
committed
feat(tms): implement ticket management system with advanced filter, sidebar sync, and production UI refinements
1 parent a593794 commit 3178149

12 files changed

Lines changed: 1458 additions & 74 deletions

File tree

‎apps/codebility/app/home/admin-controls/ticket-support/_components/TicketDataTable.tsx‎

Lines changed: 390 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"use client";
2+
3+
import { useState, useMemo, useEffect } from "react";
4+
import { useRouter } from "next/navigation";
5+
import type { TicketSupport } from "../types";
6+
import TicketDataTable from "./TicketDataTable";
7+
import TicketPreviewSidebar from "./TicketPreviewSidebar";
8+
import { useNavStore } from "@/hooks/use-sidebar";
9+
10+
interface TicketManagementViewProps {
11+
initialTickets: TicketSupport[];
12+
codevList: { id: string; name: string; image_url: string | null }[];
13+
}
14+
15+
type SortField = "created_at" | "priority" | "status" | "ticket_number" | "assigned_to";
16+
type SortDirection = "asc" | "desc";
17+
18+
const PRIORITY_ORDER: Record<string, number> = {
19+
URGENT: 4,
20+
HIGH: 3,
21+
MEDIUM: 2,
22+
LOW: 1,
23+
};
24+
25+
const STATUS_ORDER: Record<string, number> = {
26+
PENDING: 1,
27+
OPEN: 2,
28+
IN_PROGRESS: 3,
29+
RESOLVED: 4,
30+
CLOSED: 5,
31+
};
32+
33+
export default function TicketManagementView({
34+
initialTickets,
35+
codevList,
36+
}: TicketManagementViewProps) {
37+
const router = useRouter();
38+
const { closeNav, toggleNav, isToggleOpen } = useNavStore();
39+
const [selectedTicket, setSelectedTicket] = useState<TicketSupport | null>(null);
40+
const [searchQuery, setSearchQuery] = useState("");
41+
const [sortField, setSortField] = useState<SortField>("created_at");
42+
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
43+
const [filterStatus, setFilterStatus] = useState<string>("ALL");
44+
const [filterPriority, setFilterPriority] = useState<string>("ALL");
45+
const [filterAssignedTo, setFilterAssignedTo] = useState<string>("ALL");
46+
47+
const [wasSidebarOpen, setWasSidebarOpen] = useState(false);
48+
49+
50+
useEffect(() => {
51+
if (selectedTicket) {
52+
const updated = initialTickets.find((t) => t.id === selectedTicket.id);
53+
if (updated) {
54+
55+
setSelectedTicket(updated);
56+
} else {
57+
58+
handleCloseSidebar();
59+
}
60+
}
61+
}, [initialTickets]);
62+
63+
const filteredAndSortedTickets = useMemo(() => {
64+
let result = [...initialTickets];
65+
66+
// Search filter
67+
if (searchQuery.trim()) {
68+
const query = searchQuery.toLowerCase();
69+
result = result.filter(
70+
(t) =>
71+
t.ticket_number.toLowerCase().includes(query) ||
72+
(t.subject || "").toLowerCase().includes(query) ||
73+
t.full_name.toLowerCase().includes(query) ||
74+
t.ticket_type.toLowerCase().includes(query)
75+
);
76+
}
77+
78+
// Status filter
79+
if (filterStatus !== "ALL") {
80+
result = result.filter((t) => t.status === filterStatus);
81+
}
82+
83+
// Priority filter
84+
if (filterPriority !== "ALL") {
85+
result = result.filter((t) => t.priority === filterPriority);
86+
}
87+
88+
// Assigned To filter
89+
if (filterAssignedTo !== "ALL") {
90+
if (filterAssignedTo === "UNASSIGNED") {
91+
result = result.filter((t) => !t.assigned_to);
92+
} else {
93+
result = result.filter((t) => t.assigned_to === filterAssignedTo);
94+
}
95+
}
96+
97+
// Sort
98+
result.sort((a, b) => {
99+
let comparison = 0;
100+
switch (sortField) {
101+
case "created_at":
102+
comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
103+
break;
104+
case "priority":
105+
comparison = (PRIORITY_ORDER[a.priority] || 0) - (PRIORITY_ORDER[b.priority] || 0);
106+
break;
107+
case "status":
108+
comparison = (STATUS_ORDER[a.status] || 0) - (STATUS_ORDER[b.status] || 0);
109+
break;
110+
case "ticket_number":
111+
comparison = a.ticket_number.localeCompare(b.ticket_number);
112+
break;
113+
case "assigned_to":
114+
comparison = (a.assigned_to_name || "").localeCompare(b.assigned_to_name || "");
115+
break;
116+
}
117+
return sortDirection === "desc" ? -comparison : comparison;
118+
});
119+
120+
return result;
121+
}, [initialTickets, searchQuery, sortField, sortDirection, filterStatus, filterPriority, filterAssignedTo]);
122+
123+
const handleTicketSelect = (ticket: TicketSupport) => {
124+
setSelectedTicket(ticket);
125+
setWasSidebarOpen(isToggleOpen);
126+
if (isToggleOpen) {
127+
closeNav();
128+
}
129+
};
130+
131+
const handleCloseSidebar = () => {
132+
setSelectedTicket(null);
133+
if (wasSidebarOpen && !isToggleOpen) {
134+
toggleNav();
135+
}
136+
};
137+
138+
const handleTicketUpdated = () => {
139+
// Trigger server-side re-fetch of data
140+
router.refresh();
141+
};
142+
143+
return (
144+
<div className="relative flex min-h-[600px]">
145+
{/* Main Table Area */}
146+
<div
147+
className={`flex-1 transition-all duration-300 ${
148+
selectedTicket ? "pr-0 lg:pr-[420px]" : ""
149+
}`}
150+
>
151+
<TicketDataTable
152+
tickets={filteredAndSortedTickets}
153+
searchQuery={searchQuery}
154+
onSearchChange={setSearchQuery}
155+
sortField={sortField}
156+
sortDirection={sortDirection}
157+
onSortChange={(field) => {
158+
const f = field as SortField;
159+
if (f === sortField) {
160+
setSortDirection((d) => (d === "asc" ? "desc" : "asc"));
161+
} else {
162+
setSortField(f);
163+
setSortDirection("desc");
164+
}
165+
}}
166+
filterStatus={filterStatus}
167+
onFilterStatusChange={setFilterStatus}
168+
filterPriority={filterPriority}
169+
onFilterPriorityChange={setFilterPriority}
170+
filterAssignedTo={filterAssignedTo}
171+
onFilterAssignedToChange={setFilterAssignedTo}
172+
codevList={codevList}
173+
selectedTicketId={selectedTicket?.id || null}
174+
onTicketSelect={handleTicketSelect}
175+
/>
176+
</div>
177+
178+
{/* Overlay for clicking outside */}
179+
{selectedTicket && (
180+
<div
181+
className="fixed inset-0 z-[40] bg-black/5 dark:bg-black/20"
182+
/>
183+
)}
184+
185+
{/* Preview Sidebar */}
186+
{selectedTicket && (
187+
<TicketPreviewSidebar
188+
ticket={selectedTicket}
189+
codevList={codevList}
190+
onClose={handleCloseSidebar}
191+
onUpdated={handleTicketUpdated}
192+
/>
193+
)}
194+
</div>
195+
);
196+
}

0 commit comments

Comments
 (0)