diff --git a/client/src/components/property/EditLeaseDialog.tsx b/client/src/components/property/EditLeaseDialog.tsx new file mode 100644 index 0000000..f25f632 --- /dev/null +++ b/client/src/components/property/EditLeaseDialog.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState } from 'react'; +import { Pencil } from 'lucide-react'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, SelectTrigger, SelectValue, SelectContent, SelectItem, +} from '@/components/ui/select'; +import { useToast } from '@/hooks/use-toast'; +import { useUpdateLease, type Lease } from '@/hooks/use-property'; + +interface EditLeaseDialogProps { + propertyId: string; + lease: Lease; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function EditLeaseDialog({ propertyId, lease, open, onOpenChange }: EditLeaseDialogProps) { + const [form, setForm] = useState({ + tenantName: '', + tenantEmail: '', + tenantPhone: '', + startDate: '', + endDate: '', + monthlyRent: '', + securityDeposit: '', + status: 'active', + }); + const { toast } = useToast(); + const updateLease = useUpdateLease(propertyId, lease.id); + + useEffect(() => { + if (open) { + setForm({ + tenantName: lease.tenantName || '', + tenantEmail: lease.tenantEmail || '', + tenantPhone: lease.tenantPhone || '', + startDate: lease.startDate?.slice(0, 10) || '', + endDate: lease.endDate?.slice(0, 10) || '', + monthlyRent: lease.monthlyRent || '', + securityDeposit: lease.securityDeposit || '', + status: lease.status || 'active', + }); + } + }, [open, lease]); + + function handleChange(field: keyof typeof form, value: string) { + setForm(prev => ({ ...prev, [field]: value })); + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + const payload: Record = { + tenantName: form.tenantName.trim(), + startDate: form.startDate, + endDate: form.endDate, + monthlyRent: form.monthlyRent, + status: form.status, + }; + if (form.tenantEmail.trim()) payload.tenantEmail = form.tenantEmail.trim(); + if (form.tenantPhone.trim()) payload.tenantPhone = form.tenantPhone.trim(); + if (form.securityDeposit) payload.securityDeposit = form.securityDeposit; + + updateLease.mutate(payload, { + onSuccess: () => { + toast({ title: 'Lease updated', description: `Lease for ${payload.tenantName} has been saved.` }); + onOpenChange(false); + }, + onError: (error: Error) => { + toast({ title: 'Failed to update lease', description: error.message, variant: 'destructive' }); + }, + }); + } + + const isValid = + form.tenantName.trim() !== '' && + form.startDate !== '' && + form.endDate !== '' && + form.monthlyRent !== ''; + + return ( + + + + + + Edit Lease + + Update lease terms and tenant information. + + +
+
+ + handleChange('tenantName', e.target.value)} required /> +
+ +
+
+ + handleChange('tenantEmail', e.target.value)} /> +
+
+ + handleChange('tenantPhone', e.target.value)} /> +
+
+ +
+
+ + handleChange('startDate', e.target.value)} required /> +
+
+ + handleChange('endDate', e.target.value)} required /> +
+
+ +
+
+ + handleChange('monthlyRent', e.target.value)} required /> +
+
+ + handleChange('securityDeposit', e.target.value)} /> +
+
+ +
+ + +
+ + + + + +
+
+
+ ); +} diff --git a/client/src/components/property/EditUnitDialog.tsx b/client/src/components/property/EditUnitDialog.tsx new file mode 100644 index 0000000..4c69c98 --- /dev/null +++ b/client/src/components/property/EditUnitDialog.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react'; +import { Pencil } from 'lucide-react'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/hooks/use-toast'; +import { useUpdateUnit, type Unit } from '@/hooks/use-property'; + +interface EditUnitDialogProps { + propertyId: string; + unit: Unit; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function EditUnitDialog({ propertyId, unit, open, onOpenChange }: EditUnitDialogProps) { + const [form, setForm] = useState({ + unitNumber: '', + bedrooms: '', + bathrooms: '', + squareFeet: '', + monthlyRent: '', + }); + const { toast } = useToast(); + const updateUnit = useUpdateUnit(propertyId, unit.id); + + useEffect(() => { + if (open) { + setForm({ + unitNumber: unit.unitNumber, + bedrooms: String(unit.bedrooms ?? ''), + bathrooms: String(unit.bathrooms ?? ''), + squareFeet: String(unit.squareFeet ?? ''), + monthlyRent: unit.monthlyRent || '', + }); + } + }, [open, unit]); + + function handleChange(field: keyof typeof form, value: string) { + setForm(prev => ({ ...prev, [field]: value })); + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + const payload: Record = { + unitNumber: form.unitNumber.trim(), + }; + if (form.bedrooms) payload.bedrooms = parseInt(form.bedrooms, 10); + if (form.bathrooms) payload.bathrooms = form.bathrooms; + if (form.squareFeet) payload.squareFeet = parseInt(form.squareFeet, 10); + if (form.monthlyRent) payload.monthlyRent = form.monthlyRent; + + updateUnit.mutate(payload, { + onSuccess: () => { + toast({ title: 'Unit updated', description: `Unit ${payload.unitNumber} has been saved.` }); + onOpenChange(false); + }, + onError: (error: Error) => { + toast({ title: 'Failed to update unit', description: error.message, variant: 'destructive' }); + }, + }); + } + + const isValid = form.unitNumber.trim() !== ''; + + return ( + + + + + + Edit Unit + + Update unit details. + + +
+
+ + handleChange('unitNumber', e.target.value)} required /> +
+ +
+
+ + handleChange('bedrooms', e.target.value)} /> +
+
+ + handleChange('bathrooms', e.target.value)} /> +
+
+ +
+
+ + handleChange('squareFeet', e.target.value)} /> +
+
+ + handleChange('monthlyRent', e.target.value)} /> +
+
+ + + + + +
+
+
+ ); +} diff --git a/client/src/hooks/use-property.ts b/client/src/hooks/use-property.ts index 68e164f..f909f73 100644 --- a/client/src/hooks/use-property.ts +++ b/client/src/hooks/use-property.ts @@ -243,6 +243,32 @@ export function useCreateLease(propertyId: string) { }); } +export function useUpdateUnit(propertyId: string, unitId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: Partial) => + apiRequest('PATCH', `/api/properties/${propertyId}/units/${unitId}`, data).then(r => r.json()), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/units`] }); + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/financials`] }); + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/rent-roll`] }); + }, + }); +} + +export function useUpdateLease(propertyId: string, leaseId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: Partial) => + apiRequest('PATCH', `/api/properties/${propertyId}/leases/${leaseId}`, data).then(r => r.json()), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/leases`] }); + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/rent-roll`] }); + qc.invalidateQueries({ queryKey: [`/api/properties/${propertyId}/financials`] }); + }, + }); +} + export function useSendPropertyAdvice(propertyId: string) { return useMutation({ mutationFn: (message: string) => diff --git a/client/src/pages/PropertyDetail.tsx b/client/src/pages/PropertyDetail.tsx index f96f16b..d089442 100644 --- a/client/src/pages/PropertyDetail.tsx +++ b/client/src/pages/PropertyDetail.tsx @@ -18,6 +18,9 @@ import ValuationTab from '@/components/property/ValuationTab'; import AddUnitDialog from '@/components/property/AddUnitDialog'; import AddLeaseDialog from '@/components/property/AddLeaseDialog'; import EditPropertyDialog from '@/components/property/EditPropertyDialog'; +import EditUnitDialog from '@/components/property/EditUnitDialog'; +import EditLeaseDialog from '@/components/property/EditLeaseDialog'; +import type { Unit, Lease } from '@/hooks/use-property'; import WorkspaceTab from '@/components/property/WorkspaceTab'; import CommsPanel from '@/components/property/CommsPanel'; import WorkflowBoard from '@/components/property/WorkflowBoard'; @@ -30,6 +33,10 @@ export default function PropertyDetail() { const [addUnitOpen, setAddUnitOpen] = useState(false); const [addLeaseOpen, setAddLeaseOpen] = useState(false); const [editPropertyOpen, setEditPropertyOpen] = useState(false); + const [editUnitOpen, setEditUnitOpen] = useState(false); + const [selectedUnit, setSelectedUnit] = useState(null); + const [editLeaseOpen, setEditLeaseOpen] = useState(false); + const [selectedLease, setSelectedLease] = useState(null); const { data: property, isLoading } = useProperty(id); const { data: units = [] } = usePropertyUnits(id); @@ -90,6 +97,8 @@ export default function PropertyDetail() { {id && } {id && } {property && } + {selectedUnit && id && } + {selectedLease && id && } @@ -165,6 +174,7 @@ export default function PropertyDetail() { Rent Tenant Status + @@ -176,12 +186,27 @@ export default function PropertyDetail() { {u.bedrooms}br / {u.bathrooms}ba {u.squareFeet?.toLocaleString()} {formatCurrency(parseFloat(u.monthlyRent || '0'))} - {lease?.tenantName || '\u2014'} + + {lease ? ( + + ) : '\u2014'} + {lease ? 'Leased' : 'Vacant'} + + + ); })}