Skip to content

Commit d2f1090

Browse files
committed
feat: implement archive and unarchive functionality for ticket support
1 parent be4b1b0 commit d2f1090

7 files changed

Lines changed: 184 additions & 19 deletions

File tree

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33

4-
import { Search, ChevronDown, SlidersHorizontal, ArrowUpDown } from "lucide-react";
4+
import { Search, ChevronDown, SlidersHorizontal, ArrowUpDown, Archive as FolderArchive } from "lucide-react";
55
import {
66
Table,
77
TableBody,
@@ -38,6 +38,8 @@ interface TicketDataTableProps {
3838
codevList: { id: string; name: string; image_url: string | null }[];
3939
selectedTicketId: string | null;
4040
onTicketSelect: (ticket: TicketSupport) => void;
41+
showArchived: boolean;
42+
onShowArchivedChange: (show: boolean) => void;
4143
}
4244

4345
function DropdownMenu({
@@ -118,6 +120,8 @@ export default function TicketDataTable({
118120
codevList,
119121
selectedTicketId,
120122
onTicketSelect,
123+
showArchived,
124+
onShowArchivedChange,
121125
}: TicketDataTableProps) {
122126
const activeFilterCount =
123127
(filterStatus !== "ALL" ? 1 : 0) +
@@ -141,6 +145,18 @@ export default function TicketDataTable({
141145
</div>
142146

143147
<div className="flex items-center gap-2">
148+
{/* Archive Toggle */}
149+
<button
150+
onClick={() => onShowArchivedChange(!showArchived)}
151+
className={`flex items-center gap-2 rounded-lg border px-4 py-2 text-sm transition-all duration-200 ${
152+
showArchived
153+
? "border-purple-500/50 bg-purple-500/10 text-purple-600 dark:text-purple-400"
154+
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300 dark:border-zinc-700 dark:bg-zinc-900 dark:text-gray-300"
155+
}`}
156+
>
157+
<FolderArchive className={`h-4 w-4 ${showArchived ? "animate-pulse" : ""}`} />
158+
{showArchived ? "Showing Archived" : "Show Archived"}
159+
</button>
144160
{/* Sort By */}
145161
<DropdownMenu
146162
trigger={

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default function TicketManagementView({
4343
const [filterStatus, setFilterStatus] = useState<string>("ALL");
4444
const [filterPriority, setFilterPriority] = useState<string>("ALL");
4545
const [filterAssignedTo, setFilterAssignedTo] = useState<string>("ALL");
46+
const [showArchived, setShowArchived] = useState(false);
4647

4748
const [wasSidebarOpen, setWasSidebarOpen] = useState(false);
4849

@@ -63,6 +64,13 @@ export default function TicketManagementView({
6364
const filteredAndSortedTickets = useMemo(() => {
6465
let result = [...initialTickets];
6566

67+
// Archive filter
68+
if (showArchived) {
69+
result = result.filter((t) => t.is_archived);
70+
} else {
71+
result = result.filter((t) => !t.is_archived);
72+
}
73+
6674
// Search filter
6775
if (searchQuery.trim()) {
6876
const query = searchQuery.toLowerCase();
@@ -118,7 +126,7 @@ export default function TicketManagementView({
118126
});
119127

120128
return result;
121-
}, [initialTickets, searchQuery, sortField, sortDirection, filterStatus, filterPriority, filterAssignedTo]);
129+
}, [initialTickets, searchQuery, sortField, sortDirection, filterStatus, filterPriority, filterAssignedTo, showArchived]);
122130

123131
const handleTicketSelect = (ticket: TicketSupport) => {
124132
setSelectedTicket(ticket);
@@ -172,6 +180,8 @@ export default function TicketManagementView({
172180
codevList={codevList}
173181
selectedTicketId={selectedTicket?.id || null}
174182
onTicketSelect={handleTicketSelect}
183+
showArchived={showArchived}
184+
onShowArchivedChange={setShowArchived}
175185
/>
176186
</div>
177187

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

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
updateTicketPriority,
2626
updateTicketAssignment,
2727
deleteTicket,
28+
archiveTicket,
29+
unarchiveTicket,
2830
} from "../actions";
31+
import { Archive, ArchiveRestore } from "lucide-react";
2932

3033
interface TicketPreviewSidebarProps {
3134
ticket: TicketSupport;
@@ -42,6 +45,8 @@ export default function TicketPreviewSidebar({
4245
}: TicketPreviewSidebarProps) {
4346
const [isPending, startTransition] = useTransition();
4447
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
48+
const [showArchiveConfirm, setShowArchiveConfirm] = useState(false);
49+
const [showUnarchiveConfirm, setShowUnarchiveConfirm] = useState(false);
4550
const [error, setError] = useState<string | null>(null);
4651

4752
const statusColor = STATUS_COLORS[ticket.status as TicketStatus] || STATUS_COLORS.PENDING;
@@ -133,6 +138,47 @@ export default function TicketPreviewSidebar({
133138
});
134139
};
135140

141+
const handleArchive = () => {
142+
setError(null);
143+
startTransition(async () => {
144+
try {
145+
const res = await archiveTicket(ticket.id);
146+
if (!res.success) {
147+
setError(`Archive failed: ${res.error}`);
148+
setShowArchiveConfirm(false);
149+
return;
150+
}
151+
setShowArchiveConfirm(false);
152+
onClose(); // Close sidebar after successful archive
153+
onUpdated();
154+
} catch (err: any) {
155+
console.error("Failed to archive ticket:", err);
156+
setError(`Archive failed: ${err.message}`);
157+
setShowArchiveConfirm(false);
158+
}
159+
});
160+
};
161+
162+
const handleUnarchive = () => {
163+
setError(null);
164+
startTransition(async () => {
165+
try {
166+
const res = await unarchiveTicket(ticket.id);
167+
if (!res.success) {
168+
setError(`Unarchive failed: ${res.error}`);
169+
setShowUnarchiveConfirm(false);
170+
return;
171+
}
172+
setShowUnarchiveConfirm(false);
173+
onUpdated();
174+
} catch (err: any) {
175+
console.error("Failed to unarchive ticket:", err);
176+
setError(`Unarchive failed: ${err.message}`);
177+
setShowUnarchiveConfirm(false);
178+
}
179+
});
180+
};
181+
136182
return (
137183
<div
138184
className="fixed right-0 top-[64px] bottom-0 z-[45] w-full max-w-[420px] border-l border-gray-200 bg-white shadow-2xl overflow-y-auto dark:border-zinc-800 dark:bg-zinc-950"
@@ -169,6 +215,46 @@ export default function TicketPreviewSidebar({
169215
No
170216
</button>
171217
</div>
218+
) : showArchiveConfirm ? (
219+
<div className="flex items-center gap-2">
220+
<span className="text-[10px] font-medium text-purple-500 uppercase tracking-wider">
221+
Confirm Archive?
222+
</span>
223+
<button
224+
onClick={handleArchive}
225+
disabled={isPending}
226+
className="rounded-lg bg-purple-500 px-2 py-1 text-xs font-medium text-white hover:bg-purple-600 disabled:opacity-50"
227+
>
228+
{isPending ? "..." : "Yes"}
229+
</button>
230+
<button
231+
onClick={() => setShowArchiveConfirm(false)}
232+
disabled={isPending}
233+
className="rounded-lg bg-gray-100 px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-200 dark:bg-zinc-800 dark:text-gray-400 dark:hover:bg-zinc-700"
234+
>
235+
No
236+
</button>
237+
</div>
238+
) : showUnarchiveConfirm ? (
239+
<div className="flex items-center gap-2">
240+
<span className="text-[10px] font-medium text-green-500 uppercase tracking-wider">
241+
Unarchive?
242+
</span>
243+
<button
244+
onClick={handleUnarchive}
245+
disabled={isPending}
246+
className="rounded-lg bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-green-600 disabled:opacity-50"
247+
>
248+
{isPending ? "..." : "Yes"}
249+
</button>
250+
<button
251+
onClick={() => setShowUnarchiveConfirm(false)}
252+
disabled={isPending}
253+
className="rounded-lg bg-gray-100 px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-200 dark:bg-zinc-800 dark:text-gray-400 dark:hover:bg-zinc-700"
254+
>
255+
No
256+
</button>
257+
</div>
172258
) : (
173259
<div className="flex items-center gap-1">
174260
<button
@@ -178,6 +264,23 @@ export default function TicketPreviewSidebar({
178264
>
179265
<Trash2 className="h-5 w-5" />
180266
</button>
267+
{ticket.is_archived ? (
268+
<button
269+
onClick={() => setShowUnarchiveConfirm(true)}
270+
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-green-50 hover:text-green-500 dark:hover:bg-green-500/10"
271+
title="Unarchive Ticket"
272+
>
273+
<ArchiveRestore className="h-5 w-5" />
274+
</button>
275+
) : (
276+
<button
277+
onClick={() => setShowArchiveConfirm(true)}
278+
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-purple-50 hover:text-purple-500 dark:hover:bg-purple-500/10"
279+
title="Archive Ticket"
280+
>
281+
<Archive className="h-5 w-5" />
282+
</button>
283+
)}
181284
<button
182285
onClick={onClose}
183286
className="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 hover:text-gray-900 dark:hover:text-white"
@@ -214,11 +317,18 @@ export default function TicketPreviewSidebar({
214317
{ticket.subject || "No Subject"}
215318
</h3>
216319
</div>
217-
<span
218-
className={`shrink-0 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusColor.bg} ${statusColor.text}`}
219-
>
220-
{STATUS_LABELS[ticket.status as TicketStatus]}
221-
</span>
320+
<div className="flex items-center gap-2">
321+
<span
322+
className={`shrink-0 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusColor.bg} ${statusColor.text}`}
323+
>
324+
{STATUS_LABELS[ticket.status as TicketStatus]}
325+
</span>
326+
{ticket.is_archived && (
327+
<span className="inline-flex items-center rounded-full bg-purple-500/10 px-2.5 py-0.5 text-xs font-medium text-purple-600 dark:bg-purple-500/20 dark:text-purple-400">
328+
Archived
329+
</span>
330+
)}
331+
</div>
222332
</div>
223333
</div>
224334

‎apps/codebility/app/home/admin-controls/ticket-support/actions.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,43 @@ export async function deleteTicket(ticketId: string) {
180180
revalidatePath("/home/admin-controls/ticket-support");
181181
return { success: true };
182182
}
183+
184+
export async function archiveTicket(ticketId: string) {
185+
const supabase = await createClientServerComponent();
186+
187+
const { data, error } = await supabase
188+
.from("ticket_support")
189+
.update({ is_archived: true, updated_at: new Date().toISOString() })
190+
.eq("id", ticketId)
191+
.select();
192+
193+
console.log("[archiveTicket]", { ticketId, data, error });
194+
195+
if (error) {
196+
console.error("Error archiving ticket:", error);
197+
return { success: false, error: error.message };
198+
}
199+
200+
revalidatePath("/home/admin-controls/ticket-support");
201+
return { success: true };
202+
}
203+
204+
export async function unarchiveTicket(ticketId: string) {
205+
const supabase = await createClientServerComponent();
206+
207+
const { data, error } = await supabase
208+
.from("ticket_support")
209+
.update({ is_archived: false, updated_at: new Date().toISOString() })
210+
.eq("id", ticketId)
211+
.select();
212+
213+
console.log("[unarchiveTicket]", { ticketId, data, error });
214+
215+
if (error) {
216+
console.error("Error unarchiving ticket:", error);
217+
return { success: false, error: error.message };
218+
}
219+
220+
revalidatePath("/home/admin-controls/ticket-support");
221+
return { success: true };
222+
}

‎apps/codebility/app/home/admin-controls/ticket-support/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface TicketSupport {
1515
message: string;
1616
status: TicketStatus;
1717
priority: TicketPriority;
18+
is_archived: boolean;
1819
created_at: string;
1920
updated_at: string;
2021
assigned_to: string | null;

‎apps/codebility/constants/settings.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ export const adminControlsCardData: SettingsCardDataProps[] = [
5252
title: "Client Tracker",
5353
description: "Track weekly client outreach by all admins.",
5454
},
55-
{
56-
path: "/home/admin-controls/ticket-support",
57-
imageName: "icon-ticket-support",
58-
imageAlt: "Icon Ticket Support",
59-
title: "Ticket Support Management",
60-
description: "Manage and track support tickets efficiently.",
61-
},
6255
{
6356
path: "/home/promote-modal/",
6457
imageName: "icon-edit",

‎apps/codebility/middleware.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,6 @@ export async function middleware(req: NextRequest) {
220220
}
221221

222222
if (matchedRoute && role_id) {
223-
// TEMP BYPASS for testing Ticket Management
224-
if (pathname.startsWith("/home/admin-controls")) {
225-
return NextResponse.next();
226-
}
227-
228223
const key = matchedRoute as keyof typeof routePermissionMap;
229224
const requiredPermission = routePermissionMap[key];
230225

0 commit comments

Comments
 (0)