From 9be8ec911fb383bceb0ed1cf43cdbdb2fe722156 Mon Sep 17 00:00:00 2001 From: Satyam <95536056+Satyam-Rastogi@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:31:36 +0530 Subject: [PATCH] fix(dispatch): fix Leaflet map blank/partial render on mobile and tablet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DispatchMapPanel: replace h-full/height:100% with explicit inline px height on both wrapper div and MapContainer — CollapsePanel uses height:auto which makes percentage heights resolve to 0 giving Leaflet a zero-size canvas - DispatchMapPanel: accept mapHeight prop from parent for responsive sizing - LynkDispatchPage: compute responsive mapHeight state (300px mobile → 380px sm → 460px md → 520px lg tablet) updated on window resize; passed as prop to DispatchMapPanel on all non-desktop renders - LynkDispatchPage: add missing reps={effectiveReps} prop on mobile map render so live rep status cycling is reflected on all breakpoints - LynkDispatchPage: add missing onQuickAssign={handleAssign} on desktop lead queue render so Quick Assign button works on desktop - LynkDispatchPage: add whitespace-nowrap to efficiency badge to prevent '89% AI-Dispatched' wrapping to two lines in header - LynkDispatchPage: dispatch synthetic resize event 260ms after map panel expands on mobile/tablet so Leaflet invalidateSize fires post-animation - DispatchMapPanel: add second invalidateSize call at 320ms (belt-and-suspenders alongside 50ms) to cover 220ms CollapsePanel animation on slower devices - DispatchLeadQueue: enrich DROP_POOL leads with phone, email, source sub-details and notes so Lead Quick View shows complete data on drop-in leads --- src/components/dispatch/DispatchLeadQueue.jsx | 13 ++++++-- src/components/dispatch/DispatchMapPanel.jsx | 30 ++++++++++------- src/pages/LynkDispatchPage.jsx | 33 +++++++++++++++++-- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/components/dispatch/DispatchLeadQueue.jsx b/src/components/dispatch/DispatchLeadQueue.jsx index 5e7e65a..10eee9e 100644 --- a/src/components/dispatch/DispatchLeadQueue.jsx +++ b/src/components/dispatch/DispatchLeadQueue.jsx @@ -56,23 +56,30 @@ const getAgingState = (arrivedMinsAgo, status) => { }; // Fake leads that "drop in" every 20s during demo — cycled through pool +// Keep these fully enriched so Lead Quick View shows complete detail const DROP_POOL = [ { source: 'call_center', leadType: 'roof_inspection', urgency: 'high', - customer: { name: 'Marcus Webb' }, + customer: { name: 'Marcus Webb', phone: '(972) 555-2201', email: 'marcus.webb@gmail.com' }, property: { address: '1620 Mapleshade Ln', city: 'Plano', state: 'TX', zip: '75075', lat: 33.0545, lng: -96.7634 }, + callCenter: { id: 'CC-02', name: 'DFW Ops Hub' }, + notes: 'Homeowner noticed missing shingles after last week\'s storm. No active leak reported but wants inspection before weekend rain.', estimatedDuration: 45, status: 'unassigned', arrivedMinsAgo: 0, assignedRepId: null, }, { source: 'website_form', leadType: 'emergency_tarp', urgency: 'emergency', - customer: { name: 'Sandra Kim' }, + customer: { name: 'Sandra Kim', phone: '(972) 555-2202', email: 'sandra.kim@icloud.com' }, property: { address: '3488 Midway Rd', city: 'Plano', state: 'TX', zip: '75093', lat: 33.0712, lng: -96.8090 }, + webSource: { campaign: 'Google Ads — Emergency Storm Response' }, + notes: 'Tree limb through garage roof — active water ingress reported. Customer is extremely stressed. Dispatch immediately.', estimatedDuration: 90, status: 'unassigned', arrivedMinsAgo: 0, assignedRepId: null, }, { source: 'canvassing', leadType: 'insurance_claim', urgency: 'high', - customer: { name: 'David Okonkwo' }, + customer: { name: 'David Okonkwo', phone: '(972) 555-2203', email: 'd.okonkwo@outlook.com' }, property: { address: '892 McDermott Dr', city: 'Plano', state: 'TX', zip: '75024', lat: 33.0921, lng: -96.7812 }, + canvassedBy: { agentId: 'U-09', name: 'Kenji Flores' }, + notes: null, estimatedDuration: 60, status: 'unassigned', arrivedMinsAgo: 0, assignedRepId: null, }, ]; diff --git a/src/components/dispatch/DispatchMapPanel.jsx b/src/components/dispatch/DispatchMapPanel.jsx index 216d28b..35151f5 100644 --- a/src/components/dispatch/DispatchMapPanel.jsx +++ b/src/components/dispatch/DispatchMapPanel.jsx @@ -113,13 +113,19 @@ const MapController = ({ selectedLeadId, leadPos, repPos, routePath }) => { // Invalidate size on mount and on every window resize so Leaflet fills // the full flex container — prevents the blank tile area on the right. + // On mobile the map panel is inside a CollapsePanel that animates + // height 0→auto over 220ms, so we need to wait past that before calling + // invalidateSize, otherwise Leaflet initialises at near-zero height. useEffect(() => { const invalidate = () => map.invalidateSize({ animate: false }); - // Defer one frame so the flex layout has fully settled - const t = setTimeout(invalidate, 50); + // 50ms covers desktop (no animation). 320ms covers the 220ms collapse + // animation with margin so tiles fill correctly on mobile/tablet. + const t1 = setTimeout(invalidate, 50); + const t2 = setTimeout(invalidate, 320); window.addEventListener('resize', invalidate); return () => { - clearTimeout(t); + clearTimeout(t1); + clearTimeout(t2); window.removeEventListener('resize', invalidate); }; }, [map]); @@ -186,7 +192,7 @@ const MAP_STYLES = ` // --------------------------------------------------------------------------- // Main Component // --------------------------------------------------------------------------- -const DispatchMapPanel = ({ selectedLead, dispatchLeads, reps = DISPATCH_REPS, stormMode, accent, isDesktop }) => { +const DispatchMapPanel = ({ selectedLead, dispatchLeads, reps = DISPATCH_REPS, stormMode, accent, isDesktop, mapHeight }) => { const { theme } = useTheme(); // Top recommendation for the selected lead @@ -213,13 +219,15 @@ const DispatchMapPanel = ({ selectedLead, dispatchLeads, reps = DISPATCH_REPS, s const routeColor = stormMode ? '#F59E0B' : accent; - const containerStyle = isDesktop ? { height: 'calc(72rem + 2.375rem)' } : undefined; - const containerClass = !isDesktop - ? 'min-h-[280px] sm:min-h-[360px] md:min-h-[420px]' - : ''; + // Explicit pixel height on both the wrapper div and MapContainer. + // Do NOT use h-full / height:100% — the CollapsePanel ancestor has + // height:auto so the percentage chain resolves to 0. Inline px values + // bypass the chain entirely and give Leaflet a reliable offsetHeight. + // mapHeight prop is computed responsively in LynkDispatchPage (300–520px). + const mapH = isDesktop ? 'calc(72rem + 2.375rem)' : (mapHeight || '300px'); return ( -
+
{/* Inject map animations */} @@ -228,8 +236,8 @@ const DispatchMapPanel = ({ selectedLead, dispatchLeads, reps = DISPATCH_REPS, s zoom={DEFAULT_ZOOM} scrollWheelZoom={true} zoomControl={false} - className="w-full h-full" - style={{ height: '100%', width: '100%' }} + className="w-full" + style={{ height: mapH, width: '100%' }} > {/* Tile layer — dark/light aware */} { const [isDesktop, setIsDesktop] = useState(() => window.innerWidth >= 1280); const aiPanelRef = useRef(null); + // Responsive map height for non-desktop — explicit px needed so Leaflet + // gets a reliable offsetHeight regardless of CollapsePanel's height:auto chain + const calcMapHeight = () => { + const w = window.innerWidth; + if (w >= 1280) return null; // desktop: DispatchMapPanel handles its own height + if (w >= 1024) return '520px'; // iPad Pro 11" landscape / large tablet + if (w >= 768) return '460px'; // iPad Air / md tablet + if (w >= 640) return '380px'; // small tablet / large phone landscape + return '300px'; // mobile portrait + }; + const [mapHeight, setMapHeight] = useState(calcMapHeight); + useEffect(() => { - const handler = () => setIsDesktop(window.innerWidth >= 1280); + const handler = () => { + setIsDesktop(window.innerWidth >= 1280); + setMapHeight(calcMapHeight()); + }; window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); @@ -179,6 +194,16 @@ const LynkDispatchPage = () => { const dragLeft = useCallback((delta) => setLeftWidth(w => Math.min(440, Math.max(220, w + delta))), []); const dragRight = useCallback((delta) => setRightWidth(w => Math.min(440, Math.max(260, w - delta))), []); + // When the map panel expands on mobile/tablet, fire a synthetic resize + // after the CollapsePanel animation (220ms) completes so Leaflet + // invalidateSize() in MapController gets a correct container height. + useEffect(() => { + if (!collapsed.map && !isDesktop) { + const t = setTimeout(() => window.dispatchEvent(new Event('resize')), 260); + return () => clearTimeout(t); + } + }, [collapsed.map, isDesktop]); + const handleSelectLead = (lead) => { if (selectedLead?.id === lead.id) return; setSelectedLead(lead); @@ -328,7 +353,7 @@ const LynkDispatchPage = () => { exit={{ opacity: 0 }} transition={{ duration: 0.2 }} title={`${efficiencyPct}% of dispatches used AI recommendation — ${100 - efficiencyPct}% were manual overrides`} - className="flex items-center gap-1 text-[10px] font-black cursor-default" + className="flex items-center gap-1 text-[10px] font-black cursor-default whitespace-nowrap" style={{ color: stormMode ? '#F59E0B' : '#10B981' }} > @@ -465,6 +490,7 @@ const LynkDispatchPage = () => { selectedLead={selectedLead} onSelectLead={handleSelectLead} onViewDetails={setViewDetailLead} + onQuickAssign={handleAssign} stormMode={stormMode} accent={accent} /> @@ -513,15 +539,18 @@ const LynkDispatchPage = () => { stormMode={stormMode} accent={accent} isDesktop={isDesktop} + mapHeight={null} /> ) : ( )}