import { useState, useRef } from 'react'; import { Title, Table, Group, Button, Stack, TextInput, Modal, Select, Badge, ActionIcon, Text, Loader, Center, Tooltip, Alert, } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import { IconPlus, IconEdit, IconSearch, IconTrash, IconInfoCircle, IconUpload, IconDownload } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../../services/api'; import { parseCSV, downloadBlob } from '../../utils/csv'; interface Unit { id: string; unit_number: string; address_line1: string; owner_name: string; owner_email: string; monthly_assessment: string; status: string; balance_due?: string; assessment_group_id?: string; assessment_group_name?: string; group_regular_assessment?: string; group_special_assessment?: string; group_frequency?: string; } interface AssessmentGroup { id: string; name: string; regular_assessment: string; frequency: string; is_default: boolean; } export function UnitsPage() { const [opened, { open, close }] = useDisclosure(false); const [editing, setEditing] = useState(null); const [search, setSearch] = useState(''); const [deleteConfirm, setDeleteConfirm] = useState(null); const queryClient = useQueryClient(); const fileInputRef = useRef(null); const { data: units = [], isLoading } = useQuery({ queryKey: ['units'], queryFn: async () => { const { data } = await api.get('/units'); return data; }, }); const { data: assessmentGroups = [] } = useQuery({ queryKey: ['assessment-groups'], queryFn: async () => { const { data } = await api.get('/assessment-groups'); return data; }, }); const defaultGroup = assessmentGroups.find(g => g.is_default); const hasGroups = assessmentGroups.length > 0; const form = useForm({ initialValues: { unit_number: '', address_line1: '', city: '', state: '', zip_code: '', owner_name: '', owner_email: '', owner_phone: '', assessment_group_id: '' as string | null, }, validate: { unit_number: (v) => (v.length > 0 ? null : 'Required'), assessment_group_id: (v) => (v && v.length > 0 ? null : 'Assessment group is required'), }, }); const saveMutation = useMutation({ mutationFn: (values: any) => { const payload = { ...values, assessment_group_id: values.assessment_group_id || null }; return editing ? api.put(`/units/${editing.id}`, payload) : api.post('/units', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['units'] }); notifications.show({ message: editing ? 'Unit updated' : 'Unit created', color: 'green' }); close(); setEditing(null); form.reset(); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Error', color: 'red' }); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete(`/units/${id}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['units'] }); notifications.show({ message: 'Unit deleted', color: 'green' }); setDeleteConfirm(null); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Cannot delete unit', color: 'red' }); setDeleteConfirm(null); }, }); const importMutation = useMutation({ mutationFn: async (rows: Record[]) => { const { data } = await api.post('/units/import', rows); return data; }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['units'] }); let msg = `Imported: ${data.created} created, ${data.updated} updated`; if (data.errors?.length) msg += `. ${data.errors.length} error(s): ${data.errors.slice(0, 3).join('; ')}`; notifications.show({ message: msg, color: data.errors?.length ? 'yellow' : 'green', autoClose: 10000 }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Import failed', color: 'red' }); }, }); const handleEdit = (u: Unit) => { setEditing(u); form.setValues({ unit_number: u.unit_number, address_line1: u.address_line1 || '', city: '', state: '', zip_code: '', owner_name: u.owner_name || '', owner_email: u.owner_email || '', owner_phone: '', assessment_group_id: u.assessment_group_id || '', }); open(); }; const handleNew = () => { setEditing(null); form.reset(); if (defaultGroup) form.setFieldValue('assessment_group_id', defaultGroup.id); open(); }; const handleExport = async () => { try { const response = await api.get('/units/export', { responseType: 'blob' }); downloadBlob(response.data, 'units.csv'); } catch { notifications.show({ message: 'Export failed', color: 'red' }); } }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const text = e.target?.result as string; if (!text) { notifications.show({ message: 'Could not read file', color: 'red' }); return; } const rows = parseCSV(text); if (!rows.length) { notifications.show({ message: 'No data rows found', color: 'red' }); return; } importMutation.mutate(rows); }; reader.readAsText(file); event.target.value = ''; }; const filtered = units.filter((u) => !search || u.unit_number.toLowerCase().includes(search.toLowerCase()) || (u.owner_name || '').toLowerCase().includes(search.toLowerCase()) ); if (isLoading) return
; return ( Units / Homeowners {hasGroups ? ( ) : ( )} {!hasGroups && ( } color="yellow" variant="light"> You must create at least one assessment group before adding units. Go to Assessment Groups to create one. )} } value={search} onChange={(e) => setSearch(e.currentTarget.value)} /> Unit # Address Owner Email Group Assessment Special Assessment Status {filtered.map((u) => ( {u.unit_number} {u.address_line1} {u.owner_name} {u.owner_email} {u.assessment_group_name ? ( {u.assessment_group_name} ) : ( - )} ${parseFloat(u.group_regular_assessment || u.monthly_assessment || '0').toFixed(2)} {parseFloat(u.group_special_assessment || '0') > 0 ? `$${parseFloat(u.group_special_assessment || '0').toFixed(2)}` : - } {u.status} handleEdit(u)}> setDeleteConfirm(u)}> ))} {filtered.length === 0 && No units yet}
{/* Create/Edit Modal */}
saveMutation.mutate(v))}>