mobile: Optimizations for Maps Drawer, Legend, and Customer Profile Tabs

This commit is contained in:
Satyam
2026-02-10 20:36:44 +05:30
parent 91b47e83c3
commit 83250d687e
15 changed files with 1140 additions and 91 deletions
+171 -43
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo, useEffect } from 'react';
import { MapContainer, TileLayer, Polygon, useMapEvents, Marker, Popup } from 'react-leaflet';
import { X, Home, DollarSign, User, Info, Edit2, Save, Check, MapPin, Loader2, Plus, AlertCircle } from 'lucide-react';
import { X, Home, DollarSign, User, Info, Edit2, Save, Check, MapPin, Loader2, Plus, AlertCircle, ShieldCheck } from 'lucide-react';
import 'leaflet/dist/leaflet.css';
import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
@@ -51,33 +51,53 @@ const MapInteraction = ({ onMapClick }) => {
return null;
};
const MapLegend = () => (
<div className="absolute bottom-6 left-6 z-[1000] bg-white/90 dark:bg-black/90 backdrop-blur-md p-4 rounded-2xl border border-zinc-200 dark:border-white/10 shadow-xl">
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-3">Map Legend</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-red-500 shadow-sm shadow-red-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Hot Lead</span>
const MapLegend = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
{/* Mobile Toggle - Top Right */}
<button
onClick={() => setIsOpen(!isOpen)}
className="md:hidden absolute top-4 right-4 z-[2000] bg-white dark:bg-zinc-900 p-3 rounded-full shadow-xl border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-400"
>
<Info size={20} />
</button>
{/* Legend Content - Below Toggle */}
<div className={`absolute top-20 right-4 md:bottom-6 md:right-auto md:top-auto md:left-6 z-[2000] bg-white/95 dark:bg-black/95 backdrop-blur-md p-4 rounded-2xl border border-zinc-200 dark:border-white/10 shadow-xl transition-all duration-300 origin-top-right md:origin-bottom-left
${isOpen ? 'scale-100 opacity-100' : 'scale-0 opacity-0 md:scale-100 md:opacity-100'}
`}>
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-3 flex items-center justify-between">
Map Legend
<button onClick={() => setIsOpen(false)} className="md:hidden ml-4"><X size={14} /></button>
</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-red-500 shadow-sm shadow-red-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Hot Lead</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Customer</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-blue-500 shadow-sm shadow-blue-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Renovated</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-violet-400 shadow-sm shadow-violet-400/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Neutral</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-white border-2 border-zinc-300 dark:border-zinc-500 shadow-sm"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Not Interested</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Customer</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-blue-500 shadow-sm shadow-blue-500/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Renovated</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-violet-400 shadow-sm shadow-violet-400/50"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Neutral</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full bg-white border-2 border-zinc-300 dark:border-zinc-500 shadow-sm"></div>
<span className="text-xs font-bold text-zinc-700 dark:text-zinc-300">Not Interested</span>
</div>
</div>
</div>
);
</>
);
};
// Map View
const MapView = ({ data, onSelect, onMapCreate }) => {
@@ -102,7 +122,6 @@ const MapView = ({ data, onSelect, onMapCreate }) => {
/>
<MapInteraction onMapClick={onMapCreate} />
<MapLegend />
{data.map((item) => {
let color = "#a78bfa"; // Violet-400 (New Neutral)
@@ -175,6 +194,7 @@ const RenderInput = ({ label, field, type = "text", placeholder, options, min, m
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
@@ -224,6 +244,23 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
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: ''
};
@@ -245,7 +282,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
propertyType: data.propertyData?.propertyType || 'House',
// Correctly map fields from mockStore structure (flat objects) to form state
builtUpArea: data.propertyData?.totalBuiltUpArea || '',
lotSize: data.propertyData?.lotPlotSize || '', // mockStore might return string "0.22 Acres", assuming input handles text or needs parsing if strictly number
lotSize: data.propertyData?.lotPlotSize || '',
yearBuilt: data.propertyData?.yearBuilt || '',
bedrooms: data.propertyData?.numberOfBedrooms || '',
bathrooms: data.propertyData?.numberOfBathrooms || '',
@@ -271,6 +308,23 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
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) {
@@ -340,7 +394,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
};
return (
<div className={`fixed top-4 right-4 bottom-4 w-[500px] z-[1000] flex flex-col transition-transform duration-300 ease-out ${isOpen ? 'translate-x-0' : 'translate-x-[calc(100%+2rem)]'}`}>
<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 */}
@@ -375,7 +429,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
{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-3 gap-2">
<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>
))}
@@ -388,12 +442,12 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<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-2 gap-4">
<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-3 gap-2 col-span-2">
<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" />
@@ -406,7 +460,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<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-2 gap-4">
<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"]} />
@@ -427,22 +481,22 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<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-2 gap-4">
<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-2 gap-4">
<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-2 gap-4">
<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-3 gap-2">
<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" />
@@ -459,20 +513,73 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
)}
</div>
{/* SECTION 4: OWNER INTENT (Conditional) */}
{/* 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-2 gap-4">
<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-2 gap-4">
<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>
@@ -488,7 +595,7 @@ const Drawer = ({ isOpen, onClose, data, newLocation, onUpdateStatus, onSave, lo
<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-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<RenderInput label="Phone" field="ownerPhone" />
<RenderInput label="Email" field="ownerEmail" />
</div>
@@ -666,6 +773,24 @@ function Maps() {
}
};
const newInsuranceData = {
insurance_company: formData.insurance_company,
insurance_company_not_listed: formData.insurance_company_not_listed,
damage_location: formData.damage_location,
date_of_loss: formData.date_of_loss,
claim_filed: formData.claim_filed === 'Yes',
claim_number: formData.claim_number,
has_paperwork: formData.has_paperwork === 'Yes',
adjuster_name: formData.adjuster_name,
adjuster_phone: formData.adjuster_phone,
adjuster_ext: formData.adjuster_ext,
adjuster_type: formData.adjuster_type,
adjuster_fax: formData.adjuster_fax,
adjuster_email: formData.adjuster_email,
met_with_adjuster: formData.met_with_adjuster === 'Yes',
claim_approved: formData.claim_approved === 'Yes',
};
if (selectedPropertyId) {
// Update Existing
setProperties(prev => prev.map(p => {
@@ -678,7 +803,8 @@ function Maps() {
...p.propertyData,
...newPropertyData,
details: { ...p.propertyData.details, ...newPropertyData.details } // Merge to keep existing data not in form if any
}
},
insuranceData: newInsuranceData
};
}
return p;
@@ -691,6 +817,7 @@ function Maps() {
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,
@@ -732,6 +859,7 @@ function Maps() {
onSelect={handlePropertySelect}
onMapCreate={handleMapCreate}
/>
<MapLegend />
<Drawer
isOpen={!!selectedPropertyId || !!newPropertyLocation}