feat(257): add Revert Status + Assigned to me filter
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { RefreshCw, Ticket, Plus, Send } from "lucide-react";
|
||||
import { RefreshCw, Ticket, Plus, Send, RotateCcw, User } from "lucide-react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/Card";
|
||||
import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
@@ -34,11 +35,20 @@ const priorityVariant: Record<string, "danger" | "warning" | "muted"> = {
|
||||
const statusFilters = ["", "OPEN", "IN_PROGRESS", "RESOLVED", "CLOSED"];
|
||||
const typeFilters = ["", "SUPPORT", "BILLING", "INSTALLATION"];
|
||||
|
||||
// Previous status mapping (revert flow)
|
||||
const prevStatus: Record<string, string> = {
|
||||
CLOSED: "RESOLVED",
|
||||
RESOLVED: "IN_PROGRESS",
|
||||
IN_PROGRESS: "OPEN",
|
||||
};
|
||||
|
||||
export default function TicketsPage() {
|
||||
const qc = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState("");
|
||||
const [assignedToMe, setAssignedToMe] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState<TicketItem | null>(null);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
@@ -46,12 +56,13 @@ export default function TicketsPage() {
|
||||
const [newForm, setNewForm] = useState({ subject: "", description: "", type: "SUPPORT", priority: "NORMAL", clientSearch: "", clientId: "" });
|
||||
|
||||
const { data, isLoading, refetch } = useQuery<PaginatedResponse<TicketItem>>({
|
||||
queryKey: ["tickets", search, statusFilter, typeFilter, page],
|
||||
queryKey: ["tickets", search, statusFilter, typeFilter, assignedToMe, page],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({ page: String(page), limit: "20" });
|
||||
if (search) params.set("search", search);
|
||||
if (statusFilter) params.set("status", statusFilter);
|
||||
if (typeFilter) params.set("type", typeFilter);
|
||||
if (assignedToMe && user?.id) params.set("assignedToId", user.id);
|
||||
const res = await api.get<PaginatedResponse<TicketItem>>(`/api/v1/tickets?${params}`);
|
||||
return res.data;
|
||||
},
|
||||
@@ -109,6 +120,14 @@ export default function TicketsPage() {
|
||||
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create ticket"),
|
||||
});
|
||||
|
||||
const revertStatus = useMutation({
|
||||
mutationFn: async ({ id, status }: { id: string; status: string }) => {
|
||||
await api.patch(`/api/v1/tickets/${id}`, { status });
|
||||
},
|
||||
onSuccess: () => { toast.success("Status reverted"); refetch(); refetchDetail(); qc.invalidateQueries({ queryKey: ["ticket"] }); },
|
||||
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed"),
|
||||
});
|
||||
|
||||
const tickets = data?.data ?? [];
|
||||
const total = data?.meta?.total ?? 0;
|
||||
const detail = ticketDetail ?? selected;
|
||||
@@ -150,6 +169,13 @@ export default function TicketsPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setAssignedToMe(v => !v); setPage(1); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${assignedToMe ? "bg-green-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"}`}
|
||||
title="Show only tickets assigned to me"
|
||||
>
|
||||
<User size={12} /> Assigned to me
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
@@ -203,11 +229,18 @@ export default function TicketsPage() {
|
||||
</div>
|
||||
|
||||
{/* Status actions */}
|
||||
{nextStatus[detail.status] && (
|
||||
<Button size="sm" onClick={() => updateStatus.mutate({ id: detail.id, status: nextStatus[detail.status] })} isLoading={updateStatus.isPending}>
|
||||
Move to {nextStatus[detail.status].replace("_", " ")}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{nextStatus[detail.status] && (
|
||||
<Button size="sm" onClick={() => updateStatus.mutate({ id: detail.id, status: nextStatus[detail.status] })} isLoading={updateStatus.isPending}>
|
||||
Move to {nextStatus[detail.status].replace("_", " ")}
|
||||
</Button>
|
||||
)}
|
||||
{prevStatus[detail.status] && (
|
||||
<Button size="sm" variant="outline" onClick={() => revertStatus.mutate({ id: detail.id, status: prevStatus[detail.status] })} isLoading={revertStatus.isPending}>
|
||||
<RotateCcw size={13} className="mr-1" /> Revert to {prevStatus[detail.status].replace("_", " ")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Comments */}
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user