feat: ticket system base implementation

This commit is contained in:
2026-06-19 01:19:01 +05:30
parent a18fff4d6f
commit 8c3d19a429
19 changed files with 1400 additions and 27 deletions

View File

@@ -0,0 +1,60 @@
'use client';
import { useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import type { TicketListItem } from '@/types';
import { TicketStatusBadge } from './TicketStatusBadge';
function formatDate(value: string | null | undefined) {
if (!value) return '-';
return new Intl.DateTimeFormat('en-IN', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(value));
}
export function useTicketColumns(): ColumnDef<TicketListItem>[] {
return useMemo(
() => [
{
accessorKey: 'id',
header: 'Ticket',
size: 180,
cell: ({ row }) => (
<span className="font-medium">{row.original.id}</span>
),
},
{
accessorKey: 'status',
header: 'Status',
size: 150,
cell: ({ row }) => <TicketStatusBadge status={row.original.status} />,
},
{
accessorKey: 'detection_count',
header: 'Detections',
size: 120,
},
{
accessorKey: 'assigned_to_email',
header: 'Assigned To',
size: 220,
cell: ({ row }) => row.original.assigned_to_email || '-',
},
{
accessorKey: 'created_by_email',
header: 'Uploaded By',
size: 220,
},
{
accessorKey: 'updated_at',
header: 'Updated',
size: 180,
cell: ({ row }) => formatDate(row.original.updated_at),
},
],
[],
);
}

View File

@@ -0,0 +1,60 @@
'use client';
import { SegmentSelect } from '@/components/lookups/SegmentSelect';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { TicketStatus } from '@/types';
import { formatTicketStatus } from './TicketStatusBadge';
const ticketStatuses: TicketStatus[] = [
'processing',
'unassigned',
'assigned',
'under_review',
'closed',
];
interface TicketFiltersProps {
status: TicketStatus | 'all';
segmentId: string;
onStatusChange: (status: TicketStatus | 'all') => void;
onSegmentChange: (segmentId: string) => void;
}
export function TicketFilters({
status,
segmentId,
onStatusChange,
onSegmentChange,
}: TicketFiltersProps) {
return (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select
value={status}
onValueChange={(value) => onStatusChange(value as TicketStatus | 'all')}
>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All statuses</SelectItem>
{ticketStatuses.map((item) => (
<SelectItem key={item} value={item}>
{formatTicketStatus(item)}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="w-full sm:w-64">
<SegmentSelect value={segmentId} onValueChange={onSegmentChange} />
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import type { TicketStatus } from '@/types';
const statusClasses: Record<TicketStatus, string> = {
processing: 'bg-sky-100 text-sky-700 border-sky-200',
unassigned: 'bg-amber-100 text-amber-800 border-amber-200',
assigned: 'bg-indigo-100 text-indigo-700 border-indigo-200',
under_review: 'bg-violet-100 text-violet-700 border-violet-200',
closed: 'bg-stone-100 text-stone-700 border-stone-200',
};
export function formatTicketStatus(status: TicketStatus) {
return status
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
export function TicketStatusBadge({ status }: { status: TicketStatus }) {
return (
<Badge variant="outline" className={cn('border', statusClasses[status])}>
{formatTicketStatus(status)}
</Badge>
);
}

View File

@@ -0,0 +1,69 @@
'use client';
import type { ReactNode } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { BarChart3, Eye, ChartPie } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import type { TicketListItem } from '@/types';
interface TicketTableProps {
columns: ColumnDef<TicketListItem>[];
tickets: TicketListItem[];
isLoading: boolean;
toolbar: ReactNode;
skip: number;
limit: number;
total: number;
onPageChange: (skip: number) => void;
onLimitChange: (limit: number) => void;
onView: (ticket: TicketListItem) => void;
onShowAnalytics: (ticket: TicketListItem) => void;
}
export function TicketTable({
columns,
tickets,
isLoading,
toolbar,
skip,
limit,
total,
onPageChange,
onLimitChange,
onView,
onShowAnalytics,
}: TicketTableProps) {
return (
<DataTable
title="Tickets"
columns={columns}
data={tickets}
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No tickets found."
emptyDescription="Ticket rows will appear after video processing creates them."
actions={[
{
label: 'View',
icon: <Eye className="size-4" />,
onClick: onView,
},
{
label: 'Show Analytics',
icon: <ChartPie className="size-4" />,
onClick: onShowAnalytics,
},
]}
pagination={{
skip,
limit,
totalItems: total,
onPageChange,
onLimitChange,
}}
onRowClick={onView}
actionsSticky
/>
);
}