import { useState, useMemo, useRef, useEffect } from 'react'; import { Plus, MapPin, Phone, Mail, Tag, AlignLeft } from 'lucide-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import api, { getErrorMessage } from '@/lib/api'; import type { Location } from '@/types'; import { Button } from '@/components/ui/button'; import { EmptyState } from '@/components/ui/empty-state'; import { EntityTable, EntityDetailPanel, CategoryFilterBar, type ColumnDef, type PanelField, } from '@/components/shared'; import { useTableVisibility } from '@/hooks/useTableVisibility'; import { useCategoryOrder } from '@/hooks/useCategoryOrder'; import LocationForm from './LocationForm'; export default function LocationsPage() { const queryClient = useQueryClient(); const [selectedLocationId, setSelectedLocationId] = useState(null); const [showForm, setShowForm] = useState(false); const [editingLocation, setEditingLocation] = useState(null); const [activeFilters, setActiveFilters] = useState([]); const [showPinned, setShowPinned] = useState(true); const [search, setSearch] = useState(''); const [sortKey, setSortKey] = useState('name'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); const tableRef = useRef(null); const panelOpen = selectedLocationId !== null; const visibilityMode = useTableVisibility(tableRef as React.RefObject, panelOpen); const { data: locations = [], isLoading } = useQuery({ queryKey: ['locations'], queryFn: async () => { const { data } = await api.get('/locations'); return data; }, }); const deleteMutation = useMutation({ mutationFn: async () => { await api.delete(`/locations/${selectedLocationId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['locations'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] }); queryClient.invalidateQueries({ queryKey: ['upcoming'] }); toast.success('Location deleted'); setSelectedLocationId(null); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to delete location')); }, }); // Toggle frequent mutation const toggleFrequentMutation = useMutation({ mutationFn: async (loc: Location) => { await api.put(`/locations/${loc.id}`, { is_frequent: !loc.is_frequent }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['locations'] }); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to update frequent')); }, }); const allCategories = useMemo( () => Array.from(new Set(locations.map((l) => l.category).filter(Boolean))).sort(), [locations] ); const { orderedCategories, reorder: reorderCategories } = useCategoryOrder('locations', allCategories); const sortedLocations = useMemo(() => { return [...locations].sort((a, b) => { const aVal = String(a[sortKey as keyof Location] ?? ''); const bVal = String(b[sortKey as keyof Location] ?? ''); const cmp = aVal.localeCompare(bVal); return sortDir === 'asc' ? cmp : -cmp; }); }, [locations, sortKey, sortDir]); const frequentLocations = useMemo( () => sortedLocations.filter((l) => l.is_frequent), [sortedLocations] ); const filteredLocations = useMemo( () => sortedLocations.filter((l) => { if (showPinned && l.is_frequent) return false; if (activeFilters.length > 0 && !activeFilters.includes(l.category)) return false; if (search) { const q = search.toLowerCase(); if ( !l.name.toLowerCase().includes(q) && !(l.address?.toLowerCase().includes(q)) && !(l.category?.toLowerCase().includes(q)) ) return false; } return true; }), [sortedLocations, activeFilters, search, showPinned] ); // Build row groups for the table — ordered by custom category order const groups = useMemo(() => { if (activeFilters.length <= 1) { const label = activeFilters.length === 1 ? activeFilters[0] : 'All'; return [{ label, rows: filteredLocations }]; } return orderedCategories .filter((cat) => activeFilters.includes(cat)) .map((cat) => ({ label: cat, rows: filteredLocations.filter((l) => l.category === cat), })) .filter((g) => g.rows.length > 0); }, [activeFilters, filteredLocations, orderedCategories]); const selectedLocation = useMemo( () => locations.find((l) => l.id === selectedLocationId) ?? null, [locations, selectedLocationId] ); const handleSort = (key: string) => { if (sortKey === key) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); } else { setSortKey(key); setSortDir('asc'); } }; const handleRowClick = (id: number) => { setSelectedLocationId((prev) => (prev === id ? null : id)); }; const handleEdit = () => { if (!selectedLocation) return; setEditingLocation(selectedLocation); setShowForm(true); }; // Escape key closes detail panel useEffect(() => { if (!panelOpen) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setSelectedLocationId(null); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [panelOpen]); const handleCloseForm = () => { setShowForm(false); setEditingLocation(null); }; const handleToggleCategory = (cat: string) => { setActiveFilters((prev) => prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] ); }; const selectAllCategories = () => { const allSelected = orderedCategories.every((c) => activeFilters.includes(c)); setActiveFilters(allSelected ? [] : [...orderedCategories]); }; const columns: ColumnDef[] = [ { key: 'name', label: 'Location', sortable: true, visibilityLevel: 'essential', render: (l) => (
{l.name}
), }, { key: 'address', label: 'Address', sortable: true, visibilityLevel: 'essential', render: (l) => {l.address}, }, { key: 'contact_number', label: 'Contact', sortable: false, visibilityLevel: 'filtered', render: (l) => ( {l.contact_number || '—'} ), }, { key: 'email', label: 'Email', sortable: true, visibilityLevel: 'filtered', render: (l) => ( {l.email || '—'} ), }, { key: 'category', label: 'Category', sortable: true, visibilityLevel: 'all', render: (l) => l.category && l.category.toLowerCase() !== 'other' ? ( {l.category} ) : ( ), }, ]; const panelFields: PanelField[] = [ { label: 'Contact Number', key: 'contact_number', copyable: true, icon: Phone }, { label: 'Email', key: 'email', copyable: true, icon: Mail }, { label: 'Category', key: 'category', icon: Tag }, { label: 'Address', key: 'address', copyable: true, icon: MapPin, fullWidth: true }, { label: 'Notes', key: 'notes', multiline: true, icon: AlignLeft, fullWidth: true }, ]; const renderPanel = () => ( item={selectedLocation} fields={panelFields} onEdit={handleEdit} onDelete={() => deleteMutation.mutate()} deleteLoading={deleteMutation.isPending} onClose={() => setSelectedLocationId(null)} renderHeader={(l) => (

{l.name}

{l.category}
)} getUpdatedAt={(l) => l.updated_at} getValue={(l, key) => { const val = l[key as keyof Location]; if (val == null || val === '') return undefined; if (typeof val === 'boolean') return undefined; return String(val); }} isFavourite={selectedLocation?.is_frequent} onToggleFavourite={() => selectedLocation && toggleFrequentMutation.mutate(selectedLocation)} favouriteLabel="frequent" /> ); return (
{/* Header */}

Locations

setActiveFilters([])} onTogglePinned={() => setShowPinned((v) => !v)} onToggleCategory={handleToggleCategory} onSelectAllCategories={selectAllCategories} onReorderCategories={reorderCategories} searchValue={search} onSearchChange={setSearch} />
{/* Body */}
{/* Table */}
{isLoading ? ( columns={columns} groups={[]} pinnedRows={[]} pinnedLabel="Frequent" showPinned={false} selectedId={null} onRowClick={() => {}} sortKey={sortKey} sortDir={sortDir} onSort={handleSort} visibilityMode={visibilityMode} loading={true} /> ) : locations.length === 0 ? ( setShowForm(true)} /> ) : ( columns={columns} groups={groups} pinnedRows={frequentLocations} pinnedLabel="Frequent" showPinned={showPinned} selectedId={selectedLocationId} onRowClick={handleRowClick} sortKey={sortKey} sortDir={sortDir} onSort={handleSort} visibilityMode={visibilityMode} /> )}
{/* Detail panel (desktop) */}
{renderPanel()}
{/* Mobile detail panel overlay */} {panelOpen && selectedLocation && (
setSelectedLocationId(null)} >
e.stopPropagation()} > {renderPanel()}
)} {showForm && ( )}
); }