wulf-pulse/app/admin/data-browser/tickets/page.tsx
lorentz c97e5fc45c feat: status popover + CSV export
Two follow-ons after the ⌘K palette:

StatusIndicator → Popover
- The top-bar status light is no longer a direct link to /status.
  Clicking it opens a popover with grouped issues (failing
  integrations, expired tokens, expiring tokens) so a quick glance
  answers "what's broken" without leaving the current page.  A "View
  full status" link at the bottom routes to /status when needed.
- The trigger keeps the same color rollup so the visual hint is
  visible without opening the popover.

DataTable → CSV export
- Optional `exportable` + `exportFilename` props add an "Export CSV"
  button next to the search bar.  Default behavior exports the current
  page; pass `onExportAll` for server-side full-result downloads.
- Built client-side from column defs (label → header, raw value →
  cell).  BOM-prefixed UTF-8 so Excel decodes correctly.  Quoting +
  escape handled.
- Enabled on /admin/data-browser/{companies,tickets} as initial demos.
  Other data-browser pages opt in by adding two props.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:20:27 -04:00

189 lines
5.6 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import DataTable from '@/components/admin/DataTable';
import DetailModal from '@/components/admin/DetailModal';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, Ticket } from 'lucide-react';
import Link from 'next/link';
export default function TicketsBrowserPage() {
const router = useRouter();
const [tickets, setTickets] = useState([]);
const [totalCount, setTotalCount] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(50);
const [isLoading, setIsLoading] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<any>(null);
const [modalOpen, setModalOpen] = useState(false);
const fetchTickets = async (currentPage: number, search?: string, sortBy?: string, sortOrder?: string) => {
setIsLoading(true);
try {
const params = new URLSearchParams({
page: currentPage.toString(),
limit: pageSize.toString(),
});
if (search) params.append('search', search);
if (sortBy) params.append('sort', sortBy);
if (sortOrder) params.append('order', sortOrder);
const response = await fetch(`/api/data/tickets?${params}`);
const result = await response.json();
setTickets(result.data || []);
setTotalCount(result.pagination?.total || 0);
} catch (error) {
console.error('Failed to fetch tickets:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTickets(page);
}, [page]);
const handleRowClick = (ticket: any) => {
setSelectedTicket(ticket);
setModalOpen(true);
};
const columns = [
{
key: 'id',
label: 'ID',
sortable: true,
},
{
key: 'ticket_number',
label: 'Ticket #',
sortable: true,
},
{
key: 'title',
label: 'Title',
sortable: true,
render: (value: string) => (
<div className="max-w-md truncate" title={value}>
{value}
</div>
),
},
{
key: 'status',
label: 'Status',
sortable: true,
render: (value: number) => (
<Badge variant="outline">{value}</Badge>
),
},
{
key: 'priority',
label: 'Priority',
sortable: true,
render: (value: number) => {
const variant = value === 1 ? 'destructive' : value === 2 ? 'default' : 'secondary';
return <Badge variant={variant}>{value}</Badge>;
},
},
{
key: 'company_id',
label: 'Company ID',
sortable: true,
},
{
key: 'create_date',
label: 'Created',
sortable: true,
render: (value: string) => value ? new Date(value).toLocaleDateString() : '-',
},
{
key: 'is_deleted',
label: 'Deleted',
render: (value: boolean) => (
<Badge variant={value ? 'destructive' : 'secondary'}>
{value ? 'Yes' : 'No'}
</Badge>
),
},
];
const detailFields = [
{ key: 'id', label: 'ID' },
{ key: 'ticket_number', label: 'Ticket Number' },
{ key: 'title', label: 'Title' },
{ key: 'description', label: 'Description' },
{ key: 'status', label: 'Status' },
{ key: 'priority', label: 'Priority' },
{ key: 'company_id', label: 'Company ID' },
{ key: 'contact_id', label: 'Contact ID' },
{ key: 'assigned_resource_id', label: 'Assigned Resource' },
{ key: 'queue_id', label: 'Queue ID' },
{ key: 'issue_type', label: 'Issue Type' },
{ key: 'sub_issue_type', label: 'Sub Issue Type' },
{ key: 'source', label: 'Source' },
{ key: 'due_date_time', label: 'Due Date' },
{ key: 'estimated_hours', label: 'Estimated Hours' },
{ key: 'completed_date', label: 'Completed Date' },
{ key: 'create_date', label: 'Created Date' },
{ key: 'last_activity_date', label: 'Last Activity' },
{ key: 'synced_at', label: 'Synced At' },
{ key: 'is_deleted', label: 'Is Deleted' },
];
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center gap-3">
<Link href="/admin/data-browser">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
</Link>
<Ticket className="w-6 h-6" />
<div>
<h1 className="text-2xl font-bold">Tickets Browser</h1>
<p className="text-sm text-muted-foreground">Browse and inspect ticket data</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Tickets</CardTitle>
<CardDescription>
{totalCount} total tickets in database
</CardDescription>
</CardHeader>
<CardContent>
<DataTable
columns={columns}
data={tickets}
totalCount={totalCount}
page={page}
pageSize={pageSize}
onPageChange={setPage}
onSort={(column, direction) => fetchTickets(page, undefined, column, direction)}
onSearch={(query) => fetchTickets(1, query)}
onRowClick={handleRowClick}
isLoading={isLoading}
exportable
exportFilename="tickets"
/>
</CardContent>
</Card>
<DetailModal
open={modalOpen}
onOpenChange={setModalOpen}
title={`Ticket #${selectedTicket?.ticket_number || selectedTicket?.id}`}
data={selectedTicket}
fields={detailFields}
/>
</div>
);
}