'use client'; import { Suspense } 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, AppLoading, StatusChip } from '@/components'; import type { StatusKind } from '@/components'; import { AdminDataTable, AdminEmptyState, AdminErrorState, AdminPageHeader, AdminPager, type AdminTableColumn, } from '@/components/admin'; import { adminTicketThreadPath } from '@/constants'; import { useAdminListState } from '@/hooks'; import { useAdminTickets } from '@/services/tickets'; import { TICKETS_PAGE_SIZE } from '@/services/tickets/constants'; import type { AdminTicketFilters, AdminTicketSummary, TicketCategory, TicketStatus } from '@/services/tickets/types'; import { formatShamsiDate } from '@/utils'; 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 = {}; function parseFilters(params: URLSearchParams): AdminTicketFilters { return { status: (params.get('status') as TicketStatus | null) ?? undefined, category: (params.get('category') as TicketCategory | null) ?? undefined, referenceCode: params.get('referenceCode') ?? undefined, }; } function serializeFilters(filters: AdminTicketFilters): Record { const record: Record = {}; if (filters.status) record.status = filters.status; if (filters.category) record.category = filters.category; if (filters.referenceCode) record.referenceCode = filters.referenceCode; return record; } /** * 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 mirrored into the URL (`useAdminListState`, ui-phase-11) so * browser back/refresh/a pasted link all reproduce the exact same queue view. This surface is staff-only — * the server enforces the scope; the UI just routes here. */ export default function AdminTicketsPage() { return ( }> ); } function AdminTicketsQueue() { const t = useTranslations('admin'); const locale = useLocale(); const router = useRouter(); const { draft, setDraft, applied, page, apply, clear, goToPage } = useAdminListState({ parse: parseFilters, serialize: serializeFilters, empty: EMPTY, }); const tickets = useAdminTickets(applied, page); const items = tickets.data?.items ?? []; const total = tickets.data?.total ?? 0; const pageCount = Math.max(1, Math.ceil(total / TICKETS_PAGE_SIZE)); const from = items.length === 0 ? 0 : (page - 1) * TICKETS_PAGE_SIZE + 1; const to = items.length === 0 ? 0 : from + items.length - 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 ?? '—' }, { key: 'activity', header: t('ticket_activity_col'), minWidth: 140, render: (row) => formatShamsiDate(row.createdAt, locale), }, ]; 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)}`)} footer={t('showing_range', { from, to, total })} /> )} goToPage(Math.max(1, page - 1))} onNext={() => goToPage(Math.min(pageCount, page + 1))} prevLabel={t('prev_page')} nextLabel={t('next_page')} indicator={t('page_indicator', { page, total: pageCount })} /> ); }