feat(dispatch): Phase 2 — responsive layout, collapsible panels, bug fixes
- Fix tab overflow: scrollable tab bar, shortened labels (Open/Risk/Done) - Cap DROP_POOL to 3 fires, no repeat flooding - Resizable drag handles on desktop (xl 1280px breakpoint) - Collapsible panels on mobile/tablet — Map + AI collapsed by default on mobile - iPad Air + iPad Pro 11" treated as tablet via xl breakpoint - Fixed lead list heights: 22rem mobile → 72rem xl (8 cards visible) - PageTransition h-full fix to maintain height chain
This commit is contained in:
@@ -53,6 +53,8 @@
|
|||||||
- Rep scoring with proximity, drive time, calendar fit, weather, and skill factors
|
- Rep scoring with proximity, drive time, calendar fit, weather, and skill factors
|
||||||
- Storm Mode — full UI shift to amber, storm path on map, weather-aware recommendations
|
- Storm Mode — full UI shift to amber, storm path on map, weather-aware recommendations
|
||||||
- KPI bar with animated metrics (leads in queue, assigned today, response time, storm alerts)
|
- KPI bar with animated metrics (leads in queue, assigned today, response time, storm alerts)
|
||||||
|
- New lead auto-drops every 20s during demo, Leaflet map with real Plano TX coordinates
|
||||||
|
- Sidebar label: "LynkDispatch" — branded, sidebar-friendly
|
||||||
|
|
||||||
### Maps & Territory
|
### Maps & Territory
|
||||||
- Interactive Leaflet map with color-coded property polygons
|
- Interactive Leaflet map with color-coded property polygons
|
||||||
@@ -157,4 +159,4 @@ src/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Maintained by Satyam Rastogi | Branch: revamp | Last updated: March 14, 2026**
|
**Maintained by Satyam Rastogi | Branch: revamp | Last updated: March 21, 2026**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const PageTransition = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={location.pathname}
|
key={location.pathname}
|
||||||
className="animate-in fade-in slide-in-from-bottom-4 duration-500 ease-out fill-mode-forwards"
|
className="h-full animate-in fade-in slide-in-from-bottom-4 duration-500 ease-out fill-mode-forwards"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ const STATUS_CONFIG = {
|
|||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'all', label: 'All' },
|
{ id: 'all', label: 'All' },
|
||||||
{ id: 'unassigned', label: 'Unassigned' },
|
{ id: 'unassigned', label: 'Open' },
|
||||||
{ id: 'at_risk', label: 'At Risk' },
|
{ id: 'at_risk', label: 'Risk' },
|
||||||
{ id: 'confirmed', label: 'Confirmed' },
|
{ id: 'confirmed', label: 'Done' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TAB_STATUS_FILTER = {
|
const TAB_STATUS_FILTER = {
|
||||||
@@ -90,7 +90,7 @@ const LeadCard = ({ lead, isSelected, isNew, onSelect, stormMode, accent, index
|
|||||||
transition={{ type: 'spring', stiffness: 420, damping: 32, delay: index * 0.03 }}
|
transition={{ type: 'spring', stiffness: 420, damping: 32, delay: index * 0.03 }}
|
||||||
onClick={() => onSelect(lead)}
|
onClick={() => onSelect(lead)}
|
||||||
whileHover={{ y: -2 }}
|
whileHover={{ y: -2 }}
|
||||||
className={`relative cursor-pointer rounded-xl border transition-all duration-200 p-3 select-none
|
className={`relative cursor-pointer rounded-xl border transition-all duration-200 p-2.5 select-none
|
||||||
${isSelected
|
${isSelected
|
||||||
? 'bg-blue-50/80 dark:bg-blue-500/10 border-blue-300 dark:border-blue-500/40 shadow-md shadow-blue-100/60 dark:shadow-blue-500/5'
|
? 'bg-blue-50/80 dark:bg-blue-500/10 border-blue-300 dark:border-blue-500/40 shadow-md shadow-blue-100/60 dark:shadow-blue-500/5'
|
||||||
: 'bg-white dark:bg-zinc-800/50 border-zinc-200 dark:border-white/[0.06] hover:border-zinc-300 dark:hover:border-white/[0.12] hover:shadow-md'
|
: 'bg-white dark:bg-zinc-800/50 border-zinc-200 dark:border-white/[0.06] hover:border-zinc-300 dark:hover:border-white/[0.12] hover:shadow-md'
|
||||||
@@ -99,7 +99,7 @@ const LeadCard = ({ lead, isSelected, isNew, onSelect, stormMode, accent, index
|
|||||||
style={{ borderLeftWidth: 3, borderLeftColor: borderColor }}
|
style={{ borderLeftWidth: 3, borderLeftColor: borderColor }}
|
||||||
>
|
>
|
||||||
{/* Top row: urgency badge + NEW badge + source */}
|
{/* Top row: urgency badge + NEW badge + source */}
|
||||||
<div className="flex items-start justify-between gap-2 mb-1.5">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded shrink-0 ${urgCfg.pulse ? 'animate-pulse' : ''}`}
|
className={`text-[10px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded shrink-0 ${urgCfg.pulse ? 'animate-pulse' : ''}`}
|
||||||
@@ -152,7 +152,7 @@ const LeadCard = ({ lead, isSelected, isNew, onSelect, stormMode, accent, index
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Bottom row: status + time */}
|
{/* Bottom row: status + time */}
|
||||||
<div className="flex items-center justify-between mt-2 gap-2">
|
<div className="flex items-center justify-between mt-1.5 gap-2">
|
||||||
{assignedRep ? (
|
{assignedRep ? (
|
||||||
<span className="flex items-center gap-1.5 text-[11px] font-medium min-w-0">
|
<span className="flex items-center gap-1.5 text-[11px] font-medium min-w-0">
|
||||||
<div
|
<div
|
||||||
@@ -199,10 +199,14 @@ const DispatchLeadQueue = ({ selectedLead, onSelectLead, stormMode, accent }) =>
|
|||||||
const [newLeadIds, setNewLeadIds] = useState(new Set());
|
const [newLeadIds, setNewLeadIds] = useState(new Set());
|
||||||
const dropIndexRef = useRef(0);
|
const dropIndexRef = useRef(0);
|
||||||
|
|
||||||
// 20-second interval: drop in a new lead from the pool
|
// 20-second interval: drop each lead in the pool once (max DROP_POOL.length drops)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const template = DROP_POOL[dropIndexRef.current % DROP_POOL.length];
|
if (dropIndexRef.current >= DROP_POOL.length) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const template = DROP_POOL[dropIndexRef.current];
|
||||||
dropIndexRef.current++;
|
dropIndexRef.current++;
|
||||||
const lead = { ...template, id: `LD-D${Date.now()}` };
|
const lead = { ...template, id: `LD-D${Date.now()}` };
|
||||||
|
|
||||||
@@ -249,17 +253,17 @@ const DispatchLeadQueue = ({ selectedLead, onSelectLead, stormMode, accent }) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col">
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
<div className="shrink-0 flex items-center gap-1 p-2 border-b border-zinc-100 dark:border-white/[0.06] bg-zinc-50/50 dark:bg-zinc-900/30">
|
<div className="shrink-0 flex items-center gap-1 px-2 py-1.5 border-b border-zinc-100 dark:border-white/[0.06] bg-zinc-50/50 dark:bg-zinc-900/30 overflow-x-auto scrollbar-none">
|
||||||
{TABS.map(tab => {
|
{TABS.map(tab => {
|
||||||
const isActive = activeTab === tab.id;
|
const isActive = activeTab === tab.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`relative flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-all duration-200 flex-1 justify-center
|
className={`relative flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-all duration-200 flex-1 shrink-0 justify-center
|
||||||
${isActive
|
${isActive
|
||||||
? 'bg-white dark:bg-zinc-700 shadow-sm text-zinc-900 dark:text-white'
|
? 'bg-white dark:bg-zinc-700 shadow-sm text-zinc-900 dark:text-white'
|
||||||
: 'text-zinc-400 dark:text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
|
: 'text-zinc-400 dark:text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
|
||||||
@@ -280,8 +284,8 @@ const DispatchLeadQueue = ({ selectedLead, onSelectLead, stormMode, accent }) =>
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lead list */}
|
{/* Lead list — fixed height: responsive per breakpoint */}
|
||||||
<div className="flex-1 overflow-y-auto p-2 space-y-1.5 custom-scrollbar">
|
<div className="overflow-y-auto p-1.5 space-y-1 custom-scrollbar h-[22rem] sm:h-[32rem] md:h-[44rem] lg:h-[54rem] xl:h-[72rem]">
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
{filteredLeads.length === 0 ? (
|
{filteredLeads.length === 0 ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
Reference in New Issue
Block a user