import { useState, useEffect } from 'react' import { AdminTable } from '../components/admin/AdminTable.tsx' import { TicketDetail } from '../components/tickets/TicketDetail.tsx' import { Modal } from '../components/ui/Modal.tsx' import { storage } from '../lib/storage.ts' import type { PaginatedResponse, TicketFilters } from '../lib/storage.ts' import { useModal } from '../hooks/useModal.ts' import type { Ticket, User } from '../lib/types.ts' import { Button } from '../components/ui/Button.tsx' function StatCard({ label, value }: { label: string; value: number }) { return (

{label}

{value}

) } const STATUS_OPTIONS: { value: Ticket['status'] | ''; label: string }[] = [ { value: '', label: 'All statuses' }, { value: 'open', label: 'Open' }, { value: 'in-progress', label: 'In progress' }, { value: 'resolved', label: 'Resolved' }, { value: 'closed', label: 'Closed' }, ] const TYPE_OPTIONS: { value: Ticket['type'] | ''; label: string }[] = [ { value: '', label: 'All types' }, { value: 'bug', label: 'Bug' }, { value: 'billing', label: 'Billing' }, { value: 'account', label: 'Account' }, { value: 'feature-request', label: 'Feature request' }, { value: 'feedback', label: 'Feedback' }, { value: 'other', label: 'Other' }, ] const selectClass = ` rounded-md border border-border-100 bg-bg-200 px-3 py-1.5 text-xs text-fg-100 outline-none transition-colors cursor-pointer appearance-none pr-7 hover:border-border-200 focus:border-border-200 focus:ring-1 focus:ring-ring-100 ` interface FilterBarProps { filters: TicketFilters isAuthenticated: boolean onChange: (f: TicketFilters) => void } function FilterBar({ filters, isAuthenticated, onChange }: FilterBarProps) { const hasActive = !!(filters.status || filters.type || filters.mine) return (
{/* Status */}
{/* Type */}
{/* Mine toggle — only visible when authenticated */} {isAuthenticated && ( )} {/* Clear */} {hasActive && ( )}
) } function ChevronIcon() { return ( ) } interface AdminPageProps { isAuthenticated: boolean user: User | null } const EMPTY_PAGE: PaginatedResponse = { data: [], total: 0, page: 1, pageSize: 20, totalPages: 1, } export function AdminPage({ isAuthenticated, user }: AdminPageProps) { const [result, setResult] = useState>(EMPTY_PAGE) const [page, setPage] = useState(1) const [filters, setFilters] = useState({}) const [selectedTicket, setSelectedTicket] = useState(null) const [selection, setSelection] = useState>(new Set()) const [batchDeleting, setBatchDeleting] = useState(false) const [actionError, setActionError] = useState(null) const detailModal = useModal() useEffect(() => { storage.getAllTickets(isAuthenticated, page, 20, filters).then(setResult) setSelection(new Set()) // clear selection whenever the visible page changes }, [isAuthenticated, page, filters]) const handleFilterChange = (next: TicketFilters) => { setFilters(next) setPage(1) } const stats = { total: result.total, open: result.data.filter(t => t.status === 'open').length, inProgress: result.data.filter(t => t.status === 'in-progress').length, resolved: result.data.filter(t => t.status === 'resolved').length, } const handleOpen = (ticket: Ticket) => { setSelectedTicket(ticket) detailModal.open() } const handleDetailClose = () => { detailModal.close() setSelectedTicket(null) setActionError(null) } const refetch = async () => { const fresh = await storage.getAllTickets(isAuthenticated, page, 20, filters) if (fresh.data.length === 0 && page > 1) { setPage(p => p - 1) } else { setResult(fresh) } } const handleCloseTicket = async (id: string) => { try { const updated = await storage.updateTicket(id, { status: 'closed' }) if (updated) { setResult(prev => ({ ...prev, data: prev.data.map(t => t.id === id ? updated : t) })) setSelectedTicket(updated) } } catch { setActionError('Failed to close ticket. Please try again.') } } const handleReopenTicket = async (id: string) => { try { const updated = await storage.updateTicket(id, { status: 'open' }) if (updated) { setResult(prev => ({ ...prev, data: prev.data.map(t => t.id === id ? updated : t) })) setSelectedTicket(updated) } } catch { setActionError('Failed to reopen ticket. Please try again.') } } const handleDeleteTicket = async (id: string) => { try { await storage.deleteTicket(id) handleDetailClose() await refetch() } catch { setActionError('Failed to delete ticket. Please try again.') } } const handleBatchDelete = async () => { if (selection.size === 0) return setBatchDeleting(true) try { await Promise.all([...selection].map(id => storage.deleteTicket(id))) setSelection(new Set()) await refetch() } finally { setBatchDeleting(false) } } // A ticket is owned by the current user if their IDs match. // Unauthenticated (guest) tickets have userId: null — those are always editable locally. const canModify = (ticket: Ticket) => !isAuthenticated || (user !== null && ticket.userId === user.id) return ( <>

Admin

{isAuthenticated ? 'All tickets across the system' : 'Your local tickets'}

{/* Batch action toolbar — visible only when tickets are selected */} {selection.size > 0 && (

{selection.size} {' '}ticket{selection.size !== 1 ? 's' : ''} selected

)} setPage(p => p - 1), onNext: () => setPage(p => p + 1), }} /> {selectedTicket && ( <> {actionError && (
⚠️

{actionError}

)} )}
) }