import { useCallback, useEffect, useMemo, useState } from "react"; import { Trash2, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/format-currency"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Field, FieldLabel } from "@/components/ui/field"; import { AddressAutocomplete, type AddressOption, } from "@/components/ui/form-primitives"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; /** * PropertyReportDialog — WealthX DS (L4 Template) * * Dialog for generating a property valuation report from the scenario's * linked properties. Opened from the Bank Statement tab of * `OpportunityDetailsDrawer` when the user selects "Property Report" and * clicks "Generate". * * Internal state: * - Report name (Input) * - Manually added properties (via AddressAutocomplete + Add button) * - Selected property IDs (Table with Checkboxes) * * Data handed in via props (consumer owns fetching): * - `properties` — pre-loaded properties from the scenario (API list) * - `isLoadingProperties` — spinner while fetching * - `searchResults` — address search results driven by `onSearchQueryChange` * * Layer: L4 Template */ // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface PropertyItem { id: string; address?: string; propertyType?: string; /** Estimated value in dollars. */ estimateValue?: number; } export interface PropertySearchResult { /** Unique property identifier returned by the address search service. */ id: string; /** Full address string shown in the dropdown. */ address: string; } export interface PropertyReportPayload { reportName: string; selectedPropertyIds: string[]; } export interface PropertyReportDialogProps { open: boolean; onClose: () => void; /** * Called when the user clicks Generate (only when form is valid). * Consumer is responsible for the actual API call. */ onSubmit: (payload: PropertyReportPayload) => void; /** Pre-loaded properties from the scenario's loan data. */ properties: PropertyItem[]; /** Provide `true` while the consumer is fetching the scenario's properties. */ isLoadingProperties?: boolean; /** * Address search results for the autocomplete dropdown. * Consumer debounces and fetches when `onSearchQueryChange` fires. */ searchResults?: PropertySearchResult[]; /** * Called when the user types in the address search input (debounce externally). * Pass an empty string when the query is cleared. */ onSearchQueryChange?: (query: string) => void; /** Show a loading indicator on the Generate button while the API call is in flight. */ isLoading?: boolean; className?: string; } // --------------------------------------------------------------------------- // PropertyReportDialog // --------------------------------------------------------------------------- export function PropertyReportDialog({ open, onClose, onSubmit, properties, isLoadingProperties = false, searchResults = [], onSearchQueryChange, isLoading = false, className, }: PropertyReportDialogProps) { const [reportName, setReportName] = useState("Property Report 1"); const [selectedPropertyIds, setSelectedPropertyIds] = useState([]); // Properties added manually via the address search (not in the API list) const [manualProperties, setManualProperties] = useState([]); // Address autocomplete state const [addressInputValue, setAddressInputValue] = useState(""); const [selectedSuggestion, setSelectedSuggestion] = useState(null); // Convert searchResults → AddressOption[] for AddressAutocomplete const addressOptions = useMemo( () => searchResults.map((r) => ({ id: r.id, label: r.address })), [searchResults], ); // Reset form whenever the dialog opens useEffect(() => { if (!open) return; setReportName("Property Report 1"); setSelectedPropertyIds([]); setManualProperties([]); setAddressInputValue(""); setSelectedSuggestion(null); onSearchQueryChange?.(""); }, [open]); // eslint-disable-line react-hooks/exhaustive-deps // Combined list shown in the table const allProperties = useMemo( () => [...properties, ...manualProperties], [properties, manualProperties], ); // ── Property selection ─────────────────────────────────────────────────── const areAllSelected = allProperties.length > 0 && selectedPropertyIds.length === allProperties.length; const isSomeSelected = selectedPropertyIds.length > 0 && !areAllSelected; const handleToggleProperty = (id: string) => { setSelectedPropertyIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); }; const handleToggleAll = () => { if (areAllSelected) { setSelectedPropertyIds([]); } else { setSelectedPropertyIds(allProperties.map((p) => p.id)); } }; // ── Address search & manual add ────────────────────────────────────────── const handleAddProperty = useCallback(() => { if (!selectedSuggestion) return; const { id, address } = selectedSuggestion; const alreadyExists = allProperties.some((p) => p.id === id); if (alreadyExists) return; // silently skip (button is disabled when already added) setManualProperties((prev) => [...prev, { id, address }]); setSelectedSuggestion(null); setAddressInputValue(""); onSearchQueryChange?.(""); }, [selectedSuggestion, allProperties, onSearchQueryChange]); const handleRemoveManual = useCallback((id: string) => { setManualProperties((prev) => prev.filter((p) => p.id !== id)); setSelectedPropertyIds((prev) => prev.filter((x) => x !== id)); }, []); const isManual = (id: string) => manualProperties.some((p) => p.id === id); const isAddDisabled = !selectedSuggestion || allProperties.some((p) => p.id === selectedSuggestion.id); // ── Submit guard ───────────────────────────────────────────────────────── const isSubmitDisabled = !reportName.trim() || selectedPropertyIds.length === 0; const handleSubmit = () => { if (isSubmitDisabled) return; onSubmit({ reportName, selectedPropertyIds }); }; // ── Render ─────────────────────────────────────────────────────────────── return ( !o && onClose()}> Property Report
{/* Report name */} Name setReportName(e.target.value)} placeholder="Property Report 1" /> {/* Address search + Add button */}
Search property { setAddressInputValue(v); setSelectedSuggestion(null); onSearchQueryChange?.(v); }} onSelect={(opt) => { setSelectedSuggestion({ id: opt.id, address: opt.label }); }} />
{/* Properties table */}
{isLoadingProperties ? (
) : allProperties.length === 0 ? (

No properties found. Use the search above to add properties.

) : ( Address Property Type Estimate {allProperties.map((property) => ( handleToggleProperty(property.id) } aria-label={`Select ${property.address ?? property.id}`} /> {property.address ?? "—"} {property.propertyType ?? "—"} {property.estimateValue ? formatCurrency(property.estimateValue) : "—"} {isManual(property.id) && ( )} ))}
)}
); }