import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useMemo, useState } from 'react' import { m, mKey } from '@/i18n/messages' import { useLocale, getLocale } from '@/i18n/config' import { asI18n } from '@pikku/react' import { keepPreviousData, useQueryClient } from '@tanstack/react-query' import { useDebouncedValue } from '@mantine/hooks' import { Alert, Anchor, Badge, Box, Button, Container, Group, Loader, Modal, MultiSelect, Paper, SegmentedControl, Select, SimpleGrid, Stack, Table, Text, TextInput, } from '@pikku/mantine/core' import { Check, Clock, X } from 'lucide-react' import { usePikkuMutation, usePikkuQuery } from '@project/functions-sdk/pikku/api.gen' import { usePageTitle } from '../components/AppLayout' import { RequireAuth } from '../auth' import { fmtDate, fmtDateShort } from '../lib/dates' const STATUS_KEYS = ['pending', 'approved', 'declined'] as const type EnquiryStatus = (typeof STATUS_KEYS)[number] type EnquiryDateOption = { optionId: string startDate: string endDate: string priority: number } type Enquiry = { enquiryId: string contactName: string | null contactEmail: string contactPhone: string | null website: string | null eventName: string eventOutline: string | null description: string | null dateOptions: EnquiryDateOption[] expectedPersons: number | null halfHouse: boolean notes: string | null status: string waitlistedAt: Date | null approvedAt: Date | null declinedAt: Date | null createdAt: Date bookingId: string | null } // The SDK's date reviver (transform-date.js) turns any 'YYYY-MM-DD…' response // string into a Date — including these option dates, despite their string type. // Normalize back to a plain 'YYYY-MM-DD' string so the date inputs and equality // checks below work, and parse at local midnight for display. const toYmd = (v: string | Date): string => v instanceof Date ? v.toISOString().slice(0, 10) : v const optDate = (s: string) => new Date(`${s}T00:00:00`) function StatusBadge({ enquiry }: { enquiry: Enquiry }) { useLocale() const color = enquiry.status === 'approved' ? 'green' : enquiry.status === 'declined' ? 'red' : 'blue' return ( {mKey(`common.pages.admin.enquiries.status.${enquiry.status}` as any)} {enquiry.status === 'pending' && enquiry.waitlistedAt && ( {m.common_pages_admin_enquiries_status_waitlisted()} )} ) } function AdminEnquiriesInner() { useLocale() const queryClient = useQueryClient() const navigate = useNavigate() usePageTitle(m.common_pages_admin_enquiries_title()) const [statusFilter, setStatusFilter] = useState(['pending']) const queryArgs = useMemo( () => ({ venueSlug: 'drawehn', status: statusFilter.length ? statusFilter : undefined, }), [statusFilter], ) const { data, isLoading, isFetching, error } = usePikkuQuery('getEnquiries', queryArgs, { placeholderData: keepPreviousData, }) const invalidate = () => queryClient.invalidateQueries({ queryKey: ['getEnquiries'] }) // Which enquiry has an open transition modal, and which kind. const [modal, setModal] = useState<{ kind: 'approve' | 'waitlist' | 'decline'; enquiry: Enquiry } | null>(null) const statusOptions = useMemo( () => STATUS_KEYS.map((s) => ({ value: s, label: mKey(`common.pages.admin.enquiries.status.${s}` as any), })), [getLocale()], ) if (isLoading && !data) { return {m.common_states_loading()} } if (error && !data) { return {m.common_states_error()} } if (!data) return null const enquiries = (data.enquiries as Enquiry[]).map((e) => ({ ...e, dateOptions: e.dateOptions.map((o) => ({ ...o, startDate: toYmd(o.startDate), endDate: toYmd(o.endDate), })), })) return ( {m.common_pages_admin_enquiries_stats_showing({ count: enquiries.length })} {isFetching && } {enquiries.length === 0 ? ( {m.common_pages_admin_enquiries_empty()} ) : ( {m.common_pages_admin_enquiries_cols_received()} {m.common_pages_admin_enquiries_cols_event()} {m.common_pages_admin_enquiries_cols_contact()} {m.common_pages_admin_enquiries_cols_dates()} {m.common_pages_admin_enquiries_cols_persons()} {m.common_pages_admin_enquiries_cols_status()} {m.common_pages_admin_enquiries_cols_actions()} {enquiries.map((e) => ( {asI18n(`${fmtDateShort(e.createdAt, getLocale())}`)} {asI18n(`${e.eventName}`)} {e.eventOutline && ( {asI18n(`${e.eventOutline}`)} )} {asI18n(`${e.contactName ?? '—'}`)} {asI18n(`${e.contactEmail}`)} {e.dateOptions.length === 0 ? ( {m.common_pages_admin_enquiries_no_dates()} ) : ( {e.dateOptions.map((o, i) => ( {asI18n(`${fmtDateShort(optDate(o.startDate), getLocale())} → ${fmtDateShort(optDate(o.endDate), getLocale())}`)} ))} )} {e.expectedPersons ?? '—'} {e.status === 'pending' ? ( ) : e.status === 'approved' && e.bookingId ? ( navigate({ to: '/admin/bookings/$bookingId', params: { bookingId: e.bookingId! } }) } > {m.common_pages_admin_enquiries_actions_view_booking()} ) : ( {asI18n('—')} )} ))}
)}
{modal?.kind === 'approve' && ( setModal(null)} onDone={() => { setModal(null); invalidate() }} /> )} {modal?.kind === 'waitlist' && ( setModal(null)} onDone={() => { setModal(null); invalidate() }} /> )} {modal?.kind === 'decline' && ( setModal(null)} onDone={() => { setModal(null); invalidate() }} /> )}
) } // ─── Approve modal ────────────────────────────────────────────────────────── function ApproveModal({ enquiry, onClose, onDone }: { enquiry: Enquiry; onClose: () => void; onDone: () => void }) { useLocale() const [mode, setMode] = useState<'existing' | 'new'>('existing') // Prefill from the visitor's preferred (first) date option; the admin can // pick a different option or override the inputs entirely. const topOption = enquiry.dateOptions[0] const [startDate, setStartDate] = useState(topOption?.startDate ?? '') const [endDate, setEndDate] = useState(topOption?.endDate ?? '') // Existing-client search (server-side via getClients). const [clientId, setClientId] = useState(null) const [search, setSearch] = useState('') const [debouncedSearch] = useDebouncedValue(search, 250) const { data: clientData, isFetching: clientsFetching } = usePikkuQuery( 'getClients', { venueSlug: 'drawehn', q: debouncedSearch.trim() || undefined }, { placeholderData: keepPreviousData, enabled: mode === 'existing' }, ) const clientOptions = useMemo( () => (clientData?.clients ?? []).map((c) => ({ value: c.clientId, label: c.name ? `${c.name}${c.contactEmail ? ` · ${c.contactEmail}` : ''}` : (c.contactEmail ?? c.clientId), })), [clientData], ) // New-client fields, pre-filled from the enquiry contact. const [name, setName] = useState(enquiry.contactName ?? '') const [email, setEmail] = useState(enquiry.contactEmail) const [phone, setPhone] = useState(enquiry.contactPhone ?? '') const [website, setWebsite] = useState(enquiry.website ?? '') const [billingAddress, setBillingAddress] = useState('') const mutation = usePikkuMutation('approveEnquiry', { onSuccess: onDone }) const datesValid = !!startDate && !!endDate && startDate <= endDate const clientValid = mode === 'existing' ? !!clientId : !!email.trim() const canSubmit = datesValid && clientValid const submit = () => { mutation.mutate({ enquiryId: enquiry.enquiryId, startDate, endDate, client: mode === 'existing' ? { clientId: clientId! } : { name: name.trim() || undefined, contactEmail: email.trim() || undefined, contactPhone: phone.trim() || undefined, website: website.trim() || undefined, billingAddress: billingAddress.trim() || undefined, }, }) } return ( {asI18n(`${enquiry.eventName}`)} {enquiry.dateOptions.length > 0 && ( {m.common_pages_admin_enquiries_approve_modal_date_options()} {enquiry.dateOptions.map((o, i) => { const selected = o.startDate === startDate && o.endDate === endDate return ( { setStartDate(o.startDate) setEndDate(o.endDate) }} > {asI18n(`${i + 1}. ${fmtDateShort(optDate(o.startDate), getLocale())} → ${fmtDateShort(optDate(o.endDate), getLocale())}`)} ) })} )} setStartDate(e.currentTarget.value)} /> setEndDate(e.currentTarget.value)} error={startDate && endDate && startDate > endDate ? m.common_pages_admin_enquiries_approve_modal_date_error() : undefined} /> setMode(v as 'existing' | 'new')} data={[ { value: 'existing', label: m.common_pages_admin_enquiries_approve_modal_existing_client() }, { value: 'new', label: m.common_pages_admin_enquiries_approve_modal_new_client() }, ]} /> {mode === 'existing' ? (