import { useState } from 'react'; import { Title, Table, Group, Button, Stack, Text, Modal, TextInput, NumberInput, Select, Textarea, Badge, ActionIcon, Loader, Center, Card, SimpleGrid, Progress, RingProgress, } from '@mantine/core'; import { DateInput } from '@mantine/dates'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import { IconPlus, IconEdit } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../../services/api'; import { useIsReadOnly } from '../../stores/authStore'; interface ReserveComponent { id: string; name: string; category: string; description: string; useful_life_years: number; remaining_life_years: number; replacement_cost: string; current_fund_balance: string; annual_contribution: string; last_replacement_date: string; next_replacement_date: string; condition_rating: number; } const categories = ['roof', 'pool', 'hvac', 'paving', 'painting', 'fencing', 'elevator', 'irrigation', 'clubhouse', 'other']; export function ReservesPage() { const [opened, { open, close }] = useDisclosure(false); const [editing, setEditing] = useState(null); const queryClient = useQueryClient(); const isReadOnly = useIsReadOnly(); const { data: components = [], isLoading } = useQuery({ queryKey: ['reserve-components'], queryFn: async () => { const { data } = await api.get('/reserve-components'); return data; }, }); const form = useForm({ initialValues: { name: '', category: 'other', description: '', useful_life_years: 20, remaining_life_years: 10, replacement_cost: 0, current_fund_balance: 0, annual_contribution: 0, condition_rating: 5, last_replacement_date: null as Date | null, next_replacement_date: null as Date | null, }, validate: { name: (v) => (v.length > 0 ? null : 'Required'), useful_life_years: (v) => (v > 0 ? null : 'Required'), replacement_cost: (v) => (v > 0 ? null : 'Required'), }, }); const saveMutation = useMutation({ mutationFn: (values: any) => { const payload = { ...values, last_replacement_date: values.last_replacement_date?.toISOString?.()?.split('T')[0] || null, next_replacement_date: values.next_replacement_date?.toISOString?.()?.split('T')[0] || null, }; return editing ? api.put(`/reserve-components/${editing.id}`, payload) : api.post('/reserve-components', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['reserve-components'] }); notifications.show({ message: editing ? 'Component updated' : 'Component created', color: 'green' }); close(); setEditing(null); form.reset(); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Error', color: 'red' }); }, }); const handleEdit = (c: ReserveComponent) => { setEditing(c); form.setValues({ name: c.name, category: c.category || 'other', description: c.description || '', useful_life_years: c.useful_life_years, remaining_life_years: c.remaining_life_years || 0, replacement_cost: parseFloat(c.replacement_cost || '0'), current_fund_balance: parseFloat(c.current_fund_balance || '0'), annual_contribution: parseFloat(c.annual_contribution || '0'), condition_rating: c.condition_rating || 5, last_replacement_date: c.last_replacement_date ? new Date(c.last_replacement_date) : null, next_replacement_date: c.next_replacement_date ? new Date(c.next_replacement_date) : null, }); open(); }; const fmt = (v: string | number) => parseFloat(String(v || '0')).toLocaleString('en-US', { style: 'currency', currency: 'USD' }); const totalCost = components.reduce((s, c) => s + parseFloat(c.replacement_cost || '0'), 0); const totalFunded = components.reduce((s, c) => s + parseFloat(c.current_fund_balance || '0'), 0); const pctFunded = totalCost > 0 ? (totalFunded / totalCost) * 100 : 0; if (isLoading) return
; return ( Reserve Components {!isReadOnly && } Total Replacement Cost {fmt(totalCost)} Total Funded {fmt(totalFunded)} Percent Funded = 70 ? 'green' : pctFunded >= 40 ? 'yellow' : 'red'}> {pctFunded.toFixed(1)}% = 70 ? 'green' : pctFunded >= 40 ? 'yellow' : 'red'} /> ComponentCategory Useful LifeRemaining Replacement CostFunded Condition {components.map((c) => { const funded = parseFloat(c.current_fund_balance || '0'); const cost = parseFloat(c.replacement_cost || '0'); const pct = cost > 0 ? (funded / cost) * 100 : 0; return ( {c.name} {c.category} {c.useful_life_years} yrs {c.remaining_life_years} yrs {fmt(c.replacement_cost)} = 70 ? 'green' : pct >= 40 ? 'yellow' : 'red'}>{fmt(c.current_fund_balance)} ({pct.toFixed(0)}%) = 7 ? 'green' : c.condition_rating >= 4 ? 'yellow' : 'red'} size="sm"> {c.condition_rating}/10 {!isReadOnly && handleEdit(c)}>} ); })} {components.length === 0 && No reserve components yet}
saveMutation.mutate(v))}>