114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useParams } from "react-router-dom";
|
|
import { getProject, listProcessedTickets } from "../../lib/api";
|
|
import type { ProcessedTicket, Project } from "../../lib/types";
|
|
|
|
function statusBadgeClass(status: string): string {
|
|
switch (status) {
|
|
case "Pending":
|
|
return "bg-yellow-100 text-yellow-700";
|
|
case "Analyzing":
|
|
return "bg-blue-100 text-blue-700";
|
|
case "Developing":
|
|
return "bg-purple-100 text-purple-700";
|
|
case "Done":
|
|
return "bg-green-100 text-green-700";
|
|
case "Error":
|
|
return "bg-red-100 text-red-700";
|
|
case "Cancelled":
|
|
return "bg-gray-100 text-gray-500";
|
|
default:
|
|
return "bg-gray-100 text-gray-700";
|
|
}
|
|
}
|
|
|
|
export default function TicketList() {
|
|
const { projectId } = useParams();
|
|
const [project, setProject] = useState<Project | null>(null);
|
|
const [tickets, setTickets] = useState<ProcessedTicket[]>([]);
|
|
const [filter, setFilter] = useState<string>("all");
|
|
|
|
useEffect(() => {
|
|
if (!projectId) return;
|
|
Promise.all([getProject(projectId), listProcessedTickets(projectId)]).then(
|
|
([proj, tkts]) => {
|
|
setProject(proj);
|
|
setTickets(tkts);
|
|
}
|
|
);
|
|
}, [projectId]);
|
|
|
|
const filtered = filter === "all" ? tickets : tickets.filter((t) => t.status === filter);
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<Link to={`/projects/${projectId}`} className="text-sm text-blue-600 hover:underline">
|
|
{project?.name}
|
|
</Link>
|
|
<h2 className="text-xl font-bold">Processed Tickets</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4 flex gap-2">
|
|
{["all", "Pending", "Analyzing", "Developing", "Done", "Error"].map((s) => (
|
|
<button
|
|
key={s}
|
|
onClick={() => setFilter(s)}
|
|
className={`rounded px-3 py-1 text-sm ${
|
|
filter === s
|
|
? "bg-gray-900 text-white"
|
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
}`}
|
|
>
|
|
{s === "all" ? "All" : s}
|
|
{s !== "all" && (
|
|
<span className="ml-1 text-xs opacity-60">
|
|
({tickets.filter((t) => t.status === s).length})
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{filtered.length === 0 ? (
|
|
<div className="py-8 text-center text-sm text-gray-400">No tickets found.</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{filtered.map((ticket) => (
|
|
<Link
|
|
key={ticket.id}
|
|
to={`/tickets/${ticket.id}`}
|
|
className="block rounded-lg border border-gray-200 bg-white p-4 transition-colors hover:border-blue-300"
|
|
>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-xs text-gray-400">#{ticket.artifact_id}</span>
|
|
<span className="truncate text-sm font-medium">{ticket.artifact_title}</span>
|
|
</div>
|
|
<div className="mt-1 text-xs text-gray-400">
|
|
{new Date(ticket.detected_at).toLocaleString()}
|
|
{ticket.processed_at && (
|
|
<span className="ml-2">
|
|
Processed: {new Date(ticket.processed_at).toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<span
|
|
className={`shrink-0 rounded-full px-2 py-0.5 text-xs font-medium ${statusBadgeClass(
|
|
ticket.status
|
|
)}`}
|
|
>
|
|
{ticket.status}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|