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 (
)
}
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 */}
onChange({ ...filters, status: (e.target.value as Ticket['status']) || undefined })}
>
{STATUS_OPTIONS.map(o => {o.label} )}
{/* Type */}
onChange({ ...filters, type: (e.target.value as Ticket['type']) || undefined })}
>
{TYPE_OPTIONS.map(o => {o.label} )}
{/* Mine toggle — only visible when authenticated */}
{isAuthenticated && (
onChange({ ...filters, mine: !filters.mine })}
className={`
inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium
transition-colors cursor-pointer
${filters.mine
? 'border-fg-100/30 bg-fg-100/10 text-fg-100'
: 'border-border-100 bg-bg-200 text-fg-300 hover:border-border-200 hover:text-fg-200'
}
`}
>
My tickets
)}
{/* Clear */}
{hasActive && (
onChange({})}
className="text-xs text-fg-300 hover:text-fg-200 transition-colors cursor-pointer"
>
Clear filters
)}
)
}
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
setSelection(new Set())}
className="text-xs text-fg-300 hover:text-fg-200 transition-colors cursor-pointer"
>
Clear
{batchDeleting ? 'Deleting…' : `Delete ${selection.size}`}
)}
setPage(p => p - 1),
onNext: () => setPage(p => p + 1),
}}
/>
{selectedTicket && (
<>
{actionError && (
)}
>
)}
>
)
}