feat: ticket system base implementation
This commit is contained in:
60
src/app/(modules)/ticket/components/TicketColumns.tsx
Normal file
60
src/app/(modules)/ticket/components/TicketColumns.tsx
Normal 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),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
60
src/app/(modules)/ticket/components/TicketFilters.tsx
Normal file
60
src/app/(modules)/ticket/components/TicketFilters.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/app/(modules)/ticket/components/TicketStatusBadge.tsx
Normal file
28
src/app/(modules)/ticket/components/TicketStatusBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
69
src/app/(modules)/ticket/components/TicketTable.tsx
Normal file
69
src/app/(modules)/ticket/components/TicketTable.tsx
Normal 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
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user