'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import { Box, Chip, MenuItem, Skeleton, Stack, TextField } from '@mui/material'; import { AppButton, StatusChip } from '@/components'; import type { StatusKind } from '@/components'; import { AdminDataTable, AdminEmptyState, AdminErrorState, AdminPageHeader, AdminPager, type AdminTableColumn, } from '@/components/admin'; import { adminTicketThreadPath } from '@/constants'; import { useAdminTickets } from '@/services/tickets'; import { TICKETS_PAGE_SIZE } from '@/services/tickets/constants'; import type { AdminTicketFilters, AdminTicketSummary, TicketCategory, TicketStatus } from '@/services/tickets/types'; const STATUSES: readonly TicketStatus[] = ['open', 'closed']; const CATEGORIES: readonly TicketCategory[] = ['coordination', 'support', 'refund', 'emergency']; /** Queue status → chip color: an open ticket is pending work, a closed one is neutral (phase §5). */ const STATUS_KIND: Record = { open: 'pending', closed: 'neutral' }; const EMPTY: AdminTicketFilters = {}; /** * The admin global ticket queue (f15) — EVERY ticket across the platform (not one viewer's), the entry point * into a case. Filter by status/category/reference; a row opens the admin thread where internal notes and the * refund panel live. The filter **draft** commits to the query only on Apply, so typing a reference never * refetches; the applied filters + page are the cache key (`useAdminTickets`), so revisiting a filter/page * serves from cache. This surface is staff-only — the server enforces the scope; the UI just routes here. */ export default function AdminTicketsPage() { const t = useTranslations('admin'); const locale = useLocale(); const router = useRouter(); const [draft, setDraft] = useState(EMPTY); const [applied, setApplied] = useState(EMPTY); const [page, setPage] = useState(1); const tickets = useAdminTickets(applied, page); const items = tickets.data?.items ?? []; const pageCount = Math.max(1, Math.ceil((tickets.data?.total ?? 0) / TICKETS_PAGE_SIZE)); const apply = () => { setApplied(draft); setPage(1); }; const clear = () => { setDraft(EMPTY); setApplied(EMPTY); setPage(1); }; const columns: AdminTableColumn[] = [ { key: 'ref', header: t('ticket_col_ref'), render: (row) => ( {row.referenceCode} ), }, { key: 'subject', header: t('ticket_col_subject'), render: (row) => row.subject ?? '—' }, { key: 'category', header: t('ticket_col_category'), render: (row) => , }, { key: 'status', header: t('ticket_col_status'), render: (row) => , }, { key: 'booking', header: t('ticket_col_booking'), render: (row) => row.bookingId ?? '—' }, ]; return ( setDraft((d) => ({ ...d, status: (e.target.value || undefined) as TicketStatus | undefined }))} sx={{ minWidth: 140 }} > {t('filter_all')} {STATUSES.map((s) => ( {t(`tstatus_${s}`)} ))} setDraft((d) => ({ ...d, category: (e.target.value || undefined) as TicketCategory | undefined }))} sx={{ minWidth: 160 }} > {t('filter_all')} {CATEGORIES.map((c) => ( {t(`tcat_${c}`)} ))} setDraft((d) => ({ ...d, referenceCode: e.target.value || undefined }))} sx={{ minWidth: 220 }} /> {t('apply')} {t('clear')} {tickets.isLoading ? ( {[0, 1, 2, 3].map((k) => ( ))} ) : tickets.isError ? ( tickets.refetch()} /> ) : items.length === 0 ? ( ) : ( row.id} ariaLabel={t('ticket_title')} onRowClick={(row) => router.push(`/${locale}${adminTicketThreadPath(row.id)}`)} /> )} setPage((p) => Math.max(1, p - 1))} onNext={() => setPage((p) => Math.min(pageCount, p + 1))} prevLabel={t('prev_page')} nextLabel={t('next_page')} indicator={t('page_indicator', { page })} /> ); }