Implement Lightbox, fix photo gallery accessibility, update local assets and OG tags for Vercel deployment

This commit is contained in:
Satyam
2026-02-13 05:49:55 +05:30
parent c602b3b152
commit cc58b148e4
28 changed files with 1096 additions and 629 deletions
+47 -524
View File
@@ -6,8 +6,12 @@ import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
import { SpotlightCard } from '../components/SpotlightCard';
import { useTheme } from '../context/ThemeContext';
import PropertyDetailDrawer from '../components/maps/PropertyDetailDrawer';
// --- ICONS ---
import { logger } from '../utils/logger';
import { toast } from 'sonner';
import { toast }
from 'sonner';
import Loader from '../components/Loader';
// Fix for default Leaflet marker icons not showing up
@@ -171,484 +175,6 @@ const MapView = ({ data, onSelect, onMapCreate }) => {
);
};
const RenderInput = ({ label, field, type = "text", placeholder, options, min, max }) => {
// ... helper logic ...
return (
<div className="flex flex-col space-y-1">
{/* ... */}
</div>
)
}
// ... (skipping to Drawer content) ...
// Assuming the Drawer section is below lines 300
// I will target the specific Status Select input lower down in a separate replacement if I can't catch it here.
// Wait, the instruction asked to do it all. The previous `view_file` didn't show the `Drawer` render part fully recently.
// I'll stick to updating Legend and MapView first here as they are contiguous. I will do the Dropdown and Badge in a second pass if not safely accessible in this chunk range.
// Actually, I can probably safely target just the Legend and MapView here as per the `StartLine`/`EndLine`.
// The Drawer render for `status` options is likely further down.
// Let's split this. First Edit: Legend and MapView Colors.
const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, loadingAddr }) => {
const [isEditing, setIsEditing] = useState(false);
// Initial State Matching Schema
// Initial State Matching Schema
const initialFormState = {
// Status
status: 'Neutral',
// 1.1 Basic Property Details
propertyType: 'House',
builtUpArea: '',
lotSize: '',
yearBuilt: '',
bedrooms: '',
bathrooms: '',
parkingSpaces: '',
// 1.2 Transaction & Market
purchaseDate: '',
purchasePrice: '',
marketValue: 0,
taxValue: '',
// 1.3 Renovation
roofCondition: 'Good',
lastRenovationDate: '',
// 1.4 Rental Status
isRented: 'No', // Yes/No
tenantName: '',
currentRent: '',
leaseEnd: '',
ownerWillingToRent: 'Yes',
expectedRent: '',
// 1.5 Location
schoolDistrict: '',
neighborhoodRating: 5,
// 2.1 Owner Details
ownerName: '',
ownerPhone: '',
ownerEmail: '',
ownerAddress: '',
occupation: '',
ownershipType: 'Individual',
// 2.4 Owner Intent
willingToSell: 'No',
desiredPrice: '',
minPrice: '',
// 3.0 Insurance Information
insurance_company: '',
insurance_company_not_listed: false,
damage_location: '',
date_of_loss: '',
claim_filed: 'No',
claim_number: '',
has_paperwork: 'No',
adjuster_name: '',
adjuster_phone: '',
adjuster_ext: '',
adjuster_type: '',
adjuster_fax: '',
adjuster_email: '',
met_with_adjuster: 'No',
claim_approved: 'No',
// Notes
notes: ''
};
const [formData, setFormData] = useState(initialFormState);
// Reset or Initialize Form
useEffect(() => {
if (data) {
// Mapping existing data to form state (handling potentially missing fields safely)
setFormData({
...initialFormState,
status: data.canvassingStatus || 'Neutral',
ownerName: data.ownerData?.fullName || '',
ownerPhone: data.ownerData?.primaryPhoneNumber || '',
ownerEmail: data.ownerData?.emailAddress || '',
marketValue: data.propertyData?.currentEstimatedMarketValue || 0,
notes: data.propertyData?.notes || '',
propertyType: data.propertyData?.propertyType || 'House',
// Correctly map fields from mockStore structure (flat objects) to form state
builtUpArea: data.propertyData?.totalBuiltUpArea || '',
lotSize: data.propertyData?.lotPlotSize || '',
yearBuilt: data.propertyData?.yearBuilt || '',
bedrooms: data.propertyData?.numberOfBedrooms || '',
bathrooms: data.propertyData?.numberOfBathrooms || '',
parkingSpaces: data.propertyData?.numberOfParkingSpaces || '',
taxValue: data.propertyData?.propertyTaxAssessmentValue || '',
roofCondition: data.propertyData?.roofCondition || 'Good',
lastRenovationDate: data.propertyData?.lastMajorRenovationDate || '',
isRented: data.propertyData?.currentlyRented ? 'Yes' : 'No',
tenantName: data.propertyData?.currentTenantName || '',
currentRent: data.propertyData?.currentMonthlyRentAmount || '',
leaseEnd: data.propertyData?.leaseEndDate || '',
ownerWillingToRent: data.propertyData?.ownerWillingToRent ? 'Yes' : 'No',
expectedRent: data.propertyData?.expectedMonthlyRentAmount || '',
schoolDistrict: data.propertyData?.schoolDistrict || '',
neighborhoodRating: data.propertyData?.neighborhoodRating || 5,
occupation: data.ownerData?.occupation || '',
ownershipType: data.ownerData?.ownershipType || 'Individual',
willingToSell: data.ownerData?.willingToSellProperty ? 'Yes' : 'No',
desiredPrice: data.ownerData?.desiredSellingPrice || '',
minPrice: data.ownerData?.minimumAcceptableSellingPrice || '',
ownerAddress: data.ownerData?.mailingAddress || '',
// Map Insurance Data
insurance_company: data.insuranceData?.insurance_company || '',
insurance_company_not_listed: data.insuranceData?.insurance_company_not_listed || false,
damage_location: data.insuranceData?.damage_location || '',
date_of_loss: data.insuranceData?.date_of_loss || '',
claim_filed: data.insuranceData?.claim_filed ? 'Yes' : 'No',
claim_number: data.insuranceData?.claim_number || '',
has_paperwork: data.insuranceData?.has_paperwork ? 'Yes' : 'No',
adjuster_name: data.insuranceData?.adjuster_name || '',
adjuster_phone: data.insuranceData?.adjuster_phone || '',
adjuster_ext: data.insuranceData?.adjuster_ext || '',
adjuster_type: data.insuranceData?.adjuster_type || '',
adjuster_fax: data.insuranceData?.adjuster_fax || '',
adjuster_email: data.insuranceData?.adjuster_email || '',
met_with_adjuster: data.insuranceData?.met_with_adjuster ? 'Yes' : 'No',
claim_approved: data.insuranceData?.claim_approved ? 'Yes' : 'No',
});
setIsEditing(false);
} else if (newLocation) {
setFormData(initialFormState);
setIsEditing(true);
}
}, [data, newLocation]);
const handleSave = () => {
onSave(formData);
if (data) setIsEditing(false);
};
if (!isOpen) return null;
const isCreating = !data && !!newLocation;
const address = data?.propertyData?.propertyAddress || newLocation?.address || "Unknown Location";
const coords = data ? null : newLocation;
// Helper for Inputs
const RenderInput = ({ label, field, type = "text", placeholder, options, locked = false }) => {
if (!isEditing) {
return (
<InputGroup label={label}>
<span className="text-sm font-medium text-zinc-900 dark:text-zinc-200">
{formData[field] || "-"}
</span>
</InputGroup>
);
}
if (locked) {
return (
<InputGroup label={label}>
<div className="w-full bg-zinc-100 dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/5 rounded-lg py-2 px-3 text-sm text-zinc-400 cursor-not-allowed italic">
{placeholder || "Not Applicable"}
</div>
</InputGroup>
);
}
return (
<InputGroup label={label}>
{options ? (
<select
value={formData[field]}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
className="w-full bg-zinc-50 dark:bg-black/20 border border-zinc-200 dark:border-white/10 rounded-lg py-2 px-2 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 appearance-none"
>
{options.map(opt => (
<option key={opt} value={opt} className="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white">
{opt}
</option>
))}
</select>
) : (
<input
type={type}
value={formData[field]}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
placeholder={placeholder}
className="w-full bg-zinc-50 dark:bg-black/20 border border-zinc-200 dark:border-white/10 rounded-lg py-2 px-3 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-zinc-400"
/>
)}
</InputGroup>
);
};
return (
<div className={`fixed z-[1000] flex flex-col transition-transform duration-300 ease-out inset-0 md:inset-auto md:top-4 md:right-4 md:bottom-4 md:w-[500px] ${isOpen ? 'translate-x-0' : 'translate-x-full md:translate-x-[calc(100%+2rem)]'}`}>
<SpotlightCard className="h-full flex flex-col overflow-hidden bg-white/95 dark:bg-zinc-900/95 backdrop-blur-3xl border-l border-white/20 shadow-2xl">
{/* Header */}
<div className="relative h-32 bg-gradient-to-br from-zinc-100 to-zinc-200 dark:from-zinc-900 dark:to-black p-6 flex flex-col justify-end shrink-0 border-b border-zinc-200 dark:border-white/5">
{/* Pattern */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.03] dark:opacity-10 pointer-events-none mix-blend-overlay"></div>
<button onClick={onClose} className="absolute top-4 right-4 p-2 bg-white/50 dark:bg-white/10 hover:bg-white dark:hover:bg-white/20 rounded-full transition-colors backdrop-blur-md text-zinc-900 dark:text-white">
<X size={18} />
</button>
<div className="relative z-10">
<div className="flex items-center space-x-2 mb-1">
{isCreating ? <span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-500 text-white">New Entry</span> : <StatusBadge status={isEditing ? formData.status : data.canvassingStatus} />}
</div>
<h2 className="text-xl font-black text-zinc-900 dark:text-white truncate">{loadingAddr ? "Fetching Address..." : address.split(',')[0]}</h2>
<span className="text-xs text-zinc-500 truncate block">{loadingAddr ? "Locating..." : address}</span>
</div>
</div>
{/* Body Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-8 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
{loadingAddr ? (
<div className="flex flex-col items-center justify-center h-40 space-y-3 text-zinc-500">
<Loader2 className="animate-spin" size={24} />
<span className="text-xs font-bold uppercase">Resolving Location...</span>
</div>
) : (
<>
{/* Status Selector */}
{isEditing && (
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold text-zinc-500 tracking-widest ml-1">Canvassing Status</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{["Neutral", "Hot Lead", "Renovated", "Customer", "Not Interested"].map(s => (
<button key={s} onClick={() => setFormData({ ...formData, status: s })} className={`px-1 py-2 rounded text-[10px] font-bold border transition-all ${formData.status === s ? 'bg-zinc-900 dark:bg-white text-white dark:text-black' : 'bg-transparent border-zinc-200 dark:border-white/10 text-zinc-500'}`}>{s}</button>
))}
</div>
</div>
)}
{/* SECTION 1: BASIC PROPERTY INFO */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-blue-600 dark:text-blue-400">
<Home size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Property Basics</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Property Type" field="propertyType" options={["House", "Apartment", "Condo", "Commercial", "Land"]} />
<RenderInput label="Built-Up Area (sqft)" field="builtUpArea" type="number" />
<RenderInput label="Lot Size (sqft)" field="lotSize" type="number" />
<RenderInput label="Year Built" field="yearBuilt" type="number" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 col-span-1 md:col-span-2">
<RenderInput label="Beds" field="bedrooms" type="number" />
<RenderInput label="Baths" field="bathrooms" type="number" />
<RenderInput label="Parking" field="parkingSpaces" type="number" />
</div>
</div>
</div>
{/* SECTION 2: VALUES & CONDITION */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-emerald-600 dark:text-emerald-400">
<DollarSign size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Value & Condition</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Est. Market Value ($)" field="marketValue" type="number" />
<RenderInput label="Tax Assessed Value ($)" field="taxValue" type="number" />
<RenderInput label="Roof Condition" field="roofCondition" options={["Excellent", "Good", "Fair", "Needs Repair", "Critical"]} />
<RenderInput label="Last Reno Date" field="lastRenovationDate" type="date" />
</div>
</div>
{/* SECTION 3: RENTAL STATUS (Conditional) */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-purple-600 dark:text-purple-400">
<div className="p-1 bg-purple-100 dark:bg-purple-900/30 rounded"><User size={12} /></div>
<h3 className="text-xs font-black uppercase tracking-widest">Rental Status</h3>
</div>
<RenderInput label="Currently Rented?" field="isRented" options={["Yes", "No"]} />
{formData.isRented === 'Yes' ? (
<div className="p-4 bg-purple-50 dark:bg-purple-900/10 rounded-xl space-y-4 border border-purple-100 dark:border-purple-500/20">
<h4 className="text-[10px] uppercase font-bold text-purple-600 dark:text-purple-400 border-b border-purple-200 dark:border-purple-500/20 pb-1 mb-2">Tenant Profile</h4>
<RenderInput label="Full Name *" field="tenantName" placeholder="Required" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Phone *" field="tenantPhone" placeholder="Required" />
<RenderInput label="Email" field="tenantEmail" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Occupation" field="tenantOccupation" />
<RenderInput label="Employer" field="tenantEmployer" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Annual Income" field="tenantIncome" />
<RenderInput label="Family Status" field="livingStatus" options={["Single", "Family", "Couple", "Roommates"]} />
</div>
<h4 className="text-[10px] uppercase font-bold text-purple-600 dark:text-purple-400 border-b border-purple-200 dark:border-purple-500/20 pb-1 mb-2 mt-4">Lease Details</h4>
<RenderInput label="Lease Type" field="leaseType" options={["Residential Standard", "Commercial NNN", "Short Term", "Month-to-Month"]} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<RenderInput label="Signed" field="leaseSigned" type="date" />
<RenderInput label="Start" field="leaseStart" type="date" />
<RenderInput label="End" field="leaseEnd" type="date" />
</div>
<RenderInput label="Monthly Rent ($)" field="currentRent" type="number" />
</div>
) : (
<div className="p-4 bg-zinc-50 dark:bg-white/5 rounded-xl space-y-4">
<RenderInput label="Owner Willing to Rent?" field="ownerWillingToRent" options={["Yes", "No", "Maybe"]} />
{formData.ownerWillingToRent !== 'No' && (
<RenderInput label="Expected Rent ($)" field="expectedRent" type="number" />
)}
</div>
)}
</div>
{/* SECTION 4: INSURANCE INFORMATION */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-blue-600 dark:text-blue-400">
<ShieldCheck size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Insurance Information</h3>
</div>
<div className="space-y-4">
<RenderInput label="Insurance Company" field="insurance_company" options={["State Farm", "Allstate", "Liberty Mutual", "Farmers", "Nationwide", "USAA", "Chubb", "Travelers", "Progressive", "American Family", "Other"]} />
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.insurance_company_not_listed}
onChange={(e) => isEditing && setFormData({ ...formData, insurance_company_not_listed: e.target.checked })}
disabled={!isEditing}
className="rounded border-zinc-300 dark:border-white/10"
/>
<span className="text-xs text-zinc-500">Company Not Listed</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Claim Filed?" field="claim_filed" options={["Yes", "No"]} />
<RenderInput label="Date of Loss" field="date_of_loss" type="date" />
</div>
{formData.claim_filed === 'Yes' && (
<div className="p-4 bg-blue-50 dark:bg-blue-900/10 rounded-xl space-y-4 border border-blue-100 dark:border-blue-500/20">
<RenderInput label="Damage Location" field="damage_location" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Claim Number" field="claim_number" />
<RenderInput label="Has Paperwork?" field="has_paperwork" options={["Yes", "No"]} />
</div>
<h4 className="text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 border-b border-blue-200 dark:border-blue-500/20 pb-1 mb-2 mt-2">Adjuster Details</h4>
<RenderInput label="Adjuster Name" field="adjuster_name" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<RenderInput label="Phone" field="adjuster_phone" />
<RenderInput label="Ext" field="adjuster_ext" />
<RenderInput label="Fax" field="adjuster_fax" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Email" field="adjuster_email" />
<RenderInput label="Type" field="adjuster_type" options={["Staff Adjuster", "Independent Adjuster", "Public Adjuster"]} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Met Adjuster?" field="met_with_adjuster" options={["Yes", "No"]} />
<RenderInput label="Claim Approved?" field="claim_approved" options={["Yes", "No"]} />
</div>
</div>
)}
</div>
</div>
{/* SECTION 5: OWNER INTENT (Conditional) */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-amber-600 dark:text-amber-400">
<AlertCircle size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Recall & Sales Intent</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Willing to Sell?" field="willingToSell" options={["Yes", "No", "Undecided"]} />
<RenderInput label="Ownership Type" field="ownershipType" options={["Individual", "Joint", "Corporate", "Trust"]} />
</div>
{formData.willingToSell === 'Yes' ? (
<div className="p-4 bg-amber-50 dark:bg-amber-900/10 rounded-xl space-y-4 border border-amber-100 dark:border-amber-500/20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Desired Price" field="desiredPrice" type="number" />
<RenderInput label="Min Price" field="minPrice" type="number" />
</div>
</div>
) : (
<RenderInput label="Desired Price" field="desiredPrice" locked={true} placeholder="Owner not selling" />
)}
</div>
{/* SECTION 5: OWNER CONTACT */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-zinc-500">
<User size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Owner Contact</h3>
</div>
<RenderInput label="Full Name" field="ownerName" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Phone" field="ownerPhone" />
<RenderInput label="Email" field="ownerEmail" />
</div>
<RenderInput label="Occupation" field="occupation" />
</div>
{/* SECTION 6: NOTES */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-zinc-200 dark:border-white/5 text-zinc-500">
<Edit2 size={16} /> <h3 className="text-xs font-black uppercase tracking-widest">Field Notes</h3>
</div>
{isEditing ? (
<textarea
className="w-full h-32 bg-zinc-50 dark:bg-black/20 border border-zinc-200 dark:border-white/10 rounded-xl p-3 text-sm text-zinc-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 resize-none"
value={formData.notes}
placeholder="Add notes..."
onChange={e => setFormData({ ...formData, notes: e.target.value })}
/>
) : (
<p className="text-sm text-zinc-600 dark:text-zinc-400 bg-zinc-50 dark:bg-white/5 p-4 rounded-xl border border-zinc-200 dark:border-white/5">
{formData.notes || "No notes available."}
</p>
)}
</div>
</>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-black/20 backdrop-blur-md shrink-0">
{isEditing ? (
<button onClick={handleSave} disabled={loadingAddr} className="w-full py-3 bg-zinc-900 dark:bg-white hover:bg-zinc-800 text-white dark:text-black font-bold rounded-xl shadow-lg transition-all flex items-center justify-center space-x-2 disabled:opacity-50">
<Save size={18} /> <span>{isCreating ? "Create Entry" : "Save Changes"}</span>
</button>
) : (
<button onClick={() => setIsEditing(true)} className="w-full py-3 bg-white dark:bg-zinc-800 hover:bg-zinc-50 text-zinc-900 dark:text-white border border-zinc-200 dark:border-white/10 font-bold rounded-xl shadow-sm transition-all flex items-center justify-center space-x-2">
<Edit2 size={18} /> <span>Edit Details</span>
</button>
)}
</div>
</SpotlightCard>
</div>
);
};
const InputGroup = ({ label, children }) => (
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-zinc-400 tracking-widest ml-0.5">{label}</label>
<div>{children}</div>
</div>
);
// --- MAIN PAGE COMPONENT --- //
function Maps() {
@@ -662,8 +188,6 @@ function Maps() {
return () => clearTimeout(timer);
}, []);
// Selection State
const [selectedPropertyId, setSelectedPropertyId] = useState(null);
const [newPropertyLocation, setNewPropertyLocation] = useState(null); // { lat, lng, address }
@@ -798,55 +322,54 @@ function Maps() {
return {
...p,
canvassingStatus: formData.status,
ownerData: { ...p.ownerData, ...newOwnerData, details: { ...p.ownerData.details, ...newOwnerData.details } },
propertyData: {
...p.propertyData,
...newPropertyData,
details: { ...p.propertyData.details, ...newPropertyData.details } // Merge to keep existing data not in form if any
},
insuranceData: newInsuranceData
ownerData: { ...p.ownerData, ...newOwnerData },
propertyData: { ...p.propertyData, ...newPropertyData.details, ...newPropertyData }, // Merge Flattened and Nested
insuranceData: { ...p.insuranceData, ...newInsuranceData }
};
}
return p;
}));
toast.success("Property updated successfully");
} else if (newPropertyLocation) {
// Create New
const newId = `prop-${Date.now()}`;
const newProp = {
id: newId,
polygon: [], // We don't have a polygon drawing tool yet, implies point-based or empty
canvassingStatus: formData.status,
ownerData: newOwnerData,
insuranceData: newInsuranceData,
propertyData: {
...newPropertyData,
propertyAddress: newPropertyLocation.address,
}
};
// In a real app we'd need to store the LatLng for this new valid prop separately if it has no polygon
// For now, we just add it to the list.
// NOTE: Since MapsView maps *Polygons*, this new item won't appear on map unless we add a Marker layer or mock a polygon around the point.
// For MVP, we will mock a tiny square polygon around the point so it renders.
const lat = newPropertyLocation.lat;
const lng = newPropertyLocation.lng;
const size = 0.0001; // Tiny box
newProp.polygon = [
[lat + size, lng - size],
[lat + size, lng + size],
[lat - size, lng + size],
[lat - size, lng - size]
const newId = properties.length + 1;
// Generate Polygon (Simple Box around center) as placeholder
// In real app, user would draw. Here we just spawn a box.
const d = 0.0001;
const newPoly = [
[newPropertyLocation.lat + d, newPropertyLocation.lng - d],
[newPropertyLocation.lat + d, newPropertyLocation.lng + d],
[newPropertyLocation.lat - d, newPropertyLocation.lng + d],
[newPropertyLocation.lat - d, newPropertyLocation.lng - d],
];
setProperties(prev => [...prev, newProp]);
setSelectedPropertyId(newId); // Select the new item
setNewPropertyLocation(null); // Clear creation mode
const newItem = {
id: newId,
center: [newPropertyLocation.lat, newPropertyLocation.lng],
polygon: newPoly,
canvassingStatus: formData.status,
ownerData: {
ownerId: `OWN-NEW-${newId}`,
...newOwnerData
},
propertyData: {
propertyId: `P-NEW-${newId}`,
propertyAddress: newPropertyLocation.address,
...newPropertyData.details,
...newPropertyData
},
insuranceData: {
...newInsuranceData
}
};
setProperties(prev => [...prev, newItem]);
toast.success("New property added to map");
setSelectedPropertyId(newId); // Select it
setNewPropertyLocation(null); // Exit create mode
}
toast.success("Property data saved successfully");
} catch (error) {
logger.error("Error saving property data", error);
toast.error("Failed to save changes", { description: "Please try again." });
} catch (err) {
logger.error("Failed to save property", err);
toast.error("Failed to save changes", { description: "Please check your input." });
}
};
@@ -861,13 +384,13 @@ function Maps() {
/>
<MapLegend />
<Drawer
isOpen={!!selectedPropertyId || !!newPropertyLocation}
<PropertyDetailDrawer
isOpen={!!selectedProperty || !!newPropertyLocation}
onClose={() => { setSelectedPropertyId(null); setNewPropertyLocation(null); }}
data={selectedProperty}
newLocation={newPropertyLocation}
onSave={handleSave}
loadingAddr={loadingAddr}
onSave={handleSave}
/>
</div>
);