fix(dispatch): fix Leaflet map blank/partial render on mobile and tablet

- 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
This commit is contained in:
Satyam
2026-03-26 14:31:36 +05:30
parent b804f7cff1
commit 9be8ec911f
3 changed files with 60 additions and 16 deletions
+10 -3
View File
@@ -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,
},
];
+19 -11
View File
@@ -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 (300520px).
const mapH = isDesktop ? 'calc(72rem + 2.375rem)' : (mapHeight || '300px');
return (
<div className={`relative overflow-hidden ${containerClass}`} style={containerStyle}>
<div className="relative overflow-hidden" style={{ height: mapH }}>
{/* Inject map animations */}
<style>{MAP_STYLES}</style>
@@ -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 */}
<TileLayer
+31 -2
View File
@@ -162,8 +162,23 @@ const LynkDispatchPage = () => {
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' }}
>
<span className="w-1 h-1 rounded-full" style={{ backgroundColor: 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}
/>
) : (
<CollapsePanel open={!collapsed.map}>
<DispatchMapPanel
selectedLead={selectedLead}
dispatchLeads={dispatchLeads}
reps={effectiveReps}
stormMode={stormMode}
accent={accent}
isDesktop={isDesktop}
mapHeight={mapHeight}
/>
</CollapsePanel>
)}