diff --git a/package.json b/package.json index c94e1ab..8512f4c 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,11 @@ "@mui/material": "^5.15.20", "@mui/x-date-pickers": "^6.20.2", "dayjs": "^1.11.11", + "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.23.1" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx index 32d0083..eda0fcf 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -30,7 +30,7 @@ export default function App() { import('@/pages/orders/OrdersList'))} /> import('@/pages/orders/CreateOrder'))} /> import('@/pages/orders/CreateMultipleOrders'))} /> - import('@/pages/orders/AssignOrders'))} /> + import('@/pages/dispatch/DispatchBoard'))} /> import('@/pages/orders/OrderDetails'))} /> import('@/pages/Deliveries'))} /> @@ -42,7 +42,8 @@ export default function App() { import('@/pages/hubs/HubNetwork'))} /> import('@/pages/fleet/Fleet'))} /> import('@/pages/dispatch/AiDispatch'))} /> - import('@/pages/tracking/LiveTracking'))} /> + import('@/pages/operations/DispatchTracking'))} /> + import('@/pages/tracking/LiveTracking'))} /> import('@/pages/tracking/ShipmentJourney'))} /> import('@/pages/tracking/ShipmentJourney'))} /> import('@/pages/analytics/Analytics'))} /> @@ -51,7 +52,7 @@ export default function App() { import('@/pages/tenants/Tenants'))} /> import('@/pages/tenants/CreateClient'))} /> - import('@/pages/customers/Customers'))} /> + import('@/pages/business/CustomersHub'))} /> import('@/pages/customers/CreateCustomer'))} /> import('@/pages/Pricing'))} /> @@ -60,12 +61,13 @@ export default function App() { import('@/pages/riders/CreateRider'))} /> import('@/pages/riders/EditRider'))} /> + import('@/pages/reports/ReportsHub'))} /> import('@/pages/reports/OrdersSummary'))} /> import('@/pages/reports/OrdersDetails'))} /> import('@/pages/reports/RidersSummary'))} /> import('@/pages/reports/RidersLogs'))} /> - import('@/pages/invoice/Invoices'))} /> + import('@/pages/business/FinanceHub'))} /> import('@/pages/invoice/InvoicePreview'))} /> import('@/pages/Profile'))} /> diff --git a/src/components/AiImpactSummary.jsx b/src/components/AiImpactSummary.jsx new file mode 100644 index 0000000..97c7d23 --- /dev/null +++ b/src/components/AiImpactSummary.jsx @@ -0,0 +1,50 @@ +import { Card, Grid, Stack, Box, Typography, Avatar } from '@mui/material'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; +import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined'; +import LocalGasStationOutlinedIcon from '@mui/icons-material/LocalGasStationOutlined'; +import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined'; +import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined'; + +import { aiImpact } from '@/data/mock'; + +// ==============================|| AI IMPACT ||============================== // +// What MileTruth AI delivered — six contribution metrics. Read-only, no action lists. + +const ITEMS = [ + { icon: RouteOutlinedIcon, value: `${aiImpact.routeSavings}%`, label: 'Route Savings', color: '#1D4ED8' }, + { icon: ScheduleOutlinedIcon, value: aiImpact.delaysPrevented, label: 'Delays Prevented', color: '#00A2AE' }, + { icon: TuneOutlinedIcon, value: aiImpact.optimizationActions, label: 'Optimization Actions', color: '#EA580C' }, + { icon: LocalGasStationOutlinedIcon, value: `${aiImpact.fuelSavedL.toLocaleString('en-IN')} L`, label: 'Fuel Saved', color: '#8A6500' }, + { icon: EnergySavingsLeafOutlinedIcon, value: `${(aiImpact.co2ReducedKg / 1000).toFixed(1)} t`, label: 'CO₂ Reduction', color: '#00A854' }, + { icon: SpeedOutlinedIcon, value: `${aiImpact.networkEfficiency}%`, label: 'Network Efficiency', color: '#7C3AED' } +]; + +export default function AiImpactSummary() { + return ( + + + + AI Impact + MileTruth AI · last 30 days + + + + {ITEMS.map((it) => ( + + + + + + + {it.value} + {it.label} + + + + ))} + + + ); +} diff --git a/src/components/AttentionCards.jsx b/src/components/AttentionCards.jsx new file mode 100644 index 0000000..b621395 --- /dev/null +++ b/src/components/AttentionCards.jsx @@ -0,0 +1,63 @@ +import { useNavigate } from 'react-router-dom'; +import { Grid, Card, Stack, Box, Typography, Avatar } from '@mui/material'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; +import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; +import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; +import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; + +import { attentionItems } from '@/data/mock'; + +// ==============================|| ATTENTION REQUIRED — DECISION CARDS ||============================== // +// Visual, single-click cards. Answers "what needs action now?" — no tables, no detail dumps. + +const ICON = { delay: ScheduleOutlinedIcon, alert: WarningAmberRoundedIcon, hub: HubOutlinedIcon, route: AltRouteOutlinedIcon }; +const SEV = { + high: { fg: '#A82216', bg: '#FEEAE9', label: 'High', accent: '#F04134' }, + medium: { fg: '#8A6500', bg: '#FFF7E0', label: 'Medium', accent: '#FFBF00' }, + low: { fg: '#00727B', bg: '#E0F7F8', label: 'Low', accent: '#00A2AE' } +}; + +export default function AttentionCards() { + const navigate = useNavigate(); + + return ( + + {attentionItems.map((it) => { + const Icon = ICON[it.icon] || WarningAmberRoundedIcon; + const sev = SEV[it.severity] || SEV.low; + return ( + + navigate(it.to)} + sx={{ + cursor: 'pointer', + height: '100%', + borderLeft: '3px solid', + borderLeftColor: sev.accent, + transition: 'box-shadow .15s ease, transform .15s ease', + '&:hover': { boxShadow: '0 4px 16px rgba(16,24,40,0.10)', transform: 'translateY(-1px)' } + }} + > + + + + + + + {it.count} + {sev.label} + + + {it.label} + + + + + + + ); + })} + + ); +} diff --git a/src/components/DateRangeFilter.jsx b/src/components/DateRangeFilter.jsx new file mode 100644 index 0000000..c56da9e --- /dev/null +++ b/src/components/DateRangeFilter.jsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { Button, Menu, MenuItem, Divider, Stack } from '@mui/material'; +import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import dayjs from 'dayjs'; + +import FormDialog from '@/components/FormDialog'; +import { useFilters, buildRange, customRange, DATA_TODAY } from '@/store/Filters'; + +// ==============================|| DATE RANGE FILTER ||============================== // +// A real, functional range picker bound to the global filter store. Presets + custom range; +// every report that renders it shares (and persists) the same selection. + +const PRESETS = [ + ['today', 'Today'], + ['yesterday', 'Yesterday'], + ['last7', 'Last 7 Days'], + ['last30', 'Last 30 Days'], + ['thisMonth', 'This Month'], + ['prevMonth', 'Previous Month'] +]; + +export default function DateRangeFilter() { + const { range, setRange } = useFilters(); + const [anchor, setAnchor] = useState(null); + const [customOpen, setCustomOpen] = useState(false); + const [from, setFrom] = useState(range.start || DATA_TODAY.subtract(6, 'day')); + const [to, setTo] = useState(range.end || DATA_TODAY); + + const pick = (key) => { setRange(buildRange(key)); setAnchor(null); }; + const openCustom = () => { setAnchor(null); setFrom(range.start || DATA_TODAY.subtract(6, 'day')); setTo(range.end || DATA_TODAY); setCustomOpen(true); }; + const applyCustom = () => { if (from && to) setRange(customRange(dayjs(from), dayjs(to))); setCustomOpen(false); }; + + return ( + <> + + setAnchor(null)} PaperProps={{ sx: { minWidth: 180 } }}> + {PRESETS.map(([k, l]) => ( + pick(k)}>{l} + ))} + + Custom Range… + + + setCustomOpen(false)} title="Custom Date Range" onSubmit={applyCustom} submitLabel="Apply" maxWidth="xs"> + + + + + + + ); +} diff --git a/src/components/FilterSummary.jsx b/src/components/FilterSummary.jsx new file mode 100644 index 0000000..dc31fd2 --- /dev/null +++ b/src/components/FilterSummary.jsx @@ -0,0 +1,22 @@ +import { Stack, Typography, Chip } from '@mui/material'; + +import { useFilters } from '@/store/Filters'; + +// ==============================|| FILTER SUMMARY ||============================== // +// "Showing data for: · Location: · records" — confirms exactly which +// filters are active so the data on screen is never ambiguous. + +export default function FilterSummary({ count }) { + const { range, location } = useFilters(); + return ( + + Showing data for + + · Location + + {typeof count === 'number' && ( + · {count} record{count === 1 ? '' : 's'} + )} + + ); +} diff --git a/src/components/KpiCard.jsx b/src/components/KpiCard.jsx new file mode 100644 index 0000000..ad96821 --- /dev/null +++ b/src/components/KpiCard.jsx @@ -0,0 +1,72 @@ +import { Card, CardContent, Box, Typography, Avatar, Stack, Divider } from '@mui/material'; +import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; +import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; + +// ==============================|| EXECUTIVE KPI CARD ||============================== // +// Taller operational KPI tile: headline metric + trend + an AI insight line, plus a secondary +// breakdown strip so each card carries real context. `details` = [{ label, value }]. + +export default function KpiCard({ title, value, icon: Icon, trend, comparison, insight, details = [] }) { + const trendUp = typeof trend === 'number' ? trend >= 0 : null; + + return ( + + + + + + {title} + + + {value} + + {(trendUp !== null || comparison) && ( + + {trendUp !== null && ( + + {trendUp ? : } + {Math.abs(trend)}% + + )} + {comparison && {comparison}} + + )} + + {Icon && ( + + + + )} + + + {insight && ( + + + {insight} + + )} + + {details.length > 0 && ( + <> + + + }> + {details.map((d) => ( + + {d.value} + {d.label} + + ))} + + + )} + + + ); +} diff --git a/src/components/KpiStrip.jsx b/src/components/KpiStrip.jsx new file mode 100644 index 0000000..2b8bfc6 --- /dev/null +++ b/src/components/KpiStrip.jsx @@ -0,0 +1,57 @@ +import { Card, Box, Stack, Typography, Divider } from '@mui/material'; + +// ==============================|| KPI STRIP ||============================== // +// Operational KPI row shared across dashboard & table pages (Orders, Deliveries…). +// Premium analytics styling: tall cells, prominent values, subtle separators + hover. +// items: [{ label, value, color, icon }]. + +export default function KpiStrip({ items = [], sx }) { + return ( + + } + sx={{ overflowX: 'auto', flexWrap: { xs: 'wrap', lg: 'nowrap' } }} + > + {items.map((it) => { + const Icon = it.icon; + // Keep short metrics at full prominence; step long values (e.g. ₹3,84,200) down so + // nothing truncates while six cards share one full-width row on laptop screens. + const len = String(it.value).length; + const valueSize = len <= 5 ? '2.125rem' : len <= 7 ? '1.85rem' : '1.6rem'; + return ( + + + + {it.label} + + {Icon && ( + + + + )} + + + {it.value} + + + ); + })} + + + ); +} diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx index 723d4bd..7c3afec 100644 --- a/src/components/Logo.jsx +++ b/src/components/Logo.jsx @@ -1,11 +1,13 @@ import { Box, Typography } from '@mui/material'; // ==============================|| DOORMILE WORDMARK LOGO ||============================== // -// Uses the brand wordmark asset (white PNG). `onDark` shows it as-is on dark/red -// surfaces; on light surfaces it is recoloured to near-black. `compact` (e.g. the +// The asset is the Doormile wordmark as a transparent-background mask. We paint it with the +// brand red on light surfaces and white on dark/red surfaces using a CSS mask, so a single +// asset renders crisply on both the white header and the red sidebar. `compact` (e.g. the // collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit. const LOGO_SRC = '/Doormile-logo.png'; +const ASPECT = 748 / 100; // intrinsic ratio of the wordmark asset export default function Logo({ onDark = false, compact = false, height = 26, sx }) { if (compact) { @@ -35,15 +37,21 @@ export default function Logo({ onDark = false, compact = false, height = 26, sx return ( diff --git a/src/components/NetworkFlow.jsx b/src/components/NetworkFlow.jsx new file mode 100644 index 0000000..885534f --- /dev/null +++ b/src/components/NetworkFlow.jsx @@ -0,0 +1,91 @@ +import { Card, Box, Stack, Typography, LinearProgress } from '@mui/material'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; + +import { threeMile } from '@/data/mock'; + +// ==============================|| OPERATIONS LAYER — FIRST → MID → LAST ||============================== // +// Each stage: throughput · utilization · exception count · bottleneck · AI recommendation. + +const statusFor = (onTime) => + onTime >= 98 ? { label: 'On Track', color: 'success.main' } : onTime >= 95 ? { label: 'Watch', color: 'warning.main' } : { label: 'At Risk', color: 'error.main' }; + +function FlowNode({ stage }) { + const status = statusFor(stage.onTime); + const utilColor = stage.utilization > 85 ? 'error' : stage.utilization > 70 ? 'warning' : 'success'; + return ( + + + + {stage.title} + {stage.subtitle} + + + + {status.label} + + + + + + {stage.metric} + {stage.metricLabel} + + + {stage.throughput.toLocaleString('en-IN')} + throughput + + + 0 ? 'error.main' : 'success.main' }}> + + {stage.exceptions} + + exceptions + + + + {/* utilization */} + + + Utilization + {stage.utilization}% + + + + + {/* bottleneck */} + + + + {stage.bottleneck ? `Bottleneck · ${stage.bottleneck}` : 'No active bottleneck'} + + + + {/* AI recommendation */} + + + {stage.aiRec} + + + ); +} + +export default function NetworkFlow() { + return ( + + {threeMile.map((stage, i) => ( + + + + + {i < threeMile.length - 1 && ( + + + + )} + + ))} + + ); +} diff --git a/src/components/PageHeader.jsx b/src/components/PageHeader.jsx index 2cc3a61..dbd3979 100644 --- a/src/components/PageHeader.jsx +++ b/src/components/PageHeader.jsx @@ -19,9 +19,6 @@ export default function PageHeader({ title, breadcrumbs = [], action }) { {breadcrumbs.length > 0 && ( } sx={{ mt: 0.5 }}> - - Home - {breadcrumbs.map((b, i) => b.to && i < breadcrumbs.length - 1 ? ( diff --git a/src/components/PageToolbar.jsx b/src/components/PageToolbar.jsx new file mode 100644 index 0000000..0c683a9 --- /dev/null +++ b/src/components/PageToolbar.jsx @@ -0,0 +1,46 @@ +import { Stack, TextField, MenuItem, InputAdornment, Box } from '@mui/material'; +import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; + +// ==============================|| PAGE TOOLBAR ||============================== // +// Dense search + filters + actions row for table pages. filters: [{ value, onChange, +// label, options:[{value,label}] }]. `search`/`onSearch` wire the search box; `actions` +// renders on the right. + +export default function PageToolbar({ search, onSearch, searchPlaceholder = 'Search…', filters = [], actions, sx }) { + return ( + + {onSearch && ( + onSearch(e.target.value)} + placeholder={searchPlaceholder} + sx={{ minWidth: { xs: '100%', md: 280 } }} + InputProps={{ startAdornment: }} + /> + )} + {filters.map((f) => ( + f.onChange(e.target.value)} + sx={{ minWidth: { xs: '100%', md: f.width || 160 } }} + > + {f.options.map((o) => ( + {o.label} + ))} + + ))} + + {actions} + + ); +} diff --git a/src/components/ProcessTracker.jsx b/src/components/ProcessTracker.jsx new file mode 100644 index 0000000..17233f0 --- /dev/null +++ b/src/components/ProcessTracker.jsx @@ -0,0 +1,85 @@ +import { Card, Box, Stack, Typography, LinearProgress } from '@mui/material'; +import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; +import VerifiedUserOutlinedIcon from '@mui/icons-material/VerifiedUserOutlined'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; +import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; +import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; +import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; + +import { commandPipeline } from '@/data/mock'; + +// ==============================|| END-TO-END PROCESS TRACKER ||============================== // +// Premium vertical timeline: connected progress rail, status-coloured nodes, completion %, +// KPI badges, hover interactions. + +const ICON = { + order: ReceiptLongOutlinedIcon, trust: VerifiedUserOutlinedIcon, hub: HubOutlinedIcon, fleet: LocalShippingOutlinedIcon, + ai: AutoAwesomeOutlinedIcon, rider: TwoWheelerOutlinedIcon, track: MyLocationOutlinedIcon, done: CheckRoundedIcon +}; +const TONE = { + done: { node: '#00A854', ring: 'rgba(0,168,84,0.16)', bar: 'success' }, + active: { node: '#C01227', ring: 'rgba(192,18,39,0.16)', bar: 'primary' }, + pending: { node: '#C7CDD6', ring: 'rgba(140,148,162,0.16)', bar: 'inherit' } +}; + +export default function ProcessTracker({ steps = commandPipeline, title = 'End-to-End Flow' }) { + return ( + + + {title} + + + LIVE + + + + + {steps.map((s, i) => { + const Icon = s.status === 'done' ? CheckRoundedIcon : ICON[s.icon] || ReceiptLongOutlinedIcon; + const tone = TONE[s.status] || TONE.pending; + const last = i === steps.length - 1; + const nextDone = !last && steps[i + 1] && steps[i].status === 'done'; + return ( + + {/* rail */} + + + + + {!last && } + + + {/* content */} + + + {s.label} + + {s.count} + {s.sub} + + + + + {s.pct}% + + + + ); + })} + + + ); +} diff --git a/src/components/TabbedWorkspace.jsx b/src/components/TabbedWorkspace.jsx new file mode 100644 index 0000000..a2c4654 --- /dev/null +++ b/src/components/TabbedWorkspace.jsx @@ -0,0 +1,41 @@ +import { useSearchParams } from 'react-router-dom'; +import { Box, Tabs, Tab } from '@mui/material'; + +// ==============================|| TABBED WORKSPACE ||============================== // +// Merges several existing pages into one workspace under a single nav item. The active tab is +// synced to the `?tab=` query param (deep-linkable, keeps the sidebar item active) and only the +// active panel is mounted — so heavy panels (e.g. the live map) don't run in the background. +// tabs: [{ key, label, element }]. + +export default function TabbedWorkspace({ tabs }) { + const [params, setParams] = useSearchParams(); + const keys = tabs.map((t) => t.key); + const idx = Math.max(0, keys.indexOf(params.get('tab'))); + + const setIdx = (i) => + setParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set('tab', keys[i]); + return next; + }, + { replace: true } + ); + + return ( + + setIdx(v)} + variant="scrollable" + scrollButtons="auto" + sx={{ mb: 2, borderBottom: 1, borderColor: 'divider', '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }} + > + {tabs.map((t) => ( + + ))} + + {tabs[idx].element} + + ); +} diff --git a/src/components/tracking/ActiveRidersList.jsx b/src/components/tracking/ActiveRidersList.jsx new file mode 100644 index 0000000..48e630d --- /dev/null +++ b/src/components/tracking/ActiveRidersList.jsx @@ -0,0 +1,118 @@ +import { useMemo, useState } from 'react'; +import { Card, Box, Stack, Typography, Chip, LinearProgress } from '@mui/material'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; + +import './tracking.css'; +import { vehicleTypes, vehicleIconComponents } from './vehicleMarker'; + +// ==============================|| ACTIVE RIDERS LIST ||============================== // +// Right-rail fleet roster — every active rider in one compact, filterable, scrollable list so a +// dispatcher can switch focus instantly without leaving the map. Selecting drives map + timeline. + +const liveOf = (d) => (d.status === 'Delivered' ? 'Completed' : d.etaStatus !== 'on-time' ? 'Delayed' : 'Live'); +const PILL = { + Live: { bg: '#E3F6EC', fg: '#00773B' }, + Delayed: { bg: '#FEEAE9', fg: '#A82216' }, + Completed: { bg: '#EEF1F5', fg: '#475569' } +}; + +const FILTERS = [ + { key: 'all', label: 'All', test: () => true }, + { key: 'live', label: 'Live', test: (d) => liveOf(d) === 'Live' }, + { key: 'delayed', label: 'Delayed', test: (d) => liveOf(d) === 'Delayed' }, + { key: 'completed', label: 'Completed', test: (d) => liveOf(d) === 'Completed' }, + { key: 'priority', label: 'High Priority', test: (d) => d.priority === 'high' || d.priority === 'express' } +]; + +function RiderCard({ d, selected, onSelect }) { + const vt = vehicleTypes[d.vehicle] || vehicleTypes.Bike; + const Glyph = vehicleIconComponents[d.vehicle]; + const state = liveOf(d); + const pill = PILL[state]; + const barColor = state === 'Completed' ? 'success' : state === 'Delayed' ? 'error' : 'info'; + + return ( + onSelect(d.id)} + sx={{ + p: 1.25, + borderRadius: 2, + cursor: 'pointer', + border: '1px solid', + borderColor: selected ? 'primary.main' : 'grey.200', + bgcolor: selected ? 'rgba(192,18,39,0.04)' : 'background.paper', + transition: 'border-color .12s, background .12s', + '&:hover': { borderColor: 'primary.light', bgcolor: 'grey.50' } + }} + > + + + {Glyph ? : null} + + + + {d.id} + {state} + + {d.rider} · {d.vehicle} + + + + + + {d.progress}% + + + {d.eta} + + + + ); +} + +export default function ActiveRidersList({ deliveries, selectedId, onSelect }) { + const [filter, setFilter] = useState('all'); + const active = FILTERS.find((f) => f.key === filter) || FILTERS[0]; + const rows = useMemo( + () => deliveries.filter(active.test).sort((a, b) => Number(liveOf(b) === 'Delayed') - Number(liveOf(a) === 'Delayed')), + [deliveries, active] + ); + const count = (f) => deliveries.filter(f.test).length; + + return ( + + + + Active Riders + {rows.length} shown + + + {FILTERS.map((f) => ( + setFilter(f.key)} + variant={filter === f.key ? 'filled' : 'outlined'} + sx={{ + height: 24, fontWeight: 600, cursor: 'pointer', + ...(filter === f.key + ? { bgcolor: 'primary.main', color: '#fff', '&:hover': { bgcolor: 'primary.dark' } } + : { borderColor: 'grey.300', color: 'grey.600' }) + }} + /> + ))} + + + + + + {rows.map((d) => ( + + ))} + {rows.length === 0 && No riders match this filter.} + + + + ); +} diff --git a/src/components/tracking/DeliveryQueue.jsx b/src/components/tracking/DeliveryQueue.jsx new file mode 100644 index 0000000..f35308c --- /dev/null +++ b/src/components/tracking/DeliveryQueue.jsx @@ -0,0 +1,188 @@ +import { useMemo, useState } from 'react'; +import { Card, Box, Stack, Typography, Tabs, Tab, TextField, InputAdornment, LinearProgress, Chip, MenuItem, IconButton, Button, Tooltip } from '@mui/material'; +import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined'; +import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; + +import './tracking.css'; +import StatusChip from '@/components/StatusChip'; +import { vehicleTypes } from '@/data/mock'; +import { vehicleIconComponents } from './vehicleMarker'; + +// ==============================|| OPERATIONAL DELIVERY QUEUE ||============================== // +// Left panel of the control tower: tabbed, searchable, city-filterable list of shipment cards. +// Selecting drives the map; per-card quick actions (Flag / Open 360) commit via `actions`. + +const ETA_TONE = { 'on-time': '#00773B', 'at-risk': '#8A6500', delayed: '#A82216' }; +const PRIORITY = { high: { fg: '#A82216', bg: '#FEEAE9' }, express: { fg: '#8A6500', bg: '#FFF7E0' }, standard: { fg: '#595959', bg: '#F0F0F0' } }; + +const matches = (d, q) => + [d.id, d.rider, d.vehicle, d.origin, d.destination, d.city].some((f) => f.toLowerCase().includes(q)); + +function DeliveryCard({ d, selected, onSelect, actions }) { + const tone = ETA_TONE[d.etaStatus] || ETA_TONE['on-time']; + const vt = vehicleTypes[d.vehicle] || vehicleTypes.Bike; + const pr = PRIORITY[d.priority] || PRIORITY.standard; + const live = d.status !== 'Delivered'; + const stop = (fn) => (e) => { e.stopPropagation(); fn(); }; + + return ( + onSelect(d.id)} + sx={{ + p: 1.5, + borderRadius: 2, + cursor: 'pointer', + border: '1px solid', + borderColor: selected ? 'primary.main' : 'grey.200', + borderLeft: '4px solid', + borderLeftColor: d.etaStatus === 'on-time' ? 'transparent' : tone, + bgcolor: selected ? 'rgba(192,18,39,0.03)' : 'background.paper', + transition: 'border-color .15s, background .15s', + '&:hover': { borderColor: 'primary.light' } + }} + > + + + + {(() => { const G = vehicleIconComponents[d.vehicle]; return G ? : null; })()} + + + {d.id} + {d.rider} · {d.vehicle} + + + + + {d.priority} + + {live && ( + } + label="Live" + sx={{ height: 20, bgcolor: 'success.lighter', color: 'success.dark', fontWeight: 700, '& .MuiChip-label': { px: 0.75, fontSize: 11 } }} + /> + )} + + + + + {d.origin} + + {d.destination} + + + + + {d.progress}% + + + + + + + + {d.status === 'Delivered' ? `Delivered ${d.eta}` : `ETA ${d.eta}`} + {d.etaStatus !== 'on-time' && ` · +${d.delayMin}m`} + + + + + {/* quick actions */} + {actions && ( + + + actions.flag(d.id))} sx={{ p: 0.5 }}> + + + + + + )} + + ); +} + +const TABS = [ + { key: 'active', label: 'Active', test: (d) => d.status !== 'Delivered' }, + { key: 'delayed', label: 'Delayed', test: (d) => d.etaStatus !== 'on-time' }, + { key: 'completed', label: 'Completed', test: (d) => d.status === 'Delivered' }, + { key: 'all', label: 'All', test: () => true } +]; + +export default function DeliveryQueue({ deliveries, selectedId, onSelect, actions }) { + const [tab, setTab] = useState(0); + const [q, setQ] = useState(''); + const [city, setCity] = useState('all'); + + const cities = useMemo(() => [...new Set(deliveries.map((d) => d.city))], [deliveries]); + + const rows = useMemo(() => { + const query = q.trim().toLowerCase(); + return deliveries + .filter(TABS[tab].test) + .filter((d) => (city === 'all' ? true : d.city === city)) + .filter((d) => (query ? matches(d, query) : true)) + .sort((a, b) => Number(b.etaStatus !== 'on-time') - Number(a.etaStatus !== 'on-time')); + }, [deliveries, tab, q, city]); + + const counts = useMemo(() => TABS.map((t) => deliveries.filter(t.test).length), [deliveries]); + + return ( + + + + Active Deliveries + {rows.length} shown + + + setQ(e.target.value)} + InputProps={{ startAdornment: () }} + /> + setCity(e.target.value)} sx={{ minWidth: 120 }}> + All cities + {cities.map((c) => {c})} + + + + + setTab(v)} + variant="fullWidth" + sx={{ px: 1, minHeight: 40, '& .MuiTab-root': { minHeight: 40, textTransform: 'none', fontWeight: 600, fontSize: 13 } }} + > + {TABS.map((t, i) => ( + + ))} + + + + + {rows.map((d) => ( + + ))} + {rows.length === 0 && ( + + No deliveries match this view. + + )} + + + + ); +} diff --git a/src/components/tracking/FleetMap.jsx b/src/components/tracking/FleetMap.jsx new file mode 100644 index 0000000..fc244b4 --- /dev/null +++ b/src/components/tracking/FleetMap.jsx @@ -0,0 +1,366 @@ +import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet'; +import L from 'leaflet'; +import 'leaflet.markercluster'; +import { Box, Card, Stack, Typography, Button, IconButton, Tooltip, LinearProgress, Collapse } from '@mui/material'; +import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import CallOutlinedIcon from '@mui/icons-material/CallOutlined'; +import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded'; +import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; +import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; +import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded'; + +import 'leaflet/dist/leaflet.css'; +import 'leaflet.markercluster/dist/MarkerCluster.css'; +import './tracking.css'; + +import { vehicleIcon, endpointIcon, clusterIcon, vehicleIconComponents } from './vehicleMarker'; +import { pointAlongRoute } from '@/utils/geo'; +import { vehicleTypes, fleetTotals, cityCenters, networkCorridors } from '@/data/mock'; + +const ll = (p) => (Array.isArray(p) ? p : [p.lat, p.lng]); + +// ---- inter-city corridors: gentle arcs between the three metros + active-load chips ---- +function arc(a, b, bend = 0.16) { + const dx = b[0] - a[0]; + const dy = b[1] - a[1]; + const ctrl = [(a[0] + b[0]) / 2 - dy * bend, (a[1] + b[1]) / 2 + dx * bend]; + const pts = []; + for (let i = 0; i <= 18; i += 1) { + const t = i / 18; + const u = 1 - t; + pts.push([u * u * a[0] + 2 * u * t * ctrl[0] + t * t * b[0], u * u * a[1] + 2 * u * t * ctrl[1] + t * t * b[1]]); + } + const mid = [0.25 * a[0] + 0.5 * ctrl[0] + 0.25 * b[0], 0.25 * a[1] + 0.5 * ctrl[1] + 0.25 * b[1]]; + return { pts, mid }; +} + +const laneChip = (count, from, to) => + L.divIcon({ + html: `
${count}${from}–${to}
`, + className: 'lane-chip-divicon', + iconSize: [0, 0] + }); + +// Precomputed once (cities & counts are static) so the live re-render never recomputes geometry. +const CORRIDORS = networkCorridors.map((c) => { + const { pts, mid } = arc([cityCenters[c.from].lat, cityCenters[c.from].lng], [cityCenters[c.to].lat, cityCenters[c.to].lng]); + return { ...c, pts, mid, chip: laneChip(c.active, c.from.slice(0, 3).toUpperCase(), c.to.slice(0, 3).toUpperCase()) }; +}); + +function NetworkCorridors() { + return ( + <> + {CORRIDORS.map((c) => ( + + + + + ))} + + ); +} + +// ---- ambient fleet rendered through a Leaflet marker-cluster group ---- +function FleetClusters({ vehicles }) { + const map = useMap(); + useEffect(() => { + const group = L.markerClusterGroup({ + iconCreateFunction: (c) => clusterIcon(c.getChildCount()), + showCoverageOnHover: false, + maxClusterRadius: 55, + spiderfyOnMaxZoom: true + }); + vehicles.forEach((v) => { + const m = L.marker([v.lat, v.lng], { icon: vehicleIcon({ type: v.type, bearing: v.bearing }) }); + m.bindPopup(`${v.id}
${v.type} · ${v.rider}
${v.status === 'idle' ? 'Idle' : 'On trip'}`); + group.addLayer(m); + }); + map.addLayer(group); + return () => map.removeLayer(group); + }, [map, vehicles]); + return null; +} + +// ---- imperative effects: size correctly, frame the three cities, follow the selection ---- +// Refit only when the *selection* changes (not on every live-progress tick) so the map +// doesn't constantly re-zoom while vehicles move. +function MapEffects({ selectedId, selectedRoute, selectedPoint, homeBounds }) { + const map = useMap(); + const routeRef = useRef(selectedRoute); + routeRef.current = selectedRoute; + const pointRef = useRef(selectedPoint); + pointRef.current = selectedPoint; + const prevSel = useRef(null); + const mounted = useRef(false); + useEffect(() => { + // The map mounts inside a flex container that may size after init — settle the size, + // then perform a single gentle onboarding glide from the wide framing into the network. + map.invalidateSize(); + map.flyToBounds(homeBounds, { padding: [50, 50], duration: 1.6, easeLinearity: 0.25 }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { + if (!mounted.current) { mounted.current = true; prevSel.current = selectedId; return; } + map.invalidateSize(); + if (selectedId) { + if (prevSel.current && prevSel.current !== selectedId && pointRef.current) { + // switching between riders → keep the current zoom, just recenter (fleet-monitor UX) + map.setView(pointRef.current, map.getZoom(), { animate: true }); + } else if (routeRef.current?.length) { + // first focus from the network view → frame the route once + map.fitBounds(routeRef.current, { padding: [60, 60], maxZoom: 14, duration: 0.8 }); + } + } else { + map.flyToBounds(homeBounds, { padding: [50, 50], duration: 0.9 }); + } + prevSel.current = selectedId; + }, [selectedId, map]); + return null; +} + +export default function FleetMap({ vehicles, deliveries, selectedId, onSelect, lastUpdated, actions }) { + const [map, setMap] = useState(null); + const [expanded, setExpanded] = useState(false); + useEffect(() => { setExpanded(false); }, [selectedId]); // new rider opens compact + + const tracked = useMemo(() => deliveries.filter((d) => d.route?.length), [deliveries]); + const selected = useMemo(() => deliveries.find((d) => d.id === selectedId) || null, [deliveries, selectedId]); + const focus = Boolean(selected); // Selected Tracking Mode + const selPos = useMemo(() => (selected ? pointAlongRoute(selected.route, selected.progressExact ?? selected.progress) : null), [selected]); + + // Cache vehicle icons by a coarse signature so the live animation only calls setLatLng + // (smooth glide) instead of rebuilding the marker DOM every frame. Bearing is bucketed so + // the icon stays stable along straight stretches. + const iconCache = useRef(new Map()); + const getVehicleIcon = (d, bearing, isSel) => { + const delayed = d.etaStatus !== 'on-time'; + const bucket = Math.round(bearing / 6) * 6; + const key = `${d.vehicle}|${isSel}|${delayed}|${bucket}`; + let icon = iconCache.current.get(key); + if (!icon) { + icon = vehicleIcon({ type: d.vehicle, bearing: bucket, selected: isSel, delayed }); + iconCache.current.set(key, icon); + } + return icon; + }; + + // The service area is exactly the three cities — used for the initial fit, the recenter + // control and the pan limits, so the operator never loses context. + const homeBounds = useMemo(() => L.latLngBounds(Object.values(cityCenters).map((c) => [c.lat, c.lng])), []); + const introBounds = useMemo(() => homeBounds.pad(0.35), [homeBounds]); // slightly wider first frame for the glide + const maxBounds = useMemo(() => homeBounds.pad(0.5), [homeBounds]); + + const recenter = () => map && map.fitBounds(homeBounds, { padding: [50, 50] }); + const trackRider = () => { if (map && selPos?.point) map.flyTo(selPos.point, Math.max(map.getZoom(), 14), { duration: 0.8 }); }; + + const legend = [ + { label: 'Bike', type: 'Bike', color: vehicleTypes.Bike.color, count: fleetTotals.Bike }, + { label: 'Auto', type: 'Auto', color: vehicleTypes.Auto.color, count: fleetTotals.Auto }, + { label: 'Truck', type: 'Truck', color: vehicleTypes.Truck.color, count: fleetTotals.Truck }, + { label: 'Van', type: 'Van', color: vehicleTypes.Van.color, count: fleetTotals.Van }, + { label: 'Delayed', color: '#F04134', count: fleetTotals.delayed } + ]; + + return ( + + + + + {/* Selected Tracking Mode: hide the ambient network so only the chosen shipment remains */} + {!focus && } + {!focus && } + + {/* selected route: white casing → completed (green) under remaining (blue), clean round caps */} + {selected && selPos && ( + <> + + + + + + + )} + + {/* tracked shipment vehicles (always individually visible & clickable) */} + {tracked.map((d) => { + const pos = pointAlongRoute(d.route, d.progressExact ?? d.progress); + return ( + onSelect(d.id) }} + zIndexOffset={d.id === selectedId ? 1000 : 500} + /> + ); + })} + + + {/* live status — top-left */} + + + LIVE + · updated {lastUpdated} + + + {/* recenter control — top-right (below default zoom on left) */} + + + + + + + {/* legend — bottom-right (hidden in Selected Tracking Mode to reduce clutter) */} + {!focus && ( + + Fleet on map + + {legend.map((l) => { + const Glyph = l.type ? vehicleIconComponents[l.type] : null; + return ( + + + {Glyph ? ( + + ) : ( + + )} + + {l.label} + {l.count} + + ); + })} + + + )} + + {/* compact, expandable tracking card — top-left, never covers the route */} + {selected && ( + + + {/* identity row */} + + + {(() => { const G = vehicleIconComponents[selected.vehicle]; return G ? : null; })()} + + + + {selected.id} + + + {selected.rider} · {selected.vehicle} + + onSelect(null)} sx={{ m: -0.5, flexShrink: 0 }} aria-label="Close"> + + + {/* compact metrics */} + + {[ + { l: 'Progress', v: `${selected.progress}%`, c: 'grey.900' }, + { l: 'ETA', v: selected.eta, c: 'grey.900' }, + { l: 'Delay', v: selected.etaStatus === 'on-time' ? 'On time' : `+${selected.delayMin} min`, c: selected.etaStatus === 'on-time' ? 'success.dark' : 'error.dark' } + ].map((m) => ( + + {m.l} + {m.v} + + ))} + + + {/* expanded detail */} + + + + {selected.origin} + + {selected.destination} + + + + + Last update · {lastUpdated} + + + + + actions?.call(selected)} sx={{ border: '1px solid', borderColor: 'grey.300' }}> + + + actions?.message(selected)} sx={{ border: '1px solid', borderColor: 'grey.300' }}> + + + + + + + + + + + + {/* expand / collapse toggle */} + + + + )} + + ); +} diff --git a/src/components/tracking/MiniMap.jsx b/src/components/tracking/MiniMap.jsx new file mode 100644 index 0000000..a5d498f --- /dev/null +++ b/src/components/tracking/MiniMap.jsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; +import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet'; +import L from 'leaflet'; +import { Box } from '@mui/material'; + +import 'leaflet/dist/leaflet.css'; +import './tracking.css'; +import { vehicleIcon, endpointIcon } from './vehicleMarker'; +import { pointAlongRoute } from '@/utils/geo'; + +// ==============================|| MINI MAP — single-shipment route ||============================== // +// A focused, non-clustered real map for one shipment: route (completed/remaining), pickup/drop pins, +// and the live vehicle marker placed from `progress`. Reuses the same Leaflet layer as FleetMap so the +// fake `MapPlaceholder` grid can be retired wherever a real per-shipment map is needed (Shipment 360 …). + +const ll = (p) => (Array.isArray(p) ? p : [p.lat, p.lng]); + +function Fit({ route }) { + const map = useMap(); + useEffect(() => { + map.invalidateSize(); + if (route?.length) map.fitBounds(route, { padding: [30, 30], maxZoom: 14 }); + }, [map, route]); + return null; +} + +export default function MiniMap({ route = [], progress = 0, vehicle = 'Bike', pickup, drop, height = 300 }) { + const hasRoute = route && route.length > 0; + const pos = hasRoute ? pointAlongRoute(route, progress) : null; + const center = pos ? pos.point : [12.9716, 77.5946]; + + return ( + + + + + {pos && ( + <> + + + + + )} + {pickup && } + {drop && } + + + ); +} diff --git a/src/components/tracking/RiderTimeline.jsx b/src/components/tracking/RiderTimeline.jsx new file mode 100644 index 0000000..fe0983a --- /dev/null +++ b/src/components/tracking/RiderTimeline.jsx @@ -0,0 +1,111 @@ +import { Box, Card, Stack, Typography, IconButton, Divider } from '@mui/material'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; +import PriorityHighRoundedIcon from '@mui/icons-material/PriorityHighRounded'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; + +import './tracking.css'; +import StatusChip from '@/components/StatusChip'; +import { vehicleTypes } from '@/data/mock'; +import { vehicleIconComponents } from './vehicleMarker'; + +// ==============================|| RIDER TIMELINE ||============================== // +// Replaces the generic activity feed in Selected Tracking Mode — a focused, Uber/Swiggy-style +// lifecycle for one shipment, derived live from its progress and delay state. + +function buildStages(d) { + const p = d.progress; + const delayed = d.etaStatus !== 'on-time'; + const stages = [ + { key: 'assigned', label: 'Shipment Assigned', time: '10:02 AM', done: true }, + { key: 'picked', label: 'Picked Up', time: '10:11 AM', done: p >= 5 }, + { key: 'checkpoint', label: 'Reached Checkpoint', time: '10:29 AM', done: p >= 34 } + ]; + if (delayed) { + stages.push({ key: 'delay', label: 'Delay Detected', time: `+${d.delayMin} min vs SLA`, done: p >= 38, warn: true }); + } + stages.push({ key: 'approaching', label: 'Approaching Destination', time: p >= 85 ? 'Arriving now' : `Est. ${d.eta}`, done: p >= 85 }); + stages.push({ key: 'delivered', label: 'Delivered', time: d.status === 'Delivered' ? d.eta : `ETA ${d.eta}`, done: p >= 100 }); + return stages; +} + +export default function RiderTimeline({ delivery: d, onClose }) { + if (!d) return null; + const stages = buildStages(d); + const activeIdx = stages.findIndex((s) => !s.done); // first incomplete = current step + const Glyph = vehicleIconComponents[d.vehicle]; + const vc = (vehicleTypes[d.vehicle] || {}).color; + + return ( + + {/* header */} + + + Rider Timeline + + + + + + {Glyph ? : null} + + + {d.id} + {d.rider} · {d.vehicle} + + + + + {d.origin} + + {d.destination} + + + + + + {d.progress}% · ETA {d.eta}{d.etaStatus !== 'on-time' ? ` · +${d.delayMin}m` : ''} + + + + + + + {/* timeline */} + + {stages.map((s, i) => { + const active = i === activeIdx; + const color = s.warn ? '#B45309' : s.done ? '#16A34A' : active ? '#2563EB' : '#9AA4B2'; + const last = i === stages.length - 1; + return ( + + + + {s.done ? : s.warn ? : } + + {!last && } + + + + + {s.label} + + + {active && !s.warn ? `In progress · ${s.time}` : s.time} + + + + ); + })} + + + ); +} diff --git a/src/components/tracking/TrackingControlBar.jsx b/src/components/tracking/TrackingControlBar.jsx new file mode 100644 index 0000000..31efc68 --- /dev/null +++ b/src/components/tracking/TrackingControlBar.jsx @@ -0,0 +1,77 @@ +import { Card, Box, Stack, Typography, Divider } from '@mui/material'; + +import './tracking.css'; +import { executionOps } from '@/data/mock'; + +// ==============================|| TRACKING CONTROL BAR ||============================== // +// Operational KPI strip for the control tower — roomy cells with prominent values, live +// indicators and alert tones. Single row on desktop, wraps on small screens. + +function Kpi({ glyph, label, value, color, pulse }) { + return ( + + {pulse ? ( + + ) : ( + {glyph} + )} + + + {label} + + + {value} + + + + ); +} + +export default function TrackingControlBar() { + const o = executionOps; + const items = [ + { label: 'Active Deliveries', value: o.activeDeliveries, color: '#00A854', pulse: true }, + { label: 'Delayed', value: o.delayed, color: '#F04134', pulse: true }, + { label: 'On-Time Rate', value: `${o.onTime}%`, color: '#00773B', glyph: '🟢' }, + { label: 'Active Riders', value: o.activeRiders, glyph: '🛵' }, + { label: 'Re-routes Today', value: o.reroutes, glyph: '🔄' }, + { label: 'Critical Alerts', value: o.criticalAlerts, color: '#B38600', glyph: '⚠️' } + ]; + + return ( + + } + sx={{ overflowX: 'auto', flexWrap: { xs: 'wrap', md: 'nowrap' } }} + > + {items.map((it) => ( + + ))} + + + ); +} diff --git a/src/components/tracking/tracking.css b/src/components/tracking/tracking.css new file mode 100644 index 0000000..b1cdbe6 --- /dev/null +++ b/src/components/tracking/tracking.css @@ -0,0 +1,196 @@ +/* ==============================|| LIVE TRACKING MAP — marker & overlay styles ||============================== */ + +/* Premium vehicle marker: a crisp white Material silhouette on a coloured disc, with a + direction arrow that orbits to the travel heading. The disc stays upright (icon readable); + only the arrow rotates. */ +.vm-divicon { background: none; border: none; } +.vm { + position: relative; + width: 36px; + height: 36px; + transition: transform 0.18s ease; +} +.vm-disc { + position: absolute; + top: 50%; + left: 50%; + width: 26px; + height: 26px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--vc); + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22); + display: flex; + align-items: center; + justify-content: center; + z-index: 2; +} +.vm-disc svg { display: block; } +.vm-rot { + position: absolute; + inset: 0; + z-index: 1; + transition: transform 0.5s ease-out; +} +.vm-arrow { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 7px solid var(--vc); + opacity: 0.9; +} +/* Selected: a gentle scale + one slow expanding ring (no heavy glow, no gaming pulse). */ +.vm-sel { transform: scale(1.16); z-index: 1000; } +.vm-sel .vm-disc { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 1px 4px rgba(0, 0, 0, 0.3); } +.vm-sel::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 26px; + height: 26px; + border-radius: 50%; + border: 2px solid var(--vc); + transform: translate(-50%, -50%); + animation: vm-ring 2.8s ease-out infinite; + pointer-events: none; +} +@keyframes vm-ring { + 0% { opacity: 0.28; transform: translate(-50%, -50%) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.75); } +} +.vm-del .vm-disc { box-shadow: 0 0 0 1.5px rgba(240, 65, 52, 0.35), 0 1px 3px rgba(0, 0, 0, 0.24); } + +/* Pulsing "live" dot used on KPIs, queue cards and the map endpoint markers. */ +.live-pulse { + position: relative; + display: inline-block; + width: 9px; + height: 9px; + border-radius: 50%; + background: currentColor; +} +.live-pulse::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: currentColor; + animation: live-pulse-ring 1.6s ease-out infinite; +} +@keyframes live-pulse-ring { + 0% { transform: scale(1); opacity: 0.4; } + 100% { transform: scale(2.6); opacity: 0; } +} + +/* Pickup / drop endpoint nodes — white core with a thick coloured ring */ +.ep-divicon { background: none; border: none; } +.ep { + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + border: 4px solid var(--epc); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +/* Cluster bubble — restrained, professional. A solid disc with a thin tinted ring. */ +.vcl-divicon { background: none; border: none; } +.vcl { + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #fff; + background: #1D4ED8; + border: 2.5px solid #fff; + border-radius: 50%; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22), 0 0 0 3px rgba(29, 78, 216, 0.12); +} + +/* Rider-timeline current-step dot — one quiet halo, no gaming pulse */ +.tl-dot-active { box-shadow: 0 0 0 0 var(--tl-c); animation: tl-halo 2.2s ease-out infinite; } +@keyframes tl-halo { + 0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.35); } + 70% { box-shadow: 0 0 0 7px rgba(37, 99, 235, 0); } + 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); } +} + +/* Inter-city corridor load chip — quiet pill at the lane midpoint */ +.lane-chip-divicon { background: none; border: none; } +.lane-chip { + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; + padding: 1px 7px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.94); + border: 1px solid #e2e6ec; + box-shadow: 0 1px 3px rgba(16, 24, 40, 0.1); +} +.lane-chip-n { font-size: 11px; font-weight: 800; color: #334155; line-height: 1; } +.lane-chip-l { font-size: 8.5px; font-weight: 700; letter-spacing: 0.04em; color: #94a3b8; line-height: 1; } + +/* Keep Leaflet controls visually aligned with the enterprise UI */ +.leaflet-container { font-family: inherit; background: #eef1f5; } +.leaflet-bar { border: none !important; box-shadow: 0 1px 5px rgba(0,0,0,0.15) !important; } +.leaflet-bar a { color: #434343; border-bottom-color: #f0f0f0 !important; } +.leaflet-control-attribution { font-size: 10px; opacity: 0.7; } + +/* ===== Hub Network — command-center map ===== */ +/* City wordmark watermark behind the hubs */ +.city-divicon { background: none; border: none; } +.city-wm { + transform: translate(-50%, -50%); + color: rgba(120, 132, 150, 0.45); + font-weight: 800; + font-size: 15px; + letter-spacing: 0.18em; + text-transform: uppercase; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.95); + white-space: nowrap; +} + +/* Hub node: utilization disc + floating label */ +.hub-divicon { background: none; border: none; } +.hub { display: flex; align-items: center; gap: 7px; } +.hub-dot { + flex-shrink: 0; + border-radius: 50%; + background: var(--hc); + border: 3px solid #fff; + color: #fff; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 6px rgba(16, 24, 40, 0.32), 0 0 0 4px var(--hh); +} +.hub-label { + display: flex; + flex-direction: column; + line-height: 1.15; + background: rgba(255, 255, 255, 0.97); + border: 1px solid #e6e9ef; + border-radius: 7px; + padding: 2px 8px; + box-shadow: 0 2px 6px rgba(16, 24, 40, 0.14); +} +.hub-label b { font-size: 11px; font-weight: 700; color: #1f2937; white-space: nowrap; } +.hub-label span { font-size: 9.5px; font-weight: 600; color: #8a94a6; white-space: nowrap; } + +/* Ambient last-mile fleet dot */ +.fleetdot-divicon { background: none; border: none; } +.fleetdot { width: 9px; height: 9px; border-radius: 50%; background: var(--fc); border: 1.5px solid #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.3); } + +/* Animated line-haul corridor flow */ +.corridor-flow { animation: corridor-dash 0.9s linear infinite; } +@keyframes corridor-dash { to { stroke-dashoffset: -28; } } diff --git a/src/components/tracking/vehicleMarker.js b/src/components/tracking/vehicleMarker.js new file mode 100644 index 0000000..8c3bfb8 --- /dev/null +++ b/src/components/tracking/vehicleMarker.js @@ -0,0 +1,75 @@ +import L from 'leaflet'; +import TwoWheelerRoundedIcon from '@mui/icons-material/TwoWheelerRounded'; +import ElectricRickshawRoundedIcon from '@mui/icons-material/ElectricRickshawRounded'; +import LocalShippingRoundedIcon from '@mui/icons-material/LocalShippingRounded'; +import AirportShuttleRoundedIcon from '@mui/icons-material/AirportShuttleRounded'; + +import { vehicleTypes } from '@/data/mock'; + +// Re-exported so tracking components can import type tokens from one place. +export { vehicleTypes }; + +// ==============================|| VEHICLE MARKER FACTORY ||============================== // +// Premium fleet markers: a crisp white Material vehicle silhouette on a coloured disc with a +// direction arrow that rotates to the travel heading. SVG (not emoji) so it renders identically +// everywhere and stays sharp at any zoom. + +// Shared type → MUI icon component map (used by the legend, queue & info card for consistency). +export const vehicleIconComponents = { + Bike: TwoWheelerRoundedIcon, + Auto: ElectricRickshawRoundedIcon, + Truck: LocalShippingRoundedIcon, + Van: AirportShuttleRoundedIcon +}; + +// Matching Material (Rounded) path geometry, used to paint the silhouette inside map markers. +const PATHS = { + Bike: 'M20 11c-.18 0-.36.03-.53.05L17.41 9H19c.55 0 1-.45 1-1v-.38c0-.74-.78-1.23-1.45-.89l-2.28 1.14L13.7 5.3c-.18-.19-.44-.3-.7-.3h-3c-.55 0-1 .45-1 1s.45 1 1 1h2.17c.27 0 .52.11.71.29L14.59 9h-3.35c-.16 0-.31.04-.45.11l-3.14 1.57c-.38.19-.85.12-1.15-.19l-1.2-1.2C5.11 9.11 4.85 9 4.59 9H1c-.55 0-1 .45-1 1s.45 1 1 1h3C1.48 11-.49 13.32.11 15.94c.33 1.45 1.5 2.62 2.95 2.95C5.68 19.49 8 17.52 8 15l1.41 1.41c.38.38.89.59 1.42.59h1.01c.72 0 1.38-.38 1.74-1.01l2.91-5.09 1.01 1.01c-1.13.91-1.76 2.41-1.38 4.05.34 1.44 1.51 2.61 2.95 2.94 2.61.59 4.93-1.39 4.93-3.9 0-2.21-1.79-4-4-4M4 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2m16 0c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2', + Auto: 'M21 11.18V9.72c0-.47-.16-.92-.46-1.28L16.6 3.72c-.38-.46-.94-.72-1.54-.72H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h.18C3.6 16.16 4.7 17 6 17s2.4-.84 2.82-2h8.37c.41 1.16 1.51 2 2.82 2 1.66 0 3-1.34 3-3-.01-1.3-.85-2.4-2.01-2.82M18.4 9H16V6.12zM4 5h3v4H3V6c0-.55.45-1 1-1m2 10c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m3-2v-2h2c.55 0 1-.45 1-1s-.45-1-1-1H9V5h4c.55 0 1 .45 1 1v7zm11 2c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M7 20h4v-2l6 3h-4v2z', + Truck: 'M19.5 8H17V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2 0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h1c.55 0 1-.45 1-1v-3.33c0-.43-.14-.85-.4-1.2L20.3 8.4c-.19-.25-.49-.4-.8-.4M6 18c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m13.5-8.5 1.96 2.5H17V9.5zM18 18c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1', + Van: 'm22.41 10.41-4.83-4.83c-.37-.37-.88-.58-1.41-.58H3c-1.1 0-2 .89-2 2v7c0 1.1.9 2 2 2 0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3c1.1 0 2-.9 2-2v-2.17c0-.53-.21-1.04-.59-1.42M3 10V8c0-.55.45-1 1-1h3v4H4c-.55 0-1-.45-1-1m3 7.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25M13 11H9V7h4zm5 6.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25M15 11V7h1l4 4z' +}; + +const svgCache = new Map(); +function vehicleSvg(type, color, size) { + const k = `${type}|${color}|${size}`; + if (svgCache.has(k)) return svgCache.get(k); + const d = PATHS[type] || PATHS.Bike; + const svg = ``; + svgCache.set(k, svg); + return svg; +} + +export function vehicleIcon({ type, bearing = 0, selected = false, delayed = false }) { + const cfg = vehicleTypes[type] || vehicleTypes.Bike; + const color = delayed ? '#F04134' : cfg.color; + const cls = ['vm', selected ? 'vm-sel' : '', delayed ? 'vm-del' : ''].filter(Boolean).join(' '); + const glyph = vehicleSvg(type, '#fff', selected ? 18 : 15); + + const html = ` +
+
+
${glyph}
+
`; + + const size = 36; + return L.divIcon({ html, className: 'vm-divicon', iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); +} + +export function endpointIcon(color) { + return L.divIcon({ + html: `
`, + className: 'ep-divicon', + iconSize: [18, 18], + iconAnchor: [9, 9] + }); +} + +export function clusterIcon(count) { + const size = count < 10 ? 36 : count < 50 ? 44 : 52; + return L.divIcon({ + html: `
${count}
`, + className: 'vcl-divicon', + iconSize: [size, size] + }); +} diff --git a/src/data/mock.js b/src/data/mock.js index faef1a7..3723e42 100644 --- a/src/data/mock.js +++ b/src/data/mock.js @@ -1,7 +1,14 @@ // ==============================|| DOORMILE - MOCK DATA ||============================== // // Static demo data powering every screen. -export const locations = ['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune']; +export const locations = ['Coimbatore', 'Bengaluru', 'Hyderabad']; + +// Real operating geography — the three live metros and their coordinates. +export const cityCenters = { + Coimbatore: { lat: 11.0168, lng: 76.9558 }, + Bengaluru: { lat: 12.9716, lng: 77.5946 }, + Hyderabad: { lat: 17.385, lng: 78.4867 } +}; export const tenantsList = [ 'Freshly Foods', @@ -15,12 +22,12 @@ export const tenantsList = [ export const orders = [ { id: 'DM-10241', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'HSR Layout, Sec 2', customer: 'Riya Sharma', qty: 3, cod: 0, kms: 4.2, charges: 86, notes: 'Leave at door', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, { id: 'DM-10242', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', customer: 'Karthik Rao', qty: 1, cod: 1299, kms: 12.8, charges: 142, notes: 'Call on arrival', status: 'active', date: '2026-06-05', payment: 'cod' }, - { id: 'DM-10243', tenant: 'MediQuick Pharma', location: 'Mumbai', pickup: 'Andheri West', drop: 'Bandra East', customer: 'Neha Kulkarni', qty: 2, cod: 0, kms: 6.5, charges: 98, notes: '', status: 'picked', date: '2026-06-05', payment: 'prepaid' }, - { id: 'DM-10244', tenant: 'BloomBox Florists', location: 'Delhi NCR', pickup: 'Connaught Place', drop: 'Saket, Block C', customer: 'Arjun Mehta', qty: 1, cod: 450, kms: 9.1, charges: 110, notes: 'Fragile', status: 'pending', date: '2026-06-05', payment: 'cod' }, + { id: 'DM-10243', tenant: 'MediQuick Pharma', location: 'Hyderabad', pickup: 'Hitech City Hub', drop: 'Banjara Hills', customer: 'Neha Kulkarni', qty: 2, cod: 0, kms: 6.5, charges: 98, notes: '', status: 'picked', date: '2026-06-05', payment: 'prepaid' }, + { id: 'DM-10244', tenant: 'BloomBox Florists', location: 'Coimbatore', pickup: 'Gandhipuram', drop: 'R.S. Puram', customer: 'Arjun Mehta', qty: 1, cod: 450, kms: 9.1, charges: 110, notes: 'Fragile', status: 'pending', date: '2026-06-05', payment: 'cod' }, { id: 'DM-10245', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'BTM Layout', customer: 'Sneha Iyer', qty: 4, cod: 0, kms: 3.4, charges: 74, notes: '', status: 'cancelled', date: '2026-06-03', payment: 'prepaid' }, { id: 'DM-10246', tenant: 'TechNova Retail', location: 'Hyderabad', pickup: 'Hitech City', drop: 'Gachibowli', customer: 'Vikram Reddy', qty: 1, cod: 4999, kms: 5.6, charges: 120, notes: 'ID required', status: 'created', date: '2026-06-05', payment: 'cod' }, - { id: 'DM-10247', tenant: 'GreenLeaf Organics', location: 'Pune', pickup: 'Koregaon Park', drop: 'Viman Nagar', customer: 'Pooja Desai', qty: 2, cod: 0, kms: 7.2, charges: 102, notes: '', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, - { id: 'DM-10248', tenant: 'UrbanCart', location: 'Chennai', pickup: 'T. Nagar', drop: 'Adyar', customer: 'Suresh Kumar', qty: 1, cod: 799, kms: 8.3, charges: 108, notes: 'Weekend slot', status: 'pending', date: '2026-06-05', payment: 'cod' } + { id: 'DM-10247', tenant: 'GreenLeaf Organics', location: 'Coimbatore', pickup: 'Peelamedu Hub', drop: 'Saravanampatti', customer: 'Pooja Desai', qty: 2, cod: 0, kms: 7.2, charges: 102, notes: '', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, + { id: 'DM-10248', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Jayanagar', drop: 'JP Nagar', customer: 'Suresh Kumar', qty: 1, cod: 799, kms: 8.3, charges: 108, notes: 'Weekend slot', status: 'pending', date: '2026-06-05', payment: 'cod' } ]; export const orderTimeline = [ @@ -41,10 +48,10 @@ export const deliveries = orders.map((o, i) => ({ export const riders = [ { id: 'RDR-001', userId: 'U1043', name: 'Mohan Das', phone: '+91 98450 11223', address: 'Koramangala, Bengaluru', vehicle: 'Honda Activa', vehicleNo: 'KA-05-HJ-4521', shift: 'Morning', start: '08:00', end: '16:00', fare: 320, fuel: 110, status: 'online', deliveries: 14, rating: 4.8 }, - { id: 'RDR-002', userId: 'U1044', name: 'Imran Sheikh', phone: '+91 98860 44781', address: 'Andheri, Mumbai', vehicle: 'TVS Jupiter', vehicleNo: 'MH-02-CD-9087', shift: 'Evening', start: '14:00', end: '22:00', fare: 280, fuel: 95, status: 'on-delivery', deliveries: 11, rating: 4.6 }, + { id: 'RDR-002', userId: 'U1044', name: 'Imran Sheikh', phone: '+91 98860 44781', address: 'Madhapur, Hyderabad', vehicle: 'TVS Jupiter', vehicleNo: 'TS-09-CD-9087', shift: 'Evening', start: '14:00', end: '22:00', fare: 280, fuel: 95, status: 'on-delivery', deliveries: 11, rating: 4.6 }, { id: 'RDR-003', userId: 'U1045', name: 'Ravi Teja', phone: '+91 99000 22119', address: 'Gachibowli, Hyderabad', vehicle: 'Hero Splendor', vehicleNo: 'TS-09-AB-3344', shift: 'Morning', start: '08:00', end: '16:00', fare: 300, fuel: 120, status: 'online', deliveries: 9, rating: 4.9 }, - { id: 'RDR-004', userId: 'U1046', name: 'Sandeep Roy', phone: '+91 90080 55672', address: 'Saket, Delhi', vehicle: 'Bajaj Pulsar', vehicleNo: 'DL-3C-XY-1290', shift: 'Night', start: '22:00', end: '06:00', fare: 350, fuel: 130, status: 'offline', deliveries: 0, rating: 4.4 }, - { id: 'RDR-005', userId: 'U1047', name: 'Faisal Khan', phone: '+91 97410 88123', address: 'Adyar, Chennai', vehicle: 'Honda Dio', vehicleNo: 'TN-07-PQ-6655', shift: 'Evening', start: '14:00', end: '22:00', fare: 290, fuel: 100, status: 'on-delivery', deliveries: 7, rating: 4.7 } + { id: 'RDR-004', userId: 'U1046', name: 'Sandeep Roy', phone: '+91 90080 55672', address: 'Gandhipuram, Coimbatore', vehicle: 'Bajaj Pulsar', vehicleNo: 'TN-37-XY-1290', shift: 'Night', start: '22:00', end: '06:00', fare: 350, fuel: 130, status: 'offline', deliveries: 0, rating: 4.4 }, + { id: 'RDR-005', userId: 'U1047', name: 'Faisal Khan', phone: '+91 97410 88123', address: 'Peelamedu, Coimbatore', vehicle: 'Honda Dio', vehicleNo: 'TN-37-PQ-6655', shift: 'Evening', start: '14:00', end: '22:00', fare: 290, fuel: 100, status: 'on-delivery', deliveries: 7, rating: 4.7 } ]; export const riderLogs = [ @@ -56,16 +63,16 @@ export const riderLogs = [ export const customers = [ { id: 1, name: 'Riya Sharma', phone: '+91 98111 22334', email: 'riya.sharma@mail.com', address: '24, 1st Cross, HSR Layout', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560102', totalOrders: 18, joined: '2025-03-12' }, { id: 2, name: 'Karthik Rao', phone: '+91 98222 33445', email: 'karthik.rao@mail.com', address: '8, Palm Meadows, Whitefield', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560066', totalOrders: 7, joined: '2025-07-21' }, - { id: 3, name: 'Neha Kulkarni', phone: '+91 98333 44556', email: 'neha.k@mail.com', address: '102, Sea View, Bandra East', location: 'Mumbai', city: 'Mumbai', state: 'Maharashtra', postcode: '400051', totalOrders: 31, joined: '2024-11-02' }, - { id: 4, name: 'Arjun Mehta', phone: '+91 98444 55667', email: 'arjun.m@mail.com', address: 'C-14, Saket', location: 'Delhi NCR', city: 'New Delhi', state: 'Delhi', postcode: '110017', totalOrders: 5, joined: '2025-09-18' }, - { id: 5, name: 'Pooja Desai', phone: '+91 98555 66778', email: 'pooja.d@mail.com', address: '4, Lane 5, Koregaon Park', location: 'Pune', city: 'Pune', state: 'Maharashtra', postcode: '411001', totalOrders: 12, joined: '2025-01-30' } + { id: 3, name: 'Neha Kulkarni', phone: '+91 98333 44556', email: 'neha.k@mail.com', address: '102, Lake View, Banjara Hills', location: 'Hyderabad', city: 'Hyderabad', state: 'Telangana', postcode: '500034', totalOrders: 31, joined: '2024-11-02' }, + { id: 4, name: 'Arjun Mehta', phone: '+91 98444 55667', email: 'arjun.m@mail.com', address: '14, R.S. Puram', location: 'Coimbatore', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '641002', totalOrders: 5, joined: '2025-09-18' }, + { id: 5, name: 'Pooja Desai', phone: '+91 98555 66778', email: 'pooja.d@mail.com', address: '4, Lane 5, Saravanampatti', location: 'Coimbatore', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '641035', totalOrders: 12, joined: '2025-01-30' } ]; export const tenants = [ { id: 1, name: 'Freshly Foods', contact: 'Anil Gupta', phone: '+91 80451 22000', email: 'ops@freshlyfoods.in', address: 'Koramangala 4th Block', city: 'Bengaluru', postcode: '560034', lat: '12.9352', lng: '77.6245', status: 'active', volume: 1240 }, { id: 2, name: 'UrbanCart', contact: 'Meera Nair', phone: '+91 22480 11233', email: 'logistics@urbancart.com', address: 'Indiranagar 100ft Rd', city: 'Bengaluru', postcode: '560038', lat: '12.9719', lng: '77.6412', status: 'active', volume: 980 }, - { id: 3, name: 'MediQuick Pharma', contact: 'Rohit Sen', phone: '+91 22556 99001', email: 'dispatch@mediquick.in', address: 'Andheri West', city: 'Mumbai', postcode: '400058', lat: '19.1351', lng: '72.8290', status: 'pending', volume: 410 }, - { id: 4, name: 'BloomBox Florists', contact: 'Tina Roy', phone: '+91 11409 33221', email: 'hello@bloombox.in', address: 'Connaught Place', city: 'New Delhi', postcode: '110001', lat: '28.6315', lng: '77.2167', status: 'inactive', volume: 120 }, + { id: 3, name: 'MediQuick Pharma', contact: 'Rohit Sen', phone: '+91 40556 99001', email: 'dispatch@mediquick.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'pending', volume: 410 }, + { id: 4, name: 'BloomBox Florists', contact: 'Tina Roy', phone: '+91 42240 33221', email: 'hello@bloombox.in', address: 'Gandhipuram', city: 'Coimbatore', postcode: '641012', lat: '11.0168', lng: '76.9558', status: 'inactive', volume: 120 }, { id: 5, name: 'TechNova Retail', contact: 'Sameer Joshi', phone: '+91 40229 77654', email: 'ops@technova.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'active', volume: 760 } ]; @@ -78,8 +85,8 @@ export const tenantPricing = [ export const pricing = [ { id: 1, location: 'Bengaluru', pricingId: 'PR-001', name: 'Standard', slab: '0-5 km', basePrice: 40, minKm: 2, pricePerKm: 9, maxKm: 5, minOrders: 1 }, { id: 2, location: 'Bengaluru', pricingId: 'PR-002', name: 'Express', slab: '5-10 km', basePrice: 65, minKm: 5, pricePerKm: 8, maxKm: 10, minOrders: 1 }, - { id: 3, location: 'Mumbai', pricingId: 'PR-003', name: 'Standard', slab: '0-5 km', basePrice: 45, minKm: 2, pricePerKm: 10, maxKm: 5, minOrders: 1 }, - { id: 4, location: 'Delhi NCR', pricingId: 'PR-004', name: 'Bulk', slab: '10+ km', basePrice: 95, minKm: 10, pricePerKm: 7, maxKm: 25, minOrders: 5 } + { id: 3, location: 'Hyderabad', pricingId: 'PR-003', name: 'Standard', slab: '0-5 km', basePrice: 45, minKm: 2, pricePerKm: 10, maxKm: 5, minOrders: 1 }, + { id: 4, location: 'Coimbatore', pricingId: 'PR-004', name: 'Bulk', slab: '10+ km', basePrice: 95, minKm: 10, pricePerKm: 7, maxKm: 25, minOrders: 5 } ]; export const invoices = [ @@ -178,53 +185,122 @@ export const statusBreakdown = [ // Mirrors doormile.com's primary narrative: Origin → Hub → Hub → Doorstep. export const threeMile = [ { - key: 'first', title: 'First Mile', subtitle: 'Origin to Hub', color: '#EA580C', - metric: 184, metricLabel: 'pickups today', onTime: 98.1, + key: 'first', title: 'First Mile', subtitle: 'Origin → Hub', color: '#EA580C', + metric: 184, metricLabel: 'active pickups', throughput: 412, onTime: 98.1, exceptions: 2, + utilization: 78, bottleneck: 'Peelamedu dock queue', aiRec: 'Add 2 pickup riders in Coimbatore', features: ['AI-scheduled pickups', 'Dynamic load consolidation', 'Yard & dock management', 'Pickup quality checks'] }, { - key: 'mid', title: 'Mid Mile', subtitle: 'Hub to Hub Transit', color: '#0E7C7B', - metric: 4, metricLabel: 'line-hauls live', onTime: 97.4, + key: 'mid', title: 'Mid Mile', subtitle: 'Hub → Hub Transit', color: '#0E7C7B', + metric: 4, metricLabel: 'line-hauls live', throughput: 38, onTime: 97.4, exceptions: 3, + utilization: 71, bottleneck: 'CBE → BLR corridor +18m', aiRec: 'Pre-stage backup at Hyderabad hub', features: ['Optimised line-haul routing', 'Cross-docking & sortation', 'Live SLA monitoring', 'EV-first corridors'] }, { - key: 'last', title: 'Last Mile', subtitle: 'Hub to Doorstep', color: '#1D4ED8', - metric: 96, metricLabel: 'out for delivery', onTime: 98.9, + key: 'last', title: 'Last Mile', subtitle: 'Hub → Doorstep', color: '#1D4ED8', + metric: 96, metricLabel: 'out for delivery', throughput: 1330, onTime: 98.9, exceptions: 1, + utilization: 84, bottleneck: null, aiRec: 'Consolidate 6 Whitefield drops into 1 run', features: ['Multi-stop route optimization', 'Precise delivery windows', 'Digital proof of delivery', 'Real-time customer updates'] } ]; -// ==============================|| END-TO-END SHIPMENT JOURNEY (hop-by-hop) ||============================== // -// Follows ONE parcel Chennai → Bengaluru through every node: agent → nearest hub → origin main hub -// → line-haul → destination main hub → sub hub → delivery agent → customer. With live monitoring & reroute. -export const shipmentJourney = { - id: 'DM-CHN-BLR-7741', - product: 'Documents & electronics · 3.2 kg', - from: { city: 'Chennai', area: 'T. Nagar', name: 'Suresh Kumar' }, - to: { city: 'Bengaluru', area: 'Koramangala', name: 'Riya Sharma' }, - client: 'TechNova Retail', - distance: 346, - mode: 'Standard · EV-first', - placed: '08 Jun, 09:12 AM', - eta: '09 Jun, 02:30 PM', +// ==============================|| AI COMMAND CENTER — pipeline + impact ||============================== // +// The end-to-end flow Orders → Delivered, each stage carrying a live count for the command centre. +export const commandPipeline = [ + { key: 'orders', label: 'Orders Received', count: '1,402', sub: 'today', icon: 'order', pct: 100, status: 'done' }, + { key: 'trust', label: 'Trust Validation', count: '99.2%', sub: 'cleared', icon: 'trust', pct: 99, status: 'done' }, + { key: 'hub', label: 'Hub Allocation', count: '6', sub: 'hubs', icon: 'hub', pct: 100, status: 'done' }, + { key: 'fleet', label: 'Fleet Assignment', count: '124', sub: 'vehicles', icon: 'fleet', pct: 96, status: 'done' }, + { key: 'dispatch', label: 'Dispatch AI', count: '41', sub: 'optimized', icon: 'ai', pct: 88, status: 'active' }, + { key: 'rider', label: 'Rider Allocation', count: '48', sub: 'active', icon: 'rider', pct: 74, status: 'active' }, + { key: 'tracking', label: 'Live Tracking', count: '96', sub: 'in transit', icon: 'track', pct: 52, status: 'active' }, + { key: 'delivered', label: 'Delivered', count: '1,330', sub: 'today', icon: 'done', pct: 95, status: 'active' } +]; + +// ==============================|| HUB OPERATIONS CENTER (no map) ||============================== // +// Per-city rollups for the Hub Operations cards + comparison + activity. +export const hubCityStats = [ + { city: 'Coimbatore', code: 'CBE', capacity: 10400, utilization: 70, processed: 5840, riders: 14, sla: 97.2, health: 'healthy' }, + { city: 'Bengaluru', code: 'BLR', capacity: 13200, utilization: 64, processed: 8490, riders: 22, sla: 98.6, health: 'healthy' }, + { city: 'Hyderabad', code: 'HYD', capacity: 9300, utilization: 50, processed: 4600, riders: 12, sla: 96.4, health: 'watch' } +]; + +export const hubThroughput = [ + { m: 'Mon', cbe: 720, blr: 1180, hyd: 560 }, + { m: 'Tue', cbe: 810, blr: 1240, hyd: 620 }, + { m: 'Wed', cbe: 760, blr: 1310, hyd: 680 }, + { m: 'Thu', cbe: 880, blr: 1290, hyd: 640 }, + { m: 'Fri', cbe: 940, blr: 1420, hyd: 710 }, + { m: 'Sat', cbe: 1020, blr: 1510, hyd: 760 } +]; + +export const hubActivity = [ + { time: '10:42 AM', hub: 'Hoskote Regional Hub', text: 'Inbound line-haul from Coimbatore scanned in · 320 parcels', type: 'inbound' }, + { time: '10:31 AM', hub: 'Coimbatore Regional Hub', text: 'Sortation wave 3 completed · 0 misroutes', type: 'sort' }, + { time: '10:18 AM', hub: 'Hyderabad Regional Hub', text: 'Outbound EV truck dispatched to Bengaluru', type: 'outbound' }, + { time: '09:58 AM', hub: 'Koramangala Micro Hub', text: 'Last-mile batch released to 8 riders', type: 'dispatch' }, + { time: '09:40 AM', hub: 'Peelamedu Micro Hub', text: 'Dock 3 backlog cleared', type: 'sort' } +]; + +export const maintenanceNotices = [ + { hub: 'Peelamedu Micro Hub', item: 'Backup generator test', due: 'Overdue 1 day', severity: 'high' }, + { hub: 'Hoskote Regional Hub', item: 'Conveyor belt B service', due: 'Due in 2 days', severity: 'medium' }, + { hub: 'Hyderabad Regional Hub', item: 'Dock leveler inspection', due: 'Scheduled tonight', severity: 'low' } +]; + +// AI contribution metrics for the AI Impact section. +export const aiImpact = { + routeSavings: 34, delaysPrevented: 23, optimizationActions: 41, + fuelSavedL: 1240, co2ReducedKg: 12840, networkEfficiency: 92 +}; + +// ==============================|| TRIP JOURNEY (vehicle trip + manifest) ||============================== // +// Primary entity = ONE line-haul TRIP: a single vehicle + driver moving a sealed manifest of many +// shipments along the Coimbatore → Bengaluru corridor. Milestones are TRIP-level only; each +// shipment's own lifecycle lives on Shipment 360 (/orders/:id), linked from the manifest. +export const trip = { + id: 'TRIP-CBE-BLR-0463', + corridor: 'CBE → BLR', + status: 'In Transit', + mode: 'EV-first line-haul', + vehicle: { reg: 'TN-37-LH-0463', type: 'EV Truck 4W', capacityKg: 1200, battery: 62 }, + driver: { name: 'Imran Sheikh', phone: '+91 98860 44781' }, + from: { city: 'Coimbatore', hub: 'Coimbatore Regional Hub' }, + to: { city: 'Bengaluru', hub: 'Hoskote Regional Hub' }, + distance: 365, + departed: '08 Jun, 12:05 PM', + eta: '09 Jun, 06:30 AM', progress: 52, - currentStage: 'Line-Haul · Chennai → Bengaluru', - hops: [ - { key: 'booked', mile: 'First Mile', title: 'Shipment Booked', node: 'Chennai · T. Nagar', handler: 'Customer · Suresh Kumar', icon: 'order', time: '08 Jun, 09:12 AM', status: 'done', detail: 'Shipment ID generated · digital docs & e-waybill created' }, - { key: 'pickup', mile: 'First Mile', title: 'Agent Pickup', node: 'Chennai · T. Nagar', handler: 'Rider · Faisal Khan (EV 2W)', icon: 'agent', time: '08 Jun, 10:05 AM', status: 'done', detail: 'OTP verified · photo proof captured · tamper seal applied' }, - { key: 'nearhub', mile: 'First Mile', title: 'Nearest Hub — Check-in', node: 'Nungambakkam Micro Hub', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 10:48 AM', status: 'done', detail: 'Scanned in · sorted for origin main hub' }, - { key: 'mainhub-out', mile: 'Mid Mile', title: 'Origin Main Hub — Dispatched', node: 'Guindy Regional Hub (Chennai)', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 01:20 PM', status: 'done', detail: 'Consolidated & loaded onto line-haul EV truck' }, - { key: 'linehaul', mile: 'Mid Mile', title: 'Line-Haul In Transit', node: 'NH48 · Chennai → Bengaluru', handler: 'Driver · Imran Sheikh (EV Truck 4W)', icon: 'truck', time: '08 Jun, 01:35 PM', status: 'active', detail: 'En route · live GPS · 178 km to destination hub' }, - { key: 'desthub', mile: 'Mid Mile', title: 'Destination Main Hub — Arrival', node: 'Hoskote Regional Hub (Bengaluru)', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 06:30 AM', status: 'pending', detail: 'Inbound scan & sortation' }, - { key: 'subhub', mile: 'Last Mile', title: 'Sub Hub — Last-Mile Sort', node: 'Koramangala Micro Hub', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 10:15 AM', status: 'pending', detail: 'Routed to delivery agent zone' }, - { key: 'assigned', mile: 'Last Mile', title: 'Delivery Agent Assigned', node: 'Bengaluru · Koramangala', handler: 'MileTruth AI · auto-assign', icon: 'agent', time: 'Est. 09 Jun, 11:00 AM', status: 'pending', detail: 'Multi-stop route optimized for the agent' }, - { key: 'ofd', mile: 'Last Mile', title: 'Out for Delivery', node: 'Bengaluru · Koramangala', handler: 'Rider · auto-assigned (EV 2W)', icon: 'agent', time: 'Est. 09 Jun, 01:40 PM', status: 'pending', detail: 'Live tracking link shared with customer' }, - { key: 'delivered', mile: 'Last Mile', title: 'Delivered', node: 'Bengaluru · Koramangala', handler: 'Customer · Riya Sharma', icon: 'done', time: 'Est. 09 Jun, 02:30 PM', status: 'pending', detail: 'OTP + photo proof of delivery' } + loadKg: 940, + pieces: 312, + // TRIP-level milestones (not per-shipment) + milestones: [ + { key: 'sealed', title: 'Manifest Sealed', node: 'Coimbatore Regional Hub', time: '08 Jun, 11:20 AM', status: 'done', detail: '12 shipments · 312 pieces · 940 kg consolidated' }, + { key: 'departed', title: 'Loaded & Departed', node: 'Coimbatore Regional Hub', time: '08 Jun, 12:05 PM', status: 'done', detail: 'Bay 4 · e-waybill EWB-77410 generated' }, + { key: 'transit', title: 'In Transit · Line-Haul', node: 'CBE → BLR corridor (NH-544)', time: '08 Jun, 12:05 PM', status: 'active', detail: 'Live GPS · 168 km to destination hub · ETA protected' }, + { key: 'arrival', title: 'Arrival Scan', node: 'Hoskote Regional Hub', time: 'Est. 09 Jun, 06:30 AM', status: 'pending', detail: 'Inbound gate scan & trip reconciliation' }, + { key: 'sorted', title: 'Unloaded & Sorted', node: 'Hoskote Regional Hub', time: 'Est. 09 Jun, 07:30 AM', status: 'pending', detail: 'Shipments broken to last-mile micro hubs' }, + { key: 'handover', title: 'Handover to Last-Mile', node: 'Bengaluru micro hubs', time: 'Est. 09 Jun, 09:00 AM', status: 'pending', detail: 'Each shipment enters its own last-mile journey' } + ], + // every shipment riding this trip — links into Shipment 360 + manifest: [ + { id: 'DM-10242', client: 'UrbanCart', customer: 'Karthik Rao', dropArea: 'Whitefield', pieces: 1, weightKg: 12.8, status: 'On board' }, + { id: 'DM-10241', client: 'Freshly Foods', customer: 'Riya Sharma', dropArea: 'HSR Layout', pieces: 3, weightKg: 9.4, status: 'On board' }, + { id: 'DM-10245', client: 'Freshly Foods', customer: 'Sneha Iyer', dropArea: 'BTM Layout', pieces: 4, weightKg: 14.2, status: 'On board' }, + { id: 'DM-10248', client: 'UrbanCart', customer: 'Suresh Kumar', dropArea: 'Jayanagar', pieces: 1, weightKg: 8.3, status: 'On board' }, + { id: 'DM-10261', client: 'TechNova Retail', customer: 'Vikram Reddy', dropArea: 'Indiranagar', pieces: 2, weightKg: 6.1, status: 'On board' }, + { id: 'DM-10263', client: 'GreenLeaf Organics', customer: 'Pooja Desai', dropArea: 'Koramangala', pieces: 5, weightKg: 22.0, status: 'On board' }, + { id: 'DM-10266', client: 'BloomBox Florists', customer: 'Arjun Mehta', dropArea: 'JP Nagar', pieces: 1, weightKg: 3.2, status: 'On board' }, + { id: 'DM-10270', client: 'UrbanCart', customer: 'Meera Nair', dropArea: 'Marathahalli', pieces: 2, weightKg: 5.5, status: 'On board' }, + { id: 'DM-10274', client: 'Freshly Foods', customer: 'Anil Gupta', dropArea: 'Electronic City', pieces: 3, weightKg: 11.7, status: 'On board' }, + { id: 'DM-10277', client: 'TechNova Retail', customer: 'Sameer Joshi', dropArea: 'Hebbal', pieces: 1, weightKg: 4.0, status: 'On board' }, + { id: 'DM-10281', client: 'MediQuick Pharma', customer: 'Rohit Sen', dropArea: 'Whitefield', pieces: 2, weightKg: 7.6, status: 'On board' }, + { id: 'DM-10285', client: 'GreenLeaf Organics', customer: 'Tina Roy', dropArea: 'Yelahanka', pieces: 6, weightKg: 26.4, status: 'On board' } ], events: [ - { time: '08 Jun, 03:10 PM', type: 'reroute', title: 'Traffic on NH48 near Krishnagiri', detail: 'MileTruth AI rerouted via Hosur bypass — ETA protected (+0 min)' }, - { time: '08 Jun, 05:40 PM', type: 'monitor', title: 'Weather check · clear', detail: 'No disruption forecast on the corridor' }, - { time: '08 Jun, 08:15 PM', type: 'monitor', title: 'On schedule', detail: 'Vehicle at 62% battery · charging stop planned at Hosur' } + { time: '08 Jun, 02:10 PM', type: 'reroute', title: 'Congestion near Salem bypass', detail: 'MileTruth AI rerouted via NH-544 — ETA protected (+0 min)' }, + { time: '08 Jun, 05:40 PM', type: 'monitor', title: 'Weather check · clear', detail: 'No disruption forecast on the CBE → BLR corridor' }, + { time: '08 Jun, 08:15 PM', type: 'monitor', title: 'On schedule', detail: 'Vehicle at 62% battery · charging stop planned at Krishnagiri' } ] }; @@ -249,12 +325,12 @@ export const verticalOf = (tenant) => tenantVertical[tenant] || 'Enterprise & B2 // ==============================|| PHARMA COLD-CHAIN ||============================== // export const coldChainSummary = { monitored: 248, inRange: 241, atRisk: 5, breaches: 2, compliance: 99.2, avgTemp: 4.2 }; export const coldChainShipments = [ - { id: 'DM-CC-2041', product: 'Insulin vials', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 4.6, route: 'Andheri → Bandra', status: 'in-range', excursionMin: 0 }, - { id: 'DM-CC-2042', product: 'mRNA vaccine', tenant: 'MediQuick Pharma', range: '-20 – -15 °C', temp: -17.2, route: 'Hub HYD → BLR', status: 'in-range', excursionMin: 0 }, - { id: 'DM-CC-2043', product: 'Frozen seafood', tenant: 'Freshly Foods', range: '≤ -18 °C', temp: -12.4, route: 'Koramangala → HSR', status: 'breach', excursionMin: 14 }, - { id: 'DM-CC-2044', product: 'Dairy & yogurt', tenant: 'GreenLeaf Organics', range: '2 – 6 °C', temp: 7.1, route: 'Koregaon → Viman Nagar', status: 'at-risk', excursionMin: 3 }, - { id: 'DM-CC-2045', product: 'Blood samples', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 5.3, route: 'Connaught Pl → Saket', status: 'in-range', excursionMin: 0 }, - { id: 'DM-CC-2046', product: 'Antibiotics', tenant: 'MediQuick Pharma', range: '15 – 25 °C', temp: 21.4, route: 'Hitech City → Gachibowli', status: 'in-range', excursionMin: 0 } + { id: 'DM-CC-2041', product: 'Insulin vials', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 4.6, route: 'Hyderabad MedTech Zone → Jubilee Hills Hospital', status: 'in-range', excursionMin: 0 }, + { id: 'DM-CC-2042', product: 'mRNA vaccine', tenant: 'MediQuick Pharma', range: '-20 – -15 °C', temp: -17.2, route: 'Hub HYD → Hub BLR', status: 'in-range', excursionMin: 0 }, + { id: 'DM-CC-2043', product: 'Blood plasma', tenant: 'MediQuick Pharma', range: '≤ -18 °C', temp: -12.4, route: 'Bengaluru Pharma Park → Whitefield Clinic', status: 'breach', excursionMin: 14 }, + { id: 'DM-CC-2044', product: 'Dairy & yogurt', tenant: 'GreenLeaf Organics', range: '2 – 6 °C', temp: 7.1, route: 'Coimbatore Cold Store → Peelamedu', status: 'at-risk', excursionMin: 3 }, + { id: 'DM-CC-2045', product: 'Blood samples', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 5.3, route: 'Coimbatore Medical Hub → Peelamedu Hospital', status: 'in-range', excursionMin: 0 }, + { id: 'DM-CC-2046', product: 'Antibiotics', tenant: 'MediQuick Pharma', range: '15 – 25 °C', temp: 21.4, route: 'Hyderabad MedTech Zone → Gachibowli Clinic', status: 'in-range', excursionMin: 0 } ]; // ==============================|| LOGISTICS OPERATING SYSTEM — LAYER DATA ||============================== // @@ -291,37 +367,37 @@ export const trustQueue = [ ]; // ---- Layer 3: Intelligent Hub Network ---- +// Live hub network across the three operating metros: Coimbatore · Bengaluru · Hyderabad. export const hubs = [ - { id: 'HUB-BLR-01', name: 'Koramangala Micro Hub', type: 'Micro Hub', city: 'Bengaluru', lat: 12.9352, lng: 77.6245, capacity: 1200, load: 940, inbound: 180, outbound: 210, dock: 4, status: 'online' }, - { id: 'HUB-BLR-02', name: 'Whitefield City Hub', type: 'City Hub', city: 'Bengaluru', lat: 12.9698, lng: 77.7499, capacity: 4000, load: 2860, inbound: 520, outbound: 610, dock: 10, status: 'online' }, + { id: 'HUB-CBE-RG', name: 'Coimbatore Regional Hub', type: 'Regional Hub', city: 'Coimbatore', lat: 11.0168, lng: 76.9558, capacity: 9000, load: 6120, inbound: 1400, outbound: 1520, dock: 18, status: 'online' }, + { id: 'HUB-CBE-01', name: 'Peelamedu Micro Hub', type: 'Micro Hub', city: 'Coimbatore', lat: 11.0290, lng: 76.9960, capacity: 1400, load: 1180, inbound: 240, outbound: 260, dock: 5, status: 'busy' }, { id: 'HUB-BLR-RG', name: 'Hoskote Regional Hub', type: 'Regional Hub', city: 'Bengaluru', lat: 13.0707, lng: 77.7980, capacity: 12000, load: 7400, inbound: 1900, outbound: 2100, dock: 24, status: 'online' }, - { id: 'HUB-MUM-01', name: 'Andheri City Hub', type: 'City Hub', city: 'Mumbai', lat: 19.1197, lng: 72.8468, capacity: 4500, load: 3950, inbound: 700, outbound: 760, dock: 12, status: 'busy' }, - { id: 'HUB-HYD-01', name: 'Hitech Cross Dock', type: 'Cross Dock', city: 'Hyderabad', lat: 17.4435, lng: 78.3772, capacity: 3000, load: 1100, inbound: 340, outbound: 360, dock: 8, status: 'online' }, - { id: 'HUB-DEL-RG', name: 'Bilaspur Regional Hub', type: 'Regional Hub', city: 'Delhi NCR', lat: 28.4231, lng: 77.0490, capacity: 14000, load: 11200, inbound: 2400, outbound: 2600, dock: 28, status: 'busy' } + { id: 'HUB-BLR-01', name: 'Koramangala Micro Hub', type: 'Micro Hub', city: 'Bengaluru', lat: 12.9352, lng: 77.6245, capacity: 1200, load: 1090, inbound: 180, outbound: 210, dock: 4, status: 'busy' }, + { id: 'HUB-HYD-RG', name: 'Hyderabad Regional Hub', type: 'Regional Hub', city: 'Hyderabad', lat: 17.4435, lng: 78.3772, capacity: 8000, load: 3960, inbound: 920, outbound: 980, dock: 16, status: 'online' }, + { id: 'HUB-HYD-01', name: 'Gachibowli Micro Hub', type: 'Micro Hub', city: 'Hyderabad', lat: 17.4401, lng: 78.3489, capacity: 1300, load: 640, inbound: 150, outbound: 165, dock: 5, status: 'online' } ]; export const hubNetworkTypes = [ - { type: 'Micro Hubs (Urban)', count: 5, desc: 'Last-mile EV staging' }, - { type: 'City Hubs', count: 4, desc: 'Sort & local distribution' }, - { type: 'Regional Hubs', count: 2, desc: 'Aggregation & line-haul origin' }, - { type: 'Cross Dock Points', count: 1, desc: 'No-store transfer' } + { type: 'Regional Hubs', count: 3, desc: 'Aggregation & line-haul origin' }, + { type: 'Micro Hubs (Urban)', count: 3, desc: 'Last-mile EV staging' } ]; +// Inter-city line-haul corridors connecting the three regional hubs (the network triangle). export const lineHauls = [ - { id: 'LH-001', from: 'Hoskote Regional Hub', to: 'Bilaspur Regional Hub', corridor: 'BLR → DEL', distance: 2150, vehicle: 'EV Truck 4W', load: 86, eta: '34h', status: 'in-transit' }, - { id: 'LH-002', from: 'Andheri City Hub', to: 'Hoskote Regional Hub', corridor: 'MUM → BLR', distance: 980, vehicle: 'ICE Truck 6W', load: 72, eta: '18h', status: 'in-transit' }, - { id: 'LH-003', from: 'Hitech Cross Dock', to: 'Hoskote Regional Hub', corridor: 'HYD → BLR', distance: 570, vehicle: 'EV Truck 4W', load: 64, eta: '11h', status: 'scheduled' }, - { id: 'LH-004', from: 'Bilaspur Regional Hub', to: 'Andheri City Hub', corridor: 'DEL → MUM', distance: 1420, vehicle: 'ICE Truck 6W', load: 91, eta: '24h', status: 'loading' } + { id: 'LH-001', from: 'Coimbatore Regional Hub', to: 'Hoskote Regional Hub', corridor: 'CBE → BLR', distance: 365, vehicle: 'EV Truck 4W', load: 84, eta: '7h', status: 'in-transit' }, + { id: 'LH-002', from: 'Hoskote Regional Hub', to: 'Hyderabad Regional Hub', corridor: 'BLR → HYD', distance: 570, vehicle: 'EV Truck 4W', load: 71, eta: '11h', status: 'in-transit' }, + { id: 'LH-003', from: 'Hyderabad Regional Hub', to: 'Coimbatore Regional Hub', corridor: 'HYD → CBE', distance: 790, vehicle: 'ICE Truck 6W', load: 58, eta: '15h', status: 'scheduled' }, + { id: 'LH-004', from: 'Hoskote Regional Hub', to: 'Coimbatore Regional Hub', corridor: 'BLR → CBE', distance: 365, vehicle: 'EV Truck 4W', load: 92, eta: '7h', status: 'loading' } ]; // ---- Layer 4: Fleet & Rider Operating System ---- export const fleet = [ { id: 'VH-EV-018', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 78, range: 96, health: 94, capacityKg: 600, status: 'on-trip', rider: 'Mohan Das', hub: 'Koramangala Micro Hub', uptime: 98.2 }, - { id: 'VH-EV-022', model: 'Euler HiLoad EV', type: 'EV 3W', powertrain: 'EV', battery: 41, range: 38, health: 89, capacityKg: 688, status: 'charging', rider: '—', hub: 'Whitefield City Hub', uptime: 96.5 }, - { id: 'VH-2W-104', model: 'Ola S1 Pro', type: 'EV 2W', powertrain: 'EV', battery: 63, range: 71, health: 91, capacityKg: 20, status: 'on-trip', rider: 'Ravi Teja', hub: 'Hitech Cross Dock', uptime: 97.1 }, + { id: 'VH-EV-022', model: 'Euler HiLoad EV', type: 'EV 3W', powertrain: 'EV', battery: 41, range: 38, health: 89, capacityKg: 688, status: 'charging', rider: '—', hub: 'Hoskote Regional Hub', uptime: 96.5 }, + { id: 'VH-2W-104', model: 'Ola S1 Pro', type: 'EV 2W', powertrain: 'EV', battery: 63, range: 71, health: 91, capacityKg: 20, status: 'on-trip', rider: 'Ravi Teja', hub: 'Hyderabad Regional Hub', uptime: 97.1 }, { id: 'VH-IC-211', model: 'Mahindra Bolero', type: 'ICE 4W', powertrain: 'ICE', battery: null, range: 320, health: 82, capacityKg: 1500, status: 'idle', rider: '—', hub: 'Hoskote Regional Hub', uptime: 93.4 }, - { id: 'VH-2W-118', model: 'Honda Activa', type: 'ICE 2W', powertrain: 'ICE', battery: null, range: 180, health: 76, capacityKg: 25, status: 'maintenance', rider: '—', hub: 'Andheri City Hub', uptime: 88.9 }, - { id: 'VH-EV-031', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 88, range: 108, health: 96, capacityKg: 600, status: 'idle', rider: '—', hub: 'Bilaspur Regional Hub', uptime: 99.0 } + { id: 'VH-2W-118', model: 'Honda Activa', type: 'ICE 2W', powertrain: 'ICE', battery: null, range: 180, health: 76, capacityKg: 25, status: 'maintenance', rider: '—', hub: 'Coimbatore Regional Hub', uptime: 88.9 }, + { id: 'VH-EV-031', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 88, range: 108, health: 96, capacityKg: 600, status: 'idle', rider: '—', hub: 'Hyderabad Regional Hub', uptime: 99.0 } ]; export const fleetSummary = { @@ -332,8 +408,8 @@ export const fleetSummary = { // ---- Layer 5: MileTruth AI Engine — dispatch & optimization ---- export const dispatchQueue = [ { id: 'DM-10246', pickup: 'Hitech City', drop: 'Gachibowli', priority: 'high', sla: '45 min', suggestedRider: 'Ravi Teja', confidence: 96, status: 'matched', etaMin: 22 }, - { id: 'DM-10244', pickup: 'Connaught Place', drop: 'Saket, Block C', priority: 'standard', sla: '90 min', suggestedRider: 'Sandeep Roy', confidence: 88, status: 'optimizing', etaMin: 41 }, - { id: 'DM-10248', pickup: 'T. Nagar', drop: 'Adyar', priority: 'standard', sla: '120 min', suggestedRider: 'Faisal Khan', confidence: 91, status: 'matched', etaMin: 33 }, + { id: 'DM-10244', pickup: 'Gandhipuram', drop: 'R.S. Puram', priority: 'standard', sla: '90 min', suggestedRider: 'Sandeep Roy', confidence: 88, status: 'optimizing', etaMin: 41 }, + { id: 'DM-10248', pickup: 'Jayanagar', drop: 'JP Nagar', priority: 'standard', sla: '120 min', suggestedRider: 'Faisal Khan', confidence: 91, status: 'matched', etaMin: 33 }, { id: 'DM-10242', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', priority: 'express', sla: '60 min', suggestedRider: 'Mohan Das', confidence: 94, status: 'dispatched', etaMin: 28 } ]; @@ -359,13 +435,152 @@ export const aiMetrics = { routeSavings: 34, avgEtaAccuracy: 92, batchRate: 2.6, // ---- Layer 6: Execution & Visibility ---- export const executionFeed = [ - { id: 'DM-10242', stage: 'In-Transit', rider: 'Mohan Das', loc: 'HSR Layout', detail: 'On route · 4.2 km to drop', time: '10:42 AM', proof: null }, - { id: 'DM-10243', stage: 'Picked Up', rider: 'Imran Sheikh', loc: 'Andheri West', detail: 'OTP verified · photo proof logged', time: '10:38 AM', proof: 'pickup' }, - { id: 'DM-10247', stage: 'Delivered', rider: 'Ravi Teja', loc: 'Viman Nagar', detail: 'eSign captured · POD uploaded', time: '10:30 AM', proof: 'delivery' }, - { id: 'DM-10251', stage: 'Exception', rider: 'Faisal Khan', loc: 'Adyar', detail: 'Customer unavailable · reattempt scheduled', time: '10:21 AM', proof: null }, - { id: 'DM-10246', stage: 'Dispatched', rider: 'Sandeep Roy', loc: 'Hitech City', detail: 'Rider en route to pickup', time: '10:18 AM', proof: null } + { id: 'DM-10242', stage: 'In-Transit', rider: 'Mohan Das', loc: 'Indiranagar → Whitefield', detail: 'On route · 4.2 km to drop', time: '10:42 AM', proof: null }, + { id: 'DM-10243', stage: 'Picked Up', rider: 'Imran Sheikh', loc: 'Gandhipuram → Peelamedu', detail: 'OTP verified · photo proof logged', time: '10:38 AM', proof: 'pickup' }, + { id: 'DM-10247', stage: 'Delivered', rider: 'Ravi Teja', loc: 'Banjara Hills → Madhapur', detail: 'eSign captured · POD uploaded', time: '10:30 AM', proof: 'delivery' }, + { id: 'DM-10251', stage: 'Exception', rider: 'Imran Sheikh', loc: 'R.S. Puram → Saravanampatti', detail: 'Customer unavailable · reattempt scheduled', time: '10:21 AM', proof: null }, + { id: 'DM-10246', stage: 'Dispatched', rider: 'Sandeep Roy', loc: 'Hitech City → Gachibowli', detail: 'Rider en route to pickup', time: '10:18 AM', proof: null } ]; +// Active Deliveries Board — the live shipments an ops manager is accountable for right now. +// etaStatus drives row tone & ETA colour: on-time (green), at-risk (amber), delayed (red). +// delayMin is the signed deviation vs SLA (negative = ahead, positive = late) for delayed/at-risk rows. +// ---- Live Tracking Control Tower — geo-coded shipments with real coordinates & routes ---- +// Each delivery carries pickup/drop lat-lng plus a road-shaped polyline; the map interpolates +// the vehicle's live position from `progress`. vehicle ∈ Bike | Auto | Truck | Van. +export const activeDeliveries = [ + { + id: 'DM-10242', rider: 'Mohan Das', vehicle: 'Bike', priority: 'express', city: 'Bengaluru', + origin: 'Indiranagar Store', destination: 'Whitefield, Phase 1', eta: '11:10 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 68, + pickup: { lat: 12.9719, lng: 77.6412 }, drop: { lat: 12.9698, lng: 77.7499 }, + route: [[12.9719, 77.6412], [12.9716, 77.672], [12.975, 77.701], [12.9705, 77.73], [12.9698, 77.7499]] + }, + { + id: 'DM-10243', rider: 'Imran Sheikh', vehicle: 'Bike', priority: 'high', city: 'Coimbatore', + origin: 'Gandhipuram', destination: 'Peelamedu', eta: '11:24 AM', etaStatus: 'delayed', delayMin: 18, status: 'In-Transit', progress: 42, + pickup: { lat: 11.0177, lng: 76.9656 }, drop: { lat: 11.03, lng: 76.9963 }, + route: [[11.0177, 76.9656], [11.021, 76.976], [11.027, 76.988], [11.03, 76.9963]] + }, + { + id: 'DM-10246', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'high', city: 'Hyderabad', + origin: 'Hitech City', destination: 'Gachibowli', eta: '11:02 AM', etaStatus: 'on-time', delayMin: 0, status: 'Dispatched', progress: 12, + pickup: { lat: 17.4435, lng: 78.3772 }, drop: { lat: 17.4401, lng: 78.3489 }, + route: [[17.4435, 78.3772], [17.443, 78.365], [17.441, 78.355], [17.4401, 78.3489]] + }, + { + id: 'DM-10248', rider: 'Faisal Khan', vehicle: 'Truck', priority: 'standard', city: 'Bengaluru', + origin: 'BTM Layout Depot', destination: 'Jayanagar 4th Block', eta: '11:35 AM', etaStatus: 'at-risk', delayMin: 7, status: 'In-Transit', progress: 55, + pickup: { lat: 12.9166, lng: 77.6101 }, drop: { lat: 12.925, lng: 77.5938 }, + route: [[12.9166, 77.6101], [12.92, 77.602], [12.925, 77.5938]] + }, + { + id: 'DM-10244', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'standard', city: 'Bengaluru', + origin: 'Koramangala Hub', destination: 'Electronic City', eta: '12:05 PM', etaStatus: 'at-risk', delayMin: 9, status: 'Picked Up', progress: 30, + pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.8452, lng: 77.6602 }, + route: [[12.9352, 77.6245], [12.91, 77.635], [12.88, 77.648], [12.8452, 77.6602]] + }, + { + id: 'DM-10251', rider: 'Imran Sheikh', vehicle: 'Auto', priority: 'high', city: 'Coimbatore', + origin: 'R.S. Puram', destination: 'Saravanampatti', eta: 'Reattempt', etaStatus: 'delayed', delayMin: 35, status: 'Exception', progress: 80, + pickup: { lat: 11.005, lng: 76.95 }, drop: { lat: 11.079, lng: 77.009 }, + route: [[11.005, 76.95], [11.03, 76.97], [11.055, 76.99], [11.079, 77.009]] + }, + { + id: 'DM-10241', rider: 'Mohan Das', vehicle: 'Auto', priority: 'standard', city: 'Bengaluru', + origin: 'Koramangala Hub', destination: 'HSR Layout, Sec 2', eta: '10:58 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 88, + pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.9081, lng: 77.6476 }, + route: [[12.9352, 77.6245], [12.922, 77.636], [12.9081, 77.6476]] + }, + { + id: 'DM-10247', rider: 'Ravi Teja', vehicle: 'Bike', priority: 'express', city: 'Hyderabad', + origin: 'Banjara Hills', destination: 'Madhapur', eta: '10:46 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 74, + pickup: { lat: 17.4156, lng: 78.4347 }, drop: { lat: 17.4483, lng: 78.3915 }, + route: [[17.4156, 78.4347], [17.43, 78.41], [17.4483, 78.3915]] + }, + { + id: 'DM-10239', rider: 'Ravi Teja', vehicle: 'Bike', priority: 'standard', city: 'Bengaluru', + origin: 'Koramangala Hub', destination: 'Koramangala 5th Block', eta: '10:18 AM', etaStatus: 'on-time', delayMin: 0, status: 'Delivered', progress: 100, + pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.9352, lng: 77.6135 }, + route: [[12.9352, 77.6245], [12.9352, 77.619], [12.9352, 77.6135]] + }, + { + id: 'DM-10240', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'standard', city: 'Hyderabad', + origin: 'Hitech City', destination: 'Kondapur', eta: '10:05 AM', etaStatus: 'on-time', delayMin: 0, status: 'Delivered', progress: 100, + pickup: { lat: 17.4435, lng: 78.3772 }, drop: { lat: 17.4615, lng: 78.3635 }, + route: [[17.4435, 78.3772], [17.452, 78.37], [17.4615, 78.3635]] + } +]; + +// Control-tower KPI strip — the live operational pulse across the whole fleet. +export const executionOps = { + activeDeliveries: 96, + delayed: 4, + onTime: 98.6, + activeRiders: 12, + reroutes: 7, + criticalAlerts: 3 +}; + +// Vehicle-type design tokens — icon glyph + brand colour, shared by queue, markers & legend. +export const vehicleTypes = { + Bike: { glyph: '🛵', color: '#1D4ED8' }, + Auto: { glyph: '🛺', color: '#EA580C' }, + Truck: { glyph: '🚚', color: '#7C3AED' }, + Van: { glyph: '🚐', color: '#0E7C7B' } +}; + +// Fleet totals for the map legend (whole fleet, not just tracked shipments). +export const fleetTotals = { Bike: 68, Auto: 16, Truck: 8, Van: 6, delayed: 4 }; + +// Inter-city corridor load — active shipments moving between the three service metros. +// Rendered as faint lanes on the map and summarised in the corridor strip. +export const networkCorridors = [ + { from: 'Coimbatore', to: 'Bengaluru', active: 12, onTime: 97 }, + { from: 'Bengaluru', to: 'Hyderabad', active: 9, onTime: 98 }, + { from: 'Coimbatore', to: 'Hyderabad', active: 6, onTime: 96 } +]; + +// Ambient live fleet scattered around the three metros — gives the map a real control-tower +// density and feeds marker clustering. Deterministically generated so positions stay stable. +export const fleetVehicles = (() => { + const centers = { + Bengaluru: [12.9716, 77.5946], + Coimbatore: [11.0168, 76.9558], + Hyderabad: [17.385, 78.4867] + }; + const cityKeys = Object.keys(centers); + // small seeded PRNG (mulberry32) for stable scatter + let s = 0x9e3779b9; + const rnd = () => { + s |= 0; s = (s + 0x6d2b79f5) | 0; + let t = Math.imul(s ^ (s >>> 15), 1 | s); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + const counts = { Bike: 64, Auto: 14, Truck: 7, Van: 3 }; // tracked vehicles make up the legend remainder + const riderNames = ['Mohan Das', 'Imran Sheikh', 'Ravi Teja', 'Sandeep Roy', 'Faisal Khan', 'Arjun Mehta', 'Vikram Reddy', 'Suresh Kumar']; + const out = []; + let n = 0; + Object.entries(counts).forEach(([type, count]) => { + for (let i = 0; i < count; i++) { + const ck = cityKeys[Math.floor(rnd() * cityKeys.length)]; + const [clat, clng] = centers[ck]; + const spread = ck === 'Bengaluru' ? 0.09 : 0.06; + out.push({ + id: `VH-${type.slice(0, 2).toUpperCase()}-${String(++n).padStart(3, '0')}`, + type, + city: ck, + lat: clat + (rnd() - 0.5) * spread * 2, + lng: clng + (rnd() - 0.5) * spread * 2, + bearing: Math.floor(rnd() * 360), + rider: riderNames[Math.floor(rnd() * riderNames.length)], + status: rnd() > 0.25 ? 'on-trip' : 'idle' + }); + } + }); + return out; +})(); + export const executionStages = [ { key: 'pickup', title: 'Pickup Execution', items: ['Rider reaches pickup', 'OTP / eSign', 'Photo proof', 'Proof of pickup'], count: 12 }, { key: 'transit', title: 'In-Transit Tracking', items: ['Live GPS', 'Route monitoring', 'ETA updates', 'Geofencing'], count: 96 }, @@ -417,3 +632,79 @@ export const integrations = [ { name: 'Transport Partners', group: 'Partners & Ecosystem', desc: 'Line-haul carriers', status: 'connected', calls: '8 carriers', icon: 'partner' }, { name: 'Warehouse Partners', group: 'Partners & Ecosystem', desc: 'Fulfilment nodes', status: 'pending', calls: '—', icon: 'partner' } ]; + +// ==============================|| MILETRUTH AI — INSIGHTS & RECOMMENDATIONS ||============================== // +// Powers the dashboard "DoorMile AI Insights" command panel. +// type drives the icon; severity drives the chip colour (high=error, medium=warning, low/info=info). +export const aiInsights = [ + { + id: 'AI-201', type: 'route', severity: 'low', + title: '6 deliveries can be consolidated', + detail: 'Six Whitefield drops can merge into one multi-stop run — saves 41 km today.', + action: 'Apply optimized route' + }, + { + id: 'AI-202', type: 'delay', severity: 'medium', + title: 'Route congestion detected near Silk Board', + detail: '3 Bengaluru shipments trending +14 min; congestion clearing after 5 PM.', + action: 'Reroute via Sarjapur Road' + }, + { + id: 'AI-203', type: 'capacity', severity: 'medium', + title: 'Peelamedu hub utilization at 82%', + detail: 'Coimbatore inbound surge this evening; load factor approaching threshold.', + action: 'Divert overflow to Coimbatore Regional Hub' + }, + { + id: 'AI-204', type: 'risk', severity: 'high', + title: 'Reassign rider suggested · DM-10251', + detail: 'R.S. Puram → Saravanampatti at risk after a failed attempt — nearer rider available.', + action: 'Reassign to Sandeep Roy' + }, + { + id: 'AI-205', type: 'route', severity: 'info', + title: 'EV-first opportunity · 9 ICE trips swappable', + detail: 'Charging windows align for 9 trips currently on ICE vehicles across BLR & HYD.', + action: 'Schedule EV swap' + }, + { + id: 'AI-206', type: 'delay', severity: 'info', + title: 'Line-haul delay risk · BLR → HYD', + detail: 'Weather model predicts 22 min delay after 4 PM on the corridor.', + action: 'Pre-stage backup at Hyderabad Regional Hub' + } +]; + +// Network capacity utilization (%) — drives the AI panel mini-block. +export const capacityUtilization = { overall: 82, hubs: 76, fleet: 71, riders: 84 }; + +// ==============================|| EXCEPTION CENTER — LIVE OPERATIONAL ISSUES ||============================== // +// The triage queue an ops manager works first. category drives grouping/filter chips; +// severity → row tone (high=error, medium=warning, low=info); status → workflow state. +export const exceptionQueue = [ + { id: 'DM-10251', category: 'Customer Unavailable', severity: 'high', detail: 'R.S. Puram → Saravanampatti · customer not reachable', owner: 'Imran Sheikh', age: '9 min', status: 'open' }, + { id: 'HUB-CBE-01', category: 'Hub Capacity Exceeded', severity: 'high', detail: 'Peelamedu Micro Hub at 84% · inbound surge', owner: 'Hub ops · CBE', age: '8 min', status: 'open' }, + { id: 'DM-10243', category: 'Traffic Delay', severity: 'medium', detail: 'Gandhipuram → Peelamedu · +18 min on current traffic', owner: 'Imran Sheikh', age: '12 min', status: 'investigating' }, + { id: 'DM-CC-2043', category: 'Weather Delay', severity: 'high', detail: 'Cold-chain breach risk · Bengaluru Pharma Park run', owner: 'MediQuick Pharma', age: '14 min', status: 'open' }, + { id: 'VH-2W-118', category: 'Vehicle Breakdown', severity: 'medium', detail: 'Honda Activa down · Coimbatore Regional Hub', owner: 'Fleet ops · CBE', age: '21 min', status: 'open' }, + { id: 'DM-10244', category: 'Incorrect Address', severity: 'medium', detail: 'Incomplete drop address · R.S. Puram', owner: 'Sandeep Roy', age: '46 min', status: 'assigned' }, + { id: 'DM-10248', category: 'Traffic Delay', severity: 'low', detail: 'Rider off optimized path · Jayanagar loop', owner: 'Faisal Khan', age: '6 min', status: 'monitoring' } +]; + +// ==============================|| ATTENTION REQUIRED — DECISION CARDS ||============================== // +// Rolled-up counts of what needs a manager's attention now. Each card is a single click into +// the module that owns the workflow. severity → accent (high=error, medium=warning, low=info). +export const attentionItems = [ + { key: 'delayed', label: 'Delayed Orders', count: 7, severity: 'high', icon: 'delay', to: '/orders' }, + { key: 'critical', label: 'Critical Alerts', count: 3, severity: 'high', icon: 'alert', to: '/tracking' }, + { key: 'hub', label: 'Hub Bottleneck', count: 1, severity: 'medium', icon: 'hub', to: '/hubs' }, + { key: 'route', label: 'Route Deviations', count: 2, severity: 'medium', icon: 'route', to: '/dispatch' } +]; + +// Compact tracking-alert feed for the Live Operations column. +export const trackingAlerts = [ + { id: 'DM-10242', type: 'geofence', detail: 'Approaching drop · 4.2 km · HSR Layout', time: '2 min ago', severity: 'low' }, + { id: 'DM-10251', type: 'exception', detail: 'Customer unavailable · reattempt scheduled', time: '6 min ago', severity: 'high' }, + { id: 'DM-10243', type: 'delay', detail: 'ETA slipping · +18 min vs SLA', time: '11 min ago', severity: 'medium' }, + { id: 'DM-10246', type: 'dispatch', detail: 'Rider en route to pickup · Hitech City', time: '14 min ago', severity: 'low' } +]; diff --git a/src/layout/MainLayout/Header.jsx b/src/layout/MainLayout/Header.jsx index acefa4d..3dd445e 100644 --- a/src/layout/MainLayout/Header.jsx +++ b/src/layout/MainLayout/Header.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppBar, @@ -17,15 +17,17 @@ import { Tooltip, Button, Stack, + Select, alpha } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; -import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; import LogoutIcon from '@mui/icons-material/Logout'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; @@ -33,33 +35,41 @@ import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined'; import DoneAllIcon from '@mui/icons-material/DoneAll'; import Logo from '@/components/Logo'; +import { locations } from '@/data/mock'; +import { useFilters } from '@/store/Filters'; -const RED = '#C01227'; // brand accent (avatars, dots) -const BAR = '#8E1F2A'; // muted deep-brick top bar (toned down from vivid #C01227) +const RED = '#C01227'; // brand accent (reserved for attention: avatar, unread dots) const INITIAL_NOTIFICATIONS = [ { id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false }, - { id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false }, + { id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/fleet', read: false }, { id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false }, { id: 4, icon: AssignmentOutlinedIcon, title: 'MileTruth AI re-optimized 41 routes', time: '3 hrs ago', to: '/dispatch', read: true } ]; -const MESSAGES = [ - { id: 1, name: 'Priya Nair', text: 'Can we reroute the MG Road batch?', time: '5 min ago', initials: 'PN' }, - { id: 2, name: 'Imran Khan', text: 'Reached the warehouse, loading now.', time: '22 min ago', initials: 'IK' }, - { id: 3, name: 'Acme Logistics', text: 'Please confirm the revised pricing.', time: '2 hrs ago', initials: 'AL' } -]; - export default function Header({ onToggle }) { const navigate = useNavigate(); const [account, setAccount] = useState(null); const [notifAnchor, setNotifAnchor] = useState(null); - const [msgAnchor, setMsgAnchor] = useState(null); const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS); const [search, setSearch] = useState(''); + const { location, setLocation } = useFilters(); // global location — single source of truth + const searchRef = useRef(null); const unread = notifications.filter((n) => !n.read).length; + // ⌘K / Ctrl+K focuses global search + useEffect(() => { + const onKey = (e) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + searchRef.current?.focus(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); + const openNotif = (e) => setNotifAnchor(e.currentTarget); const closeNotif = () => setNotifAnchor(null); const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); @@ -79,76 +89,124 @@ export default function Header({ onToggle }) { t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }} + sx={{ + bgcolor: '#FFFFFF', + color: 'grey.800', + zIndex: (t) => t.zIndex.drawer + 1, + borderBottom: '1px solid', + borderColor: 'grey.200' + }} > - - - - - {/* Brand wordmark — left side */} - navigate('/dashboard')} - sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} - > - + {/* LEFT — hamburger + brand (equal-flex zone) */} + + + + + navigate('/dashboard')} + sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer', flexShrink: 0 }} + > + + - - - {/* Search — moved to the right */} + {/* CENTER — global search, the primary nav element (fixed width = stays truly centered) */} - + setSearch(e.target.value)} - placeholder="Search orders, riders, customers…" - sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }} - inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }} + placeholder="Search orders, shipments, riders, customers…" + sx={{ color: 'grey.800', fontSize: '0.875rem', flex: 1 }} + inputProps={{ 'aria-label': 'search' }} /> + + ⌘K + - - setMsgAnchor(e.currentTarget)}> - - - - - - - - - - - - + {/* RIGHT — location + notifications + profile (equal-flex zone, right-aligned) */} + + - setAccount(e.currentTarget)} - sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }} - > - AD - - - Aman Deshmukh - - - Operations Admin - + + + + + + + + + setAccount(e.currentTarget)} + sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: 'grey.100' } }} + > + AD + + + Aman Deshmukh + + + Operations Admin + + + @@ -180,7 +238,7 @@ export default function Header({ onToggle }) { return ( onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - + @@ -200,39 +258,6 @@ export default function Header({ onToggle }) { - {/* Messages dropdown */} - setMsgAnchor(null)} - transformOrigin={{ horizontal: 'right', vertical: 'top' }} - anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} - PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }} - > - - Messages - - - {MESSAGES.map((m) => ( - setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - - - {m.initials} - - - - - {m.time} - - - ))} - - {/* Account dropdown */} - - {depth > 0 && !Icon ? : Icon ? : null} + + {open && ( )} @@ -68,122 +77,66 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) { const isActive = (url) => url && location.pathname.startsWith(url); const expanded = open || isMobile; - const initialOpen = navItems - .flatMap((g) => g.items) - .filter((i) => i.children && i.children.some((c) => isActive(c.url))) - .map((i) => i.id); - const [collapse, setCollapse] = useState(initialOpen); - const go = (url) => { navigate(url); if (isMobile) onMobileClose(); }; const content = ( - - - + + {/* brand — restrained height, generous top alignment */} + + + + {/* navigation */} {navItems.map((grp) => ( - - {expanded && ( + + {expanded && grp.group && ( {grp.group} )} - - {grp.items.map((item) => { - if (item.children) { - const opened = collapse.includes(item.id); - const childActive = item.children.some((c) => isActive(c.url)); - const Icon = item.icon; - const head = ( - - expanded - ? setCollapse((p) => (p.includes(item.id) ? p.filter((x) => x !== item.id) : [...p, item.id])) - : go(item.children[0].url) - } - sx={{ - minHeight: 44, - my: 0.25, - mx: expanded ? 1 : 0.75, - px: expanded ? 1.5 : 0, - justifyContent: expanded ? 'flex-start' : 'center', - borderRadius: 2, - color: childActive ? '#fff' : 'rgba(255,255,255,0.78)', - bgcolor: childActive && !opened ? 'rgba(255,255,255,0.12)' : 'transparent', - '&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' } - }} - > - - - - {expanded && ( - <> - - {opened ? : } - - )} - - ); - return ( - - {expanded ? head : {head}} - {expanded && ( - - - {item.children.map((c) => ( - go(c.url)} /> - ))} - - - )} - - ); - } - return ( - go(item.url)} /> - ); - })} + + {grp.items.map((item) => ( + go(item.url)} /> + ))} ))} - {expanded && ( - - - Delivering Trust. - - - Beyond Boundaries · v1.0 - - - )} ); @@ -201,23 +154,24 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) { ); } + // Desktop: an in-flow grid-column element (NOT a fixed Drawer paper) so it can never be clipped + // or overlap the content. Width comes from the layout grid; this just fills its column and pins + // to the viewport while content scrolls. return ( - t.transitions.create('width', { duration: t.transitions.duration.standard }) - } + gridColumn: 1, + position: 'sticky', + top: 0, + height: '100vh', + overflow: 'hidden', + borderRight: '1px solid', + borderColor: 'rgba(192,18,39,0.14)', + boxShadow: '1px 0 4px rgba(192,18,39,0.05)' }} - open={open} > {content} - + ); } diff --git a/src/layout/MainLayout/index.jsx b/src/layout/MainLayout/index.jsx index 5caab1b..4421858 100644 --- a/src/layout/MainLayout/index.jsx +++ b/src/layout/MainLayout/index.jsx @@ -6,9 +6,16 @@ import { useTheme } from '@mui/material/styles'; import Header from './Header'; import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar'; +// ==============================|| MAIN LAYOUT — CSS GRID SHELL ||============================== // +// One deterministic grid: [ sidebar (fixed px) | content (flexible, minWidth 0) ]. +// The sidebar width is the single source of truth (exported from Sidebar.jsx). No width-calc +// hacks, no negative margins, no fixed-paper overlap — the content column begins exactly where +// the sidebar ends, the sidebar can never be clipped, and content can never overflow horizontally. + export default function MainLayout() { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('lg')); + // Sidebar stays a permanent column on md+ (and through browser zoom); overlay drawer below md. + const isMobile = useMediaQuery(theme.breakpoints.down('md')); const [open, setOpen] = useState(true); const [mobileOpen, setMobileOpen] = useState(false); @@ -17,26 +24,27 @@ export default function MainLayout() { else setOpen((p) => !p); }; + const sidebarW = open ? DRAWER_WIDTH : MINI_WIDTH; + return ( - +
- setMobileOpen(false)} - /> - - - + + {/* column 1 on md+ (in-flow); overlay drawer on mobile (taken out of flow) */} + setMobileOpen(false)} /> + + {/* column 2 — content; minWidth:0 + overflowX:clip guarantee no horizontal spill */} + + + diff --git a/src/main.jsx b/src/main.jsx index 3997aa9..a6f620b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -7,6 +7,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import theme from '@/theme'; import App from '@/App'; +import { OpsProvider } from '@/store/OpsStore'; +import { FilterProvider } from '@/store/Filters'; ReactDOM.createRoot(document.getElementById('root')).render( @@ -14,7 +16,11 @@ ReactDOM.createRoot(document.getElementById('root')).render( - + + + + + diff --git a/src/menu/navItems.jsx b/src/menu/navItems.jsx index 79e61fc..b15d10d 100644 --- a/src/menu/navItems.jsx +++ b/src/menu/navItems.jsx @@ -1,102 +1,50 @@ -import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; -import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; -import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined'; -import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; -import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined'; -import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; -import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; -import SecurityOutlinedIcon from '@mui/icons-material/SecurityOutlined'; -import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; -import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined'; -import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import SpaceDashboardOutlinedIcon from '@mui/icons-material/SpaceDashboardOutlined'; import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; -import TrendingUpOutlinedIcon from '@mui/icons-material/TrendingUpOutlined'; -import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined'; -import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; +import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; +import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; +import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; +import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; +import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined'; +import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; -// ==============================|| DOORMILE - LOGISTICS OPERATING SYSTEM NAV ||============================== // -// Groups mirror the 8-layer end-to-end flow of the Doormile operating system. +// ==============================|| DOORMILE — LOGISTICS OS NAVIGATION ||============================== // +// Consolidated operational IA. The first group has no label (single Control Center entry — no +// redundant header). All icons are the Material *Outlined* family for one consistent visual weight. const navItems = [ { - group: 'Overview', + group: '', + items: [{ id: 'dashboard', title: 'Control Center', url: '/dashboard', icon: SpaceDashboardOutlinedIcon }] + }, + { + group: 'Operations', items: [ - { id: 'dashboard', title: 'System Overview', url: '/dashboard', icon: DashboardOutlinedIcon }, - { id: 'three-mile', title: 'Three-Mile Network', url: '/three-mile', icon: RouteOutlinedIcon } + { id: 'dispatch-tracking', title: 'Dispatch & Tracking', url: '/tracking', icon: MyLocationOutlinedIcon }, + { id: 'shipments', title: 'Shipments', url: '/orders', icon: Inventory2OutlinedIcon } ] }, { - group: '1 · Book & Create', + group: 'Network', items: [ - { id: 'orders', title: 'Shipments', url: '/orders', icon: Inventory2OutlinedIcon }, - { id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon } + { id: 'fleet', title: 'Fleet', url: '/fleet', icon: LocalShippingOutlinedIcon }, + { id: 'hubs', title: 'Hubs', url: '/hubs', icon: HubOutlinedIcon } ] }, { - group: '2 · Trust & Identity', - items: [{ id: 'trust', title: 'Trust & Compliance', url: '/trust', icon: SecurityOutlinedIcon }] - }, - { - group: '3 · Hub Network', - items: [{ id: 'hubs', title: 'Hub Network', url: '/hubs', icon: HubOutlinedIcon }] - }, - { - group: '4 · Fleet & Riders', + group: 'Business', items: [ - { id: 'fleet', title: 'Fleet', url: '/fleet', icon: ElectricRickshawOutlinedIcon }, - { id: 'riders', title: 'Riders', url: '/riders', icon: TwoWheelerOutlinedIcon } + { id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon }, + { id: 'finance', title: 'Finance', url: '/invoice', icon: PaymentsOutlinedIcon } ] }, { - group: '5 · MileTruth AI', - items: [{ id: 'dispatch', title: 'AI Dispatch', url: '/dispatch', icon: AutoAwesomeOutlinedIcon }] + group: 'Insights', + items: [{ id: 'reports', title: 'Reports', url: '/reports', icon: BarChartOutlinedIcon }] }, { - group: '6 · Execution', - items: [ - { id: 'tracking', title: 'Live Tracking', url: '/tracking', icon: MyLocationOutlinedIcon }, - { id: 'journey', title: 'Shipment Journey', url: '/tracking/journey', icon: AltRouteOutlinedIcon }, - { id: 'cold-chain', title: 'Cold Chain', url: '/cold-chain', icon: AcUnitOutlinedIcon }, - { id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon } - ] - }, - { - group: '7 · Analytics', - items: [ - { id: 'analytics', title: 'Analytics', url: '/analytics', icon: TrendingUpOutlinedIcon }, - { - id: 'reports', - title: 'Reports', - icon: BarChartOutlinedIcon, - children: [ - { id: 'orders-summary', title: 'Order Summary', url: '/reports/orders-summary', icon: SummarizeOutlinedIcon }, - { id: 'orders-details', title: 'Order Details', url: '/reports/orders-details', icon: FactCheckOutlinedIcon }, - { id: 'riders-summary', title: 'Riders Summary', url: '/reports/riders-summary', icon: TwoWheelerOutlinedIcon }, - { id: 'riders-logs', title: 'Riders Logs', url: '/reports/riders-logs', icon: MapOutlinedIcon } - ] - } - ] - }, - { - group: '8 · Integrations', - items: [ - { id: 'integrations', title: 'Integrations', url: '/integrations', icon: ApiOutlinedIcon }, - { id: 'tenants', title: 'Clients', url: '/tenants', icon: ApartmentOutlinedIcon } - ] - }, - { - group: 'Finance', - items: [ - { id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon }, - { id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon } - ] + group: 'System', + items: [{ id: 'settings', title: 'Settings', url: '/settings', icon: SettingsOutlinedIcon }] } ]; diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 65a6bf0..20a9ff3 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,225 +1,237 @@ -import { Grid, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, MenuItem, TextField, Avatar, LinearProgress, Chip } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { Grid, Stack, Typography, Box, Button, Divider, LinearProgress, Chip, Avatar } from '@mui/material'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; +import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; -import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined'; +import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded'; import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; +import KpiStrip from '@/components/KpiStrip'; import MainCard from '@/components/MainCard'; import StatusChip from '@/components/StatusChip'; import AreaChart from '@/components/charts/AreaChart'; -import DonutChart from '@/components/charts/DonutChart'; -import UserAvatar from '@/components/UserAvatar'; -import SystemPipeline from '@/components/SystemPipeline'; -import ThreeMileStrip from '@/components/ThreeMileStrip'; +import ProcessTracker from '@/components/ProcessTracker'; +import AiImpactSummary from '@/components/AiImpactSummary'; import Toast, { useToast } from '@/components/Toast'; -import { ordersTrend, statusBreakdown, orders, riders, aiMetrics, fleetSummary, verticals, verticalOf } from '@/data/mock'; +import { dispatchQueue, activeDeliveries, aiInsights, executionFeed, fleetSummary, lanePerformance, hubCityStats, ordersTrend, analyticsKpis } from '@/data/mock'; import { inr } from '@/utils/format'; -const VERTICAL_COLOR = Object.fromEntries(verticals.map((v) => [v.label, v.color])); +const SEV_DOT = { high: '#F04134', medium: '#FFBF00', low: '#00A2AE', info: '#8C8C8C' }; +const hubUtil = Math.round(hubCityStats.reduce((s, h) => s + h.utilization, 0) / hubCityStats.length); + +function SectionLabel({ children }) { + return {children}; +} export default function Dashboard() { + const navigate = useNavigate(); const [toast, showToast] = useToast(); + + const priority = activeDeliveries.filter((d) => (d.priority === 'high' || d.priority === 'express') && d.status !== 'Delivered').slice(0, 4); + const delayed = activeDeliveries.filter((d) => d.etaStatus !== 'on-time' && d.status !== 'Delivered').slice(0, 4); + const recs = aiInsights.slice(0, 4); + + const kpis = [ + { label: 'Total Orders', value: '1,402', icon: Inventory2OutlinedIcon }, + { label: 'Active Shipments', value: '96', color: '#1D4ED8', icon: LocalShippingOutlinedIcon }, + { label: 'Riders Online', value: '48', color: '#00773B', icon: TwoWheelerOutlinedIcon }, + { label: 'Hub Utilization', value: `${hubUtil}%`, color: hubUtil > 80 ? '#A82216' : '#8A6500', icon: HubOutlinedIcon }, + { label: 'Revenue Today', value: inr(384200), color: '#00727B', icon: CurrencyRupeeIcon }, + { label: 'SLA Performance', value: `${analyticsKpis.slaAchievement}%`, color: '#00773B', icon: TaskAltOutlinedIcon } + ]; + return ( <> - - All Locations - Bengaluru - Mumbai - - - - } + title="Operations Control Center" + breadcrumbs={[{ label: 'Control Center' }]} + action={} /> - {/* End-to-end operating-system pipeline */} - - - End-to-End Intelligent Logistics Flow - - - - - + {/* Top row — 6 live KPIs */} + - {/* Three-Mile model — First → Mid → Last */} - - - Three-Mile Network · One Connected System - - - - - - - - - - - - - - } - > - d.m)} - series={[ - { name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) }, - { name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) } - ]} - /> - - - - - - - - - - - {/* MileTruth AI + Sustainability */} - - - - MileTruth AI Engine - - } - > - - - - - - - - - - - - - {fleetSummary.evShare}% - EV fleet share - - - - {(fleetSummary.co2SavedKg / 1000).toFixed(1)}t CO₂ saved this month - - - - - - } spacing={0}> - {riders.slice(0, 4).map((r, i) => ( - - {i + 1} - - - {r.name} - {r.vehicle} · ⭐ {r.rating} - - - {r.deliveries} - deliveries - - - ))} - - - - - - - - {verticals.map((v) => ( - - - - - - {v.label} - {v.desc} - + {/* Second row — dispatch intelligence */} + + Dispatch & Exceptions + + + navigate('/orders/assign')} sx={{ fontWeight: 600 }}>Open} noPadding> + }> + {dispatchQueue.map((d) => ( + + + {d.id} + - - {v.shipments} - {v.onTime}% on-time + {d.pickup} → {d.drop} + AI → {d.suggestedRider} · SLA {d.sla} + + ))} + + + + + + + }> + {priority.map((d) => ( + + + {d.id} + {d.destination} + + {d.priority} + + ))} + + + + + + + }> + {delayed.map((d) => ( + + + {d.id} + {d.rider} + + + + +{d.delayMin}m + + + ))} + {delayed.length === 0 && No delays.} + + + + + + + }> + {recs.map((r) => ( + + + + {r.title} + {r.action} - - ))} - - + ))} + + + + - - - - - - Order ID - Customer - Vertical - Route - Status - Amount - - - - {orders.slice(0, 6).map((o) => { - const v = verticalOf(o.tenant); - return ( - - {o.id} - {o.customer} - - - - - {o.pickup} → {o.drop} - - - {inr(o.charges)} - - ); - })} - -
-
+ {/* Third row — events / fleet / route efficiency */} + + Operational Intelligence + + + + }> + {executionFeed.map((e, i) => ( + + + + + {e.stage} + {e.time} + + {e.id} · {e.loc} + + + ))} + + + + + + + + {[['On trip', fleetSummary.onTrip, '#1D4ED8'], ['Charging', fleetSummary.charging, '#00A2AE'], ['Idle', fleetSummary.idle, '#8C8C8C'], ['Maintenance', fleetSummary.maintenance, '#F04134']].map(([l, v, c]) => ( + + + {v} + {l} + + + ))} + + + EV fleet share + {fleetSummary.evShare}% + + + + + + + + }> + {lanePerformance.slice(0, 5).map((l) => ( + + + {l.lane} + {l.onTime}% · {inr(l.costPer)} + + = 98 ? 'success' : l.onTime >= 96 ? 'warning' : 'error'} sx={{ height: 5, borderRadius: 3 }} /> + + ))} + + + -
+
+ + {/* End-to-end flow + volume trend */} + + Network Flow & Volume + + + + + + } + sx={{ height: '100%' }} + > + + d.m)} + series={[ + { name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) }, + { name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) } + ]} + /> + + + + + + + {/* AI impact */} + + + + ); } -function AiStat({ icon: Icon, color, value, label }) { - return ( - - - - {value} - {label} - - - ); -} - function Legend({ color, label }) { return ( @@ -228,8 +240,3 @@ function Legend({ color, label }) { ); } - -const hexA = (hex, a) => { - const n = parseInt(hex.replace('#', ''), 16); - return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`; -}; diff --git a/src/pages/Deliveries.jsx b/src/pages/Deliveries.jsx index ae51448..4b6b542 100644 --- a/src/pages/Deliveries.jsx +++ b/src/pages/Deliveries.jsx @@ -22,8 +22,11 @@ import StatCard from '@/components/StatCard'; import EmptyState from '@/components/EmptyState'; import UserAvatar from '@/components/UserAvatar'; import TabLabelCount from '@/components/TabLabelCount'; -import { deliveries, locations, tenantsList, riders } from '@/data/mock'; +import { deliveries, tenantsList, riders } from '@/data/mock'; import { inr } from '@/utils/format'; +import DateRangeFilter from '@/components/DateRangeFilter'; +import FilterSummary from '@/components/FilterSummary'; +import { useFilters, inRange } from '@/store/Filters'; const TABS = [ { key: 'assigned', label: 'Assigned' }, @@ -140,12 +143,11 @@ function DeliveryRow({ row, index }) { } export default function Deliveries() { + const { location, range } = useFilters(); // global location + date range const [tab, setTab] = useState(0); const [search, setSearch] = useState(''); const [tenant, setTenant] = useState('all'); - const [location, setLocation] = useState('all'); const [rider, setRider] = useState('all'); - const [headerLocation, setHeaderLocation] = useState('all'); const [page, setPage] = useState(0); const [rpp, setRpp] = useState(5); @@ -170,13 +172,14 @@ export default function Deliveries() { const matchTab = d.status === tabKey; const matchTenant = tenant === 'all' || d.tenant === tenant; const matchLocation = location === 'all' || d.location === location; + const matchDate = d.date ? inRange(d.date, range) : true; const matchRider = rider === 'all' || d.rider === rider; const matchSearch = !search || [d.id, d.tenant, d.pickup, d.drop, d.rider, d.location].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchTab && matchTenant && matchLocation && matchRider && matchSearch; + return matchTab && matchTenant && matchLocation && matchDate && matchRider && matchSearch; }), - [tabKey, tenant, location, rider, search] + [tabKey, tenant, location, range, rider, search] ); const paged = filtered.slice(page * rpp, page * rpp + rpp); @@ -186,15 +189,7 @@ export default function Deliveries() { setHeaderLocation(e.target.value)} - sx={{ minWidth: 180 }} label="Location" - > - All Locations - {locations.map((l) => {l})} - - } + action={} /> @@ -206,17 +201,10 @@ export default function Deliveries() { - { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant"> All Tenants {tenantsList.map((t) => {t})} - { setLocation(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Location"> - All Locations - {locations.map((l) => {l})} - { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider"> All Riders {riders.map((r) => {r.name})} @@ -229,6 +217,10 @@ export default function Deliveries() { /> + + + + { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> {TABS.map((t, i) => ( diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index 2bac162..a9e186e 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -1,206 +1,259 @@ import { useState } from 'react'; import { - Grid, - Tabs, - Tab, - Box, - Stack, - TextField, - MenuItem, - Switch, - FormControlLabel, - Button, - Typography, - Divider, - Snackbar, - Alert + Box, Stack, Typography, Card, Tabs, Tab, TextField, MenuItem, Switch, Button, Grid, + Snackbar, Alert } from '@mui/material'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; +import RestartAltOutlinedIcon from '@mui/icons-material/RestartAltOutlined'; import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined'; -import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; +import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined'; +import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined'; +// section / row icons +import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined'; +import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; +import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; +import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined'; +import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; +import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined'; +import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined'; +import VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined'; +import DevicesOutlinedIcon from '@mui/icons-material/DevicesOutlined'; +import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined'; +import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; +import WhatsAppIcon from '@mui/icons-material/WhatsApp'; +import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; +import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; +import WebhookIcon from '@mui/icons-material/Webhook'; +import HistoryToggleOffOutlinedIcon from '@mui/icons-material/HistoryToggleOffOutlined'; +import PolicyOutlinedIcon from '@mui/icons-material/PolicyOutlined'; +import CloudDownloadOutlinedIcon from '@mui/icons-material/CloudDownloadOutlined'; +import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; -import PageHeader from '@/components/PageHeader'; -import MainCard from '@/components/MainCard'; +import StatusChip from '@/components/StatusChip'; -const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)']; -const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)']; +const RETENTION = ['90 days', '180 days', '1 year', '3 years', 'Indefinite']; -function TabPanel({ value, index, children }) { - if (value !== index) return null; - return {children}; -} +const TABS = [ + { key: 'general', label: 'General', icon: TuneOutlinedIcon }, + { key: 'operations', label: 'Operations', icon: AltRouteOutlinedIcon }, + { key: 'security', label: 'Security', icon: LockOutlinedIcon }, + { key: 'integrations', label: 'Integrations', icon: ApiOutlinedIcon }, + { key: 'compliance', label: 'Compliance', icon: GppGoodOutlinedIcon } +]; -export default function Settings() { - const [tab, setTab] = useState(0); - const [toast, setToast] = useState(false); - - // General - const [general, setGeneral] = useState({ - orgName: 'Doormile Logistics Pvt. Ltd.', - supportEmail: 'support@doormile.in', - contact: '+91 98450 11223', - timezone: TIMEZONES[0], - language: LANGUAGES[0] - }); - - // Notifications - const [notify, setNotify] = useState({ - newOrders: true, - riderStatus: true, - invoicePaid: true, - weeklyDigest: false, - emailAlerts: true, - smsAlerts: false - }); - - // Security - const [security, setSecurity] = useState({ - currentPassword: '', - newPassword: '', - confirmPassword: '', - twoFactor: false - }); - - const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value })); - const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked })); - const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked })); - - const save = () => setToast(true); +const INITIAL = { + general: { orgName: 'Doormile Logistics Pvt. Ltd.', supportEmail: 'support@doormile.in', contact: '+91 98450 11223' }, + notif: { customer: true, rider: true, delay: true, admin: true }, + security: { twoFactor: true, sessionTimeout: true } +}; +// ---- building blocks ---- +function SectionCard({ title, description, icon: Icon, action, children, sx }) { return ( - <> - } onClick={save}> - Save Changes - - } - /> - - - - - setTab(v)} - sx={{ - '& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 } - }} - > - } iconPosition="start" label="General" /> - } iconPosition="start" label="Notifications" /> - } iconPosition="start" label="Security" /> - - - - - - {/* General */} - - - - - - - - - - - - - - - {TIMEZONES.map((t) => ( - {t} - ))} - - - - - {LANGUAGES.map((l) => ( - {l} - ))} - - - - - - - {/* Notifications */} - - - } spacing={0}> - {[ - { k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' }, - { k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' }, - { k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' }, - { k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' } - ].map((row) => ( - - - {row.t} - {row.d} - - - - ))} - - - Channels - - } label="Email alerts" /> - } label="SMS alerts" /> - - - - - {/* Security */} - - - - - - - - - - - - - - - - - - - - Authenticator app - - Require a one-time code at sign-in for extra security. - - - setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} /> - - - - - - - - setToast(false)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} - > - setToast(false)} sx={{ width: '100%' }}> - Settings saved successfully. - - - + + + + + {Icon && ( + + + + )} + + {title} + {description && {description}} + + + {action && {action}} + + {children} + + + ); +} + +function ToggleRow({ icon: Icon, title, desc, checked, onChange, divider }) { + return ( + + + {Icon && ( + + + + )} + + {title} + {desc} + + + + + ); +} + +const INTEGRATIONS = [ + { key: 'emailjs', name: 'EmailJS', desc: 'Transactional email delivery', icon: EmailOutlinedIcon, status: 'connected', sync: '2 min ago' }, + { key: 'whatsapp', name: 'WhatsApp', desc: 'Customer tracking updates', icon: WhatsAppIcon, status: 'connected', sync: '5 min ago' }, + { key: 'gmaps', name: 'Google Maps', desc: 'Geocoding & route tiles', icon: MapOutlinedIcon, status: 'connected', sync: '1 min ago' }, + { key: 'razorpay', name: 'Razorpay', desc: 'Payments & COD settlement', icon: PaymentsOutlinedIcon, status: 'degraded', sync: '38 min ago' }, + { key: 'webhooks', name: 'Webhooks', desc: 'Event push to partner systems', icon: WebhookIcon, status: 'pending', sync: 'Never' } +]; + +export default function Settings() { + const [tab, setTab] = useState('general'); + const [toast, setToast] = useState(''); + const [general, setGeneral] = useState(INITIAL.general); + const [notif, setNotif] = useState(INITIAL.notif); + const [security, setSecurity] = useState(INITIAL.security); + + const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value })); + const setN = (k) => (e) => setNotif((p) => ({ ...p, [k]: e.target.checked })); + const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.checked })); + + const save = () => setToast('Settings saved successfully.'); + const reset = () => { + setGeneral(INITIAL.general); setNotif(INITIAL.notif); setSecurity(INITIAL.security); + setToast('Changes reset to last saved state.'); + }; + + return ( + + {/* header */} + + + Settings + + Manage organization preferences, notifications, integrations, and security settings. + + + + + + + + + {/* sticky tabs */} + + setTab(v)} variant="scrollable" scrollButtons="auto" sx={{ minHeight: 48, '& .MuiTab-root': { minHeight: 48, textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }}> + {TABS.map((t) => ( + } iconPosition="start" label={t.label} /> + ))} + + + + {/* ===================== GENERAL ===================== */} + {tab === 'general' && ( + + + + + + + + + + )} + + {/* ===================== OPERATIONS ===================== */} + {tab === 'operations' && ( + + {/* Notification Controls */} + + + + + + + + + + + + )} + + {/* ===================== SECURITY ===================== */} + {tab === 'security' && ( + + + }> + + {security.twoFactor ? 'Enabled for all admin accounts via authenticator app.' : 'Currently disabled — enable to add a second verification step.'} + + + + + Configure}> + Minimum 12 characters · mixed case · expires every 90 days. + + + + View Sessions}> + + Auto sign-out after 30 min idle + + + + + + Manage Roles}> + 4 roles · 12 members · 3 pending invites. + + + + )} + + {/* ===================== INTEGRATIONS ===================== */} + {tab === 'integrations' && ( + + {INTEGRATIONS.map((it) => ( + + }> + + + Last sync + {it.sync} + + + + + + ))} + + )} + + {/* ===================== COMPLIANCE ===================== */} + {tab === 'compliance' && ( + + + + + {RETENTION.map((r) => {r})} + + + + + View Logs}> + 1,284 events recorded in the last 30 days. + + + + Manage}> + Customer PII masked in exports · GDPR & DPDP aligned. + + + + }>Export}> + Orders, riders, invoices and logs as CSV / JSON. + + + + )} + + setToast('')} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}> + setToast('')} sx={{ width: '100%' }}>{toast} + + ); } diff --git a/src/pages/business/CustomersHub.jsx b/src/pages/business/CustomersHub.jsx new file mode 100644 index 0000000..25d5579 --- /dev/null +++ b/src/pages/business/CustomersHub.jsx @@ -0,0 +1,17 @@ +import TabbedWorkspace from '@/components/TabbedWorkspace'; +import Customers from '@/pages/customers/Customers'; +import Tenants from '@/pages/tenants/Tenants'; + +// ==============================|| CUSTOMERS — directory workspace ||============================== // +// Merges the consumer Customers directory and the Business Clients (tenants) directory. + +export default function CustomersHub() { + return ( + }, + { key: 'clients', label: 'Business Clients', element: } + ]} + /> + ); +} diff --git a/src/pages/business/FinanceHub.jsx b/src/pages/business/FinanceHub.jsx new file mode 100644 index 0000000..4949240 --- /dev/null +++ b/src/pages/business/FinanceHub.jsx @@ -0,0 +1,67 @@ +import { Card, Table, TableBody, TableCell, TableHead, TableRow, Typography, Box } from '@mui/material'; + +import TabbedWorkspace from '@/components/TabbedWorkspace'; +import PageHeader from '@/components/PageHeader'; +import StatusChip from '@/components/StatusChip'; +import Invoices from '@/pages/invoice/Invoices'; +import Pricing from '@/pages/Pricing'; +import { invoices } from '@/data/mock'; +import { inr } from '@/utils/format'; + +// ==============================|| FINANCE — billing workspace ||============================== // +// Merges Invoices, Pricing and a Payments ledger (settled invoices) into one screen. + +const METHODS = ['NEFT', 'UPI', 'RTGS', 'Corporate Card', 'Cheque']; + +function Payments() { + const paid = invoices.filter((i) => i.status === 'paid'); + return ( + <> + + + + + + Invoice + Client + Period + Method + Received + Amount + Status + + + + {paid.map((p, i) => ( + + {p.invoiceId} + {p.client} + {p.period} + {METHODS[i % METHODS.length]} + {p.dueDate} + {inr(p.amount)} + + + ))} + {paid.length === 0 && ( + No payments recorded. + )} + +
+
+ + + ); +} + +export default function FinanceHub() { + return ( + }, + { key: 'pricing', label: 'Pricing', element: }, + { key: 'payments', label: 'Payments', element: } + ]} + /> + ); +} diff --git a/src/pages/customers/Customers.jsx b/src/pages/customers/Customers.jsx index 514a3be..2722883 100644 --- a/src/pages/customers/Customers.jsx +++ b/src/pages/customers/Customers.jsx @@ -17,6 +17,7 @@ import MainCard from '@/components/MainCard'; import EmptyState from '@/components/EmptyState'; import UserAvatar from '@/components/UserAvatar'; import { customers, locations } from '@/data/mock'; +import { useFilters } from '@/store/Filters'; const EMPTY_FORM = { name: '', phone: '', address: '', location: '', city: '', state: '', @@ -26,7 +27,7 @@ const EMPTY_FORM = { export default function Customers() { const navigate = useNavigate(); const [search, setSearch] = useState(''); - const [location, setLocation] = useState('all'); + const { location } = useFilters(); // global location — single source of truth const [page, setPage] = useState(0); const [rpp, setRpp] = useState(5); const [editOpen, setEditOpen] = useState(false); @@ -74,13 +75,6 @@ export default function Customers() { breadcrumbs={[{ label: 'Customers' }]} action={ - { setLocation(e.target.value); setPage(0); }} - sx={{ minWidth: 160 }} label="Location" - > - All Locations - {locations.map((l) => {l})} - { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }} diff --git a/src/pages/dispatch/DispatchBoard.jsx b/src/pages/dispatch/DispatchBoard.jsx new file mode 100644 index 0000000..3c153e7 --- /dev/null +++ b/src/pages/dispatch/DispatchBoard.jsx @@ -0,0 +1,256 @@ +import { useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Grid, Card, Stack, Box, Typography, Button, Divider, Chip, MenuItem, Select, IconButton, Tooltip +} from '@mui/material'; +import BoltOutlinedIcon from '@mui/icons-material/BoltOutlined'; +import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; +import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import PersonAddAlt1OutlinedIcon from '@mui/icons-material/PersonAddAlt1Outlined'; +import UndoOutlinedIcon from '@mui/icons-material/UndoOutlined'; +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; + +import PageHeader from '@/components/PageHeader'; +import StatusChip from '@/components/StatusChip'; +import UserAvatar from '@/components/UserAvatar'; +import Toast, { useToast } from '@/components/Toast'; +import { dispatchQueue, orders, riders } from '@/data/mock'; +import { inr } from '@/utils/format'; +import { useOps } from '@/store/OpsStore'; + +const PRIORITY = { high: { fg: '#A82216', bg: '#FEEAE9' }, express: { fg: '#8A6500', bg: '#FFF7E0' }, standard: { fg: '#595959', bg: '#F0F0F0' } }; +const confColor = (c) => (c >= 90 ? '#00773B' : c >= 80 ? '#8A6500' : '#595959'); +const orderById = (id) => orders.find((o) => o.id === id); +const riderByName = (name) => riders.find((r) => r.name === name); +const availableRiders = riders.filter((r) => r.status !== 'offline'); + +function KpiCard({ label, value, subtitle, color }) { + return ( + + {label} + + {value} + + {subtitle} + + ); +} + +export default function DispatchBoard() { + const navigate = useNavigate(); + const [toast, showToast] = useToast(); + const { assignments, assignOrder, unassignOrder, riderLoad, assignedCount } = useOps(); + + // per-card chosen rider (defaults to AI suggestion) + const [choice, setChoice] = useState({}); + + const queue = useMemo(() => dispatchQueue.filter((q) => !assignments[q.id]), [assignments]); + const assignedList = useMemo(() => Object.entries(assignments).map(([id, a]) => ({ id, ...a })).sort((x, y) => y.at - x.at), [assignments]); + + const onlineCount = availableRiders.length; + const avgConfidence = queue.length ? Math.round(queue.reduce((s, q) => s + q.confidence, 0) / queue.length) : 0; + + const doAssign = (q, riderName) => { + const rider = riderByName(riderName) || availableRiders[0]; + if (!rider) return showToast('No available rider', 'warning'); + assignOrder(q.id, rider); + showToast(`${q.id} assigned to ${rider.name}`); + }; + + const autoAssignAll = () => { + if (!queue.length) return showToast('Queue is already clear', 'info'); + queue.forEach((q) => { + const rider = riderByName(q.suggestedRider) || availableRiders[0]; + if (rider) assignOrder(q.id, rider); + }); + showToast(`${queue.length} orders auto-assigned by MileTruth AI`); + }; + + return ( + <> + } onClick={autoAssignAll} disabled={!queue.length}> + Auto-assign all + + } + /> + + {/* primary KPI block — large dashboard cards */} + + + + + + + + + + + + + + + + + {/* LEFT · unassigned queue */} + + + + Awaiting Dispatch + {queue.length} orders + + + + {queue.map((q) => { + const o = orderById(q.id) || {}; + const pr = PRIORITY[q.priority] || PRIORITY.standard; + const selected = choice[q.id] ?? q.suggestedRider; + return ( + + + {/* order facts */} + + + navigate(`/orders/${q.id}`)}>{q.id} + {q.priority} + {o.tenant && {o.tenant}} + + + {q.pickup} + + {q.drop} + + + + + SLA {q.sla} · ETA {q.etaMin}m + + {o.charges != null && {inr(o.charges)}} + {o.kms != null && {o.kms} km} + + {/* AI suggestion */} + + + AI suggests + {q.suggestedRider} + + + + + {/* assign controls */} + + + + + + + ); + })} + {queue.length === 0 && ( + + + Queue clear — every order is dispatched. + + )} + + + + + {/* RIGHT · riders + assigned-this-session */} + + + + + Riders + {onlineCount} available + + + }> + {riders.map((r) => { + const load = riderLoad(r.id); + return ( + + + + {r.name} + {r.vehicle} · {r.address.split(',').pop().trim()} + + + + {r.deliveries + load} today{load > 0 ? ` (+${load})` : ''} + + + ); + })} + + + + + + Assigned this session + {assignedList.length} + + + {assignedList.length === 0 ? ( + + Nothing assigned yet. Use Assign or Auto-assign. + + ) : ( + }> + {assignedList.map((a) => ( + + + + {a.id} + → {a.riderName} · {a.vehicle} + + + { unassignOrder(a.id); showToast(`${a.id} returned to queue`, 'info'); }}> + + + + + ))} + + )} + + + + + + + + ); +} diff --git a/src/pages/fleet/Fleet.jsx b/src/pages/fleet/Fleet.jsx index 3c4bec7..9b6867d 100644 --- a/src/pages/fleet/Fleet.jsx +++ b/src/pages/fleet/Fleet.jsx @@ -1,5 +1,7 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Tooltip, TextField, MenuItem } from '@mui/material'; +import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined'; import BatteryChargingFullOutlinedIcon from '@mui/icons-material/BatteryChargingFullOutlined'; import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined'; @@ -21,6 +23,7 @@ import { fleet, fleetSummary } from '@/data/mock'; const BLANK = { id: '', model: '', type: 'EV 4W', powertrain: 'EV', capacityKg: '', hub: 'Koramangala Micro Hub' }; export default function Fleet() { + const navigate = useNavigate(); const [rows, setRows] = useState(fleet); const [open, setOpen] = useState(false); const [form, setForm] = useState(BLANK); @@ -56,7 +59,12 @@ export default function Fleet() { } onClick={() => setOpen(true)}>Add Vehicle} + action={ + + + + + } />
- {['Koramangala Micro Hub', 'Whitefield City Hub', 'Hoskote Regional Hub', 'Andheri City Hub', 'Hitech Cross Dock', 'Bilaspur Regional Hub'].map((h) => {h})} + {['Coimbatore Regional Hub', 'Peelamedu Micro Hub', 'Hoskote Regional Hub', 'Koramangala Micro Hub', 'Hyderabad Regional Hub', 'Gachibowli Micro Hub'].map((h) => {h})} diff --git a/src/pages/hubs/HubNetwork.jsx b/src/pages/hubs/HubNetwork.jsx index 6cbe8ce..45fe980 100644 --- a/src/pages/hubs/HubNetwork.jsx +++ b/src/pages/hubs/HubNetwork.jsx @@ -1,199 +1,192 @@ -import { useState } from 'react'; -import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Divider, TextField, MenuItem } from '@mui/material'; +import { Grid, Card, Stack, Typography, Box, LinearProgress, Button, Divider } from '@mui/material'; import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; -import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import AddLocationAltOutlinedIcon from '@mui/icons-material/AddLocationAltOutlined'; +import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; +import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined'; +import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; +import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined'; import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; import MainCard from '@/components/MainCard'; -import StatusChip from '@/components/StatusChip'; -import LayerBanner from '@/components/LayerBanner'; -import MapPlaceholder from '@/components/MapPlaceholder'; -import FormDialog from '@/components/FormDialog'; +import AreaChart from '@/components/charts/AreaChart'; import Toast, { useToast } from '@/components/Toast'; -import { hubs, hubNetworkTypes, lineHauls } from '@/data/mock'; +import { hubCityStats, hubThroughput, hubActivity, maintenanceNotices, exceptionQueue } from '@/data/mock'; -const BLANK = { name: '', type: 'Micro Hub', city: 'Bengaluru', capacity: '', dock: '' }; +const HEALTH = { healthy: { bg: '#E3F6EC', fg: '#00773B', label: 'Healthy' }, watch: { bg: '#FFF7E0', fg: '#8A6500', label: 'Watch' }, critical: { bg: '#FEEAE9', fg: '#A82216', label: 'Critical' } }; +const SEV = { high: '#F04134', medium: '#FFBF00', low: '#00A2AE' }; + +function Stat({ icon: Icon, label, value }) { + return ( + + + + {value} + {label} + + + ); +} + +function HubCard({ h }) { + const health = HEALTH[h.health] || HEALTH.healthy; + const utilColor = h.utilization > 80 ? 'error' : h.utilization > 70 ? 'warning' : 'success'; + return ( + + + + {h.city} + {h.code} · {h.processed.toLocaleString('en-IN')} processed today + + {health.label} + + + + Capacity utilization + {h.utilization}% + + + + + + + + + + + ); +} export default function HubNetwork() { - const [rows, setRows] = useState(hubs); - const [open, setOpen] = useState(false); - const [form, setForm] = useState(BLANK); const [toast, showToast] = useToast(); - const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); - - const addHub = () => { - if (!form.name.trim()) return showToast('Enter a hub name', 'warning'); - const cityCode = (form.city.slice(0, 3) || 'HUB').toUpperCase(); - setRows((r) => [ - ...r, - { id: `HUB-${cityCode}-${String(10 + r.length).slice(-2)}`, name: form.name, type: form.type, city: form.city, lat: 0, lng: 0, capacity: Number(form.capacity) || 1000, load: 0, inbound: 0, outbound: 0, dock: Number(form.dock) || 4, status: 'online' } - ]); - setForm(BLANK); - setOpen(false); - showToast(`${form.name} added to network`); - }; - - const totalCap = rows.reduce((s, h) => s + h.capacity, 0); - const totalLoad = rows.reduce((s, h) => s + h.load, 0); - const util = Math.round((totalLoad / totalCap) * 100); return ( <> } onClick={() => setOpen(true)}>Add Hub} - /> - - } onClick={() => showToast('Hub report exported')}>Export} /> + {/* Top — hub summary cards */} - - - l.status === 'in-transit').length} icon={LocalShippingOutlinedIcon} color="primary" caption="inter-city" /> - - - - - - - - Hub - Type - City - Load / Capacity - Docks - Status - - - - {rows.map((h) => { - const pct = Math.round((h.load / h.capacity) * 100); - return ( - - - {h.name} - {h.id} - - {h.type} - {h.city} - - - {h.load.toLocaleString('en-IN')} / {h.capacity.toLocaleString('en-IN')} - {pct}% - - 90 ? 'error' : pct > 75 ? 'warning' : 'success'} sx={{ height: 6, borderRadius: 3, mt: 0.5 }} /> - - {h.dock} - - - ); - })} - -
-
-
- - - - } spacing={0}> - {hubNetworkTypes.map((t) => ( - - - - - - {t.type} - {t.desc} - - {t.count} - - ))} - - - - - - - - - - Corridor - Vehicle - Distance - Load - ETA - Status - - - - {lineHauls.map((l) => ( - - - {l.corridor} - {l.from} → {l.to} - - {l.vehicle} - {l.distance.toLocaleString('en-IN')} km - - - {l.load}% - - {l.eta} - - - ))} - -
-
-
- - - - - - + {hubCityStats.map((h) => ( + + ))}
- setOpen(false)} title="Add Hub" onSubmit={addHub} submitLabel="Add Hub"> - - - - - {['Micro Hub', 'City Hub', 'Regional Hub', 'Cross Dock'].map((t) => {t})} - + {/* Middle — throughput trends + capacity comparison */} + + + + + + + + + } + > + + d.m)} + series={[ + { name: 'Coimbatore', color: '#EA580C', data: hubThroughput.map((d) => d.cbe) }, + { name: 'Bengaluru', color: '#1D4ED8', data: hubThroughput.map((d) => d.blr) }, + { name: 'Hyderabad', color: '#0E7C7B', data: hubThroughput.map((d) => d.hyd) } + ]} + /> + + - - - {['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune'].map((c) => {c})} - + + + + {hubCityStats.map((h) => ( + + + {h.city} + {h.utilization}% · SLA {h.sla}% + + 80 ? 'error' : h.utilization > 70 ? 'warning' : 'success'} sx={{ height: 7, borderRadius: 4 }} /> + + ))} + + - - - + + + {/* Bottom — activity / alerts / maintenance */} + + + + + }> + {hubActivity.map((a, i) => ( + + + + + {a.hub} + {a.time} + + {a.text} + + + ))} + + + + + + + }> + {exceptionQueue.slice(0, 5).map((e, i) => ( + + + + {e.category} + {e.detail} + + {e.age} + + ))} + + + + + + + }> + {maintenanceNotices.map((m, i) => ( + + + + {m.item} + {m.hub} + + {m.due} + + ))} + + + + + + ); } + +function Legend({ color, label }) { + return ( + + + {label} + + ); +} diff --git a/src/pages/operations/DispatchTracking.jsx b/src/pages/operations/DispatchTracking.jsx new file mode 100644 index 0000000..24b72b0 --- /dev/null +++ b/src/pages/operations/DispatchTracking.jsx @@ -0,0 +1,22 @@ +import TabbedWorkspace from '@/components/TabbedWorkspace'; +import LiveTracking from '@/pages/tracking/LiveTracking'; +import DispatchBoard from '@/pages/dispatch/DispatchBoard'; +import ShipmentJourney from '@/pages/tracking/ShipmentJourney'; +import ColdChain from '@/pages/coldchain/ColdChain'; + +// ==============================|| DISPATCH & TRACKING — operations workspace ||============================== // +// Merges Live Tracking (flagship map), Dispatch board (with AI suggestions), Shipment Journey +// and Cold Chain into one screen. AI recommendations live inside the Dispatch tab. + +export default function DispatchTracking() { + return ( + }, + { key: 'dispatch', label: 'Dispatch', element: }, + { key: 'journey', label: 'Journey', element: }, + { key: 'cold-chain', label: 'Cold Chain', element: } + ]} + /> + ); +} diff --git a/src/pages/orders/OrdersList.jsx b/src/pages/orders/OrdersList.jsx index 62a42bf..1911483 100644 --- a/src/pages/orders/OrdersList.jsx +++ b/src/pages/orders/OrdersList.jsx @@ -1,136 +1,138 @@ import { useState, useMemo } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { - Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, IconButton, - Tooltip, TablePagination, Typography, SpeedDial, SpeedDialAction, Chip + Card, Stack, Button, Box, Tabs, Tab, Table, TableBody, TableCell, TableContainer, TableHead, + TableRow, Checkbox, IconButton, Tooltip, TablePagination, Typography } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; import AddIcon from '@mui/icons-material/Add'; import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; -import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined'; -import TuneIcon from '@mui/icons-material/Tune'; - -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import StatusChip from '@/components/StatusChip'; -import TabLabelCount from '@/components/TabLabelCount'; -import { orders } from '@/data/mock'; -import { inr } from '@/utils/format'; - import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; +import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; +import PageHeader from '@/components/PageHeader'; +import KpiStrip from '@/components/KpiStrip'; +import PageToolbar from '@/components/PageToolbar'; +import StatusChip from '@/components/StatusChip'; +import TabLabelCount from '@/components/TabLabelCount'; +import { orders } from '@/data/mock'; +import { inr } from '@/utils/format'; +import { useOps } from '@/store/OpsStore'; +import { useFilters } from '@/store/Filters'; + +const ACTIVE = ['created', 'pending', 'picked', 'active']; const TABS = [ - { key: 'created', label: 'Created' }, - { key: 'pending', label: 'Pending' }, + { key: 'all', label: 'All' }, + { key: 'active', label: 'Active' }, { key: 'delivered', label: 'Delivered' }, - { key: 'cancelled', label: 'Cancelled' } + { key: 'delayed', label: 'Delayed' }, + { key: 'exceptions', label: 'Exceptions' } ]; +const STICKY_HEAD = { + '& .MuiTableCell-head': { position: 'sticky', top: 0, zIndex: 2, bgcolor: 'grey.50' }, + '& .MuiTableRow-root:hover': { backgroundColor: 'grey.50' } +}; + export default function OrdersList() { const navigate = useNavigate(); + const { exceptions } = useOps(); + const { location } = useFilters(); // global location — single source of truth const [searchParams] = useSearchParams(); const [tab, setTab] = useState(0); const [search, setSearch] = useState(searchParams.get('q') || ''); const [tenant, setTenant] = useState('all'); const [page, setPage] = useState(0); - const [rpp, setRpp] = useState(5); + const [rpp, setRpp] = useState(10); const [selected, setSelected] = useState([]); const tabKey = TABS[tab].key; + const inTab = (o, key) => { + if (key === 'all') return true; + if (key === 'active') return ACTIVE.includes(o.status); + if (key === 'delivered') return o.status === 'delivered'; + if (key === 'delayed') return o.status === 'pending'; // at-risk / awaiting action + if (key === 'exceptions') return Boolean(exceptions[o.id]); + return true; + }; const filtered = useMemo( () => orders.filter((o) => { - const matchTab = tabKey === 'created' ? true : o.status === tabKey; const matchTenant = tenant === 'all' || o.tenant === tenant; + const matchLoc = location === 'all' || o.location === location; const matchSearch = - !search || - [o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchTab && matchTenant && matchSearch; + !search || [o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase()); + return inTab(o, tabKey) && matchTenant && matchLoc && matchSearch; }), - [tabKey, tenant, search] + // eslint-disable-next-line react-hooks/exhaustive-deps + [tabKey, tenant, location, search, exceptions] ); const paged = filtered.slice(page * rpp, page * rpp + rpp); const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); - const counts = { - created: orders.length, - pending: orders.filter((o) => o.status === 'pending').length, - delivered: orders.filter((o) => o.status === 'delivered').length, - cancelled: orders.filter((o) => o.status === 'cancelled').length - }; + const count = (k) => orders.filter((o) => inTab(o, k)).length; + + const codTotal = orders.reduce((s, o) => s + (o.cod || 0), 0); + const kpis = [ + { label: 'Total Orders', value: orders.length, icon: Inventory2OutlinedIcon }, + { label: 'Active', value: count('active'), color: '#1D4ED8', icon: HourglassEmptyOutlinedIcon }, + { label: 'Delivered', value: count('delivered'), color: '#00773B', icon: CheckCircleOutlineIcon }, + { label: 'Exceptions', value: count('exceptions'), color: '#A82216', icon: CancelOutlinedIcon }, + { label: 'COD to Collect', value: inr(codTotal), color: '#00727B', icon: CurrencyRupeeIcon } + ]; return ( <> - - + + + + } /> - - - - - - + - - {/* filter toolbar */} - - setSearch(e.target.value)} - sx={{ minWidth: 240 }} - InputProps={{ startAdornment: }} - /> - - - setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> - All Tenants - {[...new Set(orders.map((o) => o.tenant))].map((t) => {t})} - - - All Locations - {[...new Set(orders.map((o) => o.location))].map((l) => {l})} - - - - - { setTab(v); setPage(0); }}> + + + { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> {TABS.map((t, i) => ( - } /> + } /> ))} - {selected.length > 0 && ( - - {selected.length} selected - - - )} + { setSearch(v); setPage(0); }} + searchPlaceholder="Search ID, customer, pickup, drop…" + filters={[ + { + label: 'Client', value: tenant, onChange: (v) => { setTenant(v); setPage(0); }, width: 170, + options: [{ value: 'all', label: 'All Clients' }, ...[...new Set(orders.map((o) => o.tenant))].map((t) => ({ value: t, label: t }))] + } + ]} + actions={ + selected.length > 0 && ( + + ) + } + /> - - + +
@@ -140,56 +142,56 @@ export default function OrdersList() { onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])} /> - # - Tenant + Order ID + Client Location - Pickup - Drop - QTY + Route + Qty COD - KMS + KM Charges - Notes Status - Actions + Actions {paged.map((o) => ( toggle(o.id)} /> - navigate(`/orders/${o.id}`)}>{o.id} + navigate(`/orders/${o.id}`)}>{o.id} {o.tenant} - {o.location} - {o.pickup} - {o.drop} + {o.location} + + {o.pickup} → {o.drop} + {o.qty} {o.cod ? inr(o.cod) : '—'} {o.kms} {inr(o.charges)} - {o.notes || '—'} - + navigate(`/orders/${o.id}`)}> ))} + {paged.length === 0 && ( + + + No orders match these filters. + + + )}
+ setPage(p)} - rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} + rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[10, 25, 50]} />
- - } sx={{ position: 'fixed', bottom: 28, right: 28 }} FabProps={{ color: 'primary' }}> - } tooltipTitle="AI Optimisation" onClick={() => navigate('/orders/assign')} /> - } tooltipTitle="Manual Assign" onClick={() => navigate('/orders/assign')} /> - } tooltipTitle="Delete" /> - ); } diff --git a/src/pages/reports/OrdersDetails.jsx b/src/pages/reports/OrdersDetails.jsx index e052756..3cedd48 100644 --- a/src/pages/reports/OrdersDetails.jsx +++ b/src/pages/reports/OrdersDetails.jsx @@ -13,15 +13,19 @@ import CloseIcon from '@mui/icons-material/Close'; import PageHeader from '@/components/PageHeader'; import StatusChip from '@/components/StatusChip'; import MapPlaceholder from '@/components/MapPlaceholder'; -import { ordersDetailReport, locations, tenantsList } from '@/data/mock'; +import DateRangeFilter from '@/components/DateRangeFilter'; +import FilterSummary from '@/components/FilterSummary'; +import { ordersDetailReport, orders, tenantsList } from '@/data/mock'; import { inr } from '@/utils/format'; +import { useFilters, inRange } from '@/store/Filters'; const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled']; +// join the report to raw orders for the date + location each row actually belongs to +const ORDER_META = Object.fromEntries(orders.map((o) => [o.id, { date: o.date, location: o.location }])); export default function OrdersDetails() { - const [location, setLocation] = useState('all'); + const { location, range } = useFilters(); // global location + date range const [tenant, setTenant] = useState('all'); - const [loc2, setLoc2] = useState('all'); const [status, setStatus] = useState('all'); const [search, setSearch] = useState(''); const [page, setPage] = useState(0); @@ -32,16 +36,21 @@ export default function OrdersDetails() { const filtered = useMemo( () => ordersDetailReport.filter((o) => { + const meta = ORDER_META[o.id] || {}; const matchStatus = status === 'all' || o.status === status; const matchTenant = tenant === 'all' || o.client === tenant; + const matchLoc = location === 'all' || meta.location === location; + const matchDate = meta.date ? inRange(meta.date, range) : true; const matchSearch = !search || [o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase()); - return matchStatus && matchTenant && matchSearch; + return matchStatus && matchTenant && matchLoc && matchDate && matchSearch; }), - [status, tenant, search] + [status, tenant, search, location, range] ); + // reset to first page whenever the result set changes + useMemo(() => setPage(0), [status, tenant, search, location, range]); // eslint-disable-line react-hooks/exhaustive-deps const paged = filtered.slice(page * rpp, page * rpp + rpp); return ( @@ -49,12 +58,7 @@ export default function OrdersDetails() { setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - } + action={} /> @@ -63,13 +67,6 @@ export default function OrdersDetails() { All Tenants {tenantsList.map((t) => {t})} - setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status"> {STATUSES.map((s) => {s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)})} @@ -83,6 +80,10 @@ export default function OrdersDetails() { + + + + @@ -130,6 +131,13 @@ export default function OrdersDetails() { {inr(o.charges)} ))} + {filtered.length === 0 && ( + + + No orders in this date range / location. Try widening the range or clearing the location. + + + )}
@@ -163,11 +171,10 @@ export default function OrdersDetails() { The export will include {filtered.length} record(s) matching the current filters: + - - diff --git a/src/pages/reports/OrdersSummary.jsx b/src/pages/reports/OrdersSummary.jsx index f97320a..c2f5c3c 100644 --- a/src/pages/reports/OrdersSummary.jsx +++ b/src/pages/reports/OrdersSummary.jsx @@ -9,8 +9,11 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import PageHeader from '@/components/PageHeader'; -import { ordersSummary, locations, tenantsList } from '@/data/mock'; +import DateRangeFilter from '@/components/DateRangeFilter'; +import FilterSummary from '@/components/FilterSummary'; +import { ordersSummary, tenantsList } from '@/data/mock'; import { inr } from '@/utils/format'; +import { useFilters } from '@/store/Filters'; // Show 0 in red, anything else normally. function NumCell({ value, align = 'center', bold = false }) { @@ -23,14 +26,16 @@ function NumCell({ value, align = 'center', bold = false }) { } export default function OrdersSummary() { + const { location } = useFilters(); // global location — single source of truth const [open, setOpen] = useState({}); - const [location, setLocation] = useState('all'); const [tenant, setTenant] = useState('all'); - const [loc2, setLoc2] = useState('all'); const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); - const totals = ordersSummary.reduce( + // location is the global filter; tenant is this report's own (distinct purpose) + const rows = ordersSummary.filter((r) => (location === 'all' || r.location === location) && (tenant === 'all' || r.tenant === tenant)); + + const totals = rows.reduce( (a, r) => ({ oPending: a.oPending + r.orders.pending, oCancelled: a.oCancelled + r.orders.cancelled, @@ -46,35 +51,24 @@ export default function OrdersSummary() { { oPending: 0, oCancelled: 0, oCompleted: 0, dPending: 0, dCancelled: 0, dCompleted: 0, collection: 0, kms: 0, actualKms: 0, amount: 0 } ); - const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; + const headSx = { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', whiteSpace: 'nowrap', borderBottom: '2px solid', borderColor: 'grey.200' }; return ( <> setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - } + action={} /> - - setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> All Tenants {tenantsList.map((t) => {t})} - setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - + + @@ -101,7 +95,10 @@ export default function OrdersSummary() { - {ordersSummary.map((r, idx) => ( + {rows.length === 0 && ( + No tenants match this location. + )} + {rows.map((r, idx) => ( @@ -172,7 +169,7 @@ export default function OrdersSummary() { ))} {/* totals */} - + Totals diff --git a/src/pages/reports/ReportsHub.jsx b/src/pages/reports/ReportsHub.jsx new file mode 100644 index 0000000..a59e811 --- /dev/null +++ b/src/pages/reports/ReportsHub.jsx @@ -0,0 +1,21 @@ +import TabbedWorkspace from '@/components/TabbedWorkspace'; +import OrdersSummary from '@/pages/reports/OrdersSummary'; +import RidersSummary from '@/pages/reports/RidersSummary'; +import Analytics from '@/pages/analytics/Analytics'; +import OrdersDetails from '@/pages/reports/OrdersDetails'; + +// ==============================|| REPORTS — reporting center ||============================== // +// Merges the report surfaces into one tabbed center: Orders, Riders, Revenue, Performance. + +export default function ReportsHub() { + return ( + }, + { key: 'riders', label: 'Riders', element: }, + { key: 'revenue', label: 'Revenue', element: }, + { key: 'performance', label: 'Performance', element: } + ]} + /> + ); +} diff --git a/src/pages/reports/RidersSummary.jsx b/src/pages/reports/RidersSummary.jsx index 1203e58..e6f43f6 100644 --- a/src/pages/reports/RidersSummary.jsx +++ b/src/pages/reports/RidersSummary.jsx @@ -13,7 +13,9 @@ import CloseIcon from '@mui/icons-material/Close'; import PageHeader from '@/components/PageHeader'; import UserAvatar from '@/components/UserAvatar'; import MapPlaceholder from '@/components/MapPlaceholder'; -import { ridersSummary, locations } from '@/data/mock'; +import DateRangeFilter from '@/components/DateRangeFilter'; +import FilterSummary from '@/components/FilterSummary'; +import { ridersSummary } from '@/data/mock'; import { inr } from '@/utils/format'; function NumCell({ value, align = 'center' }) { @@ -32,32 +34,24 @@ function KmsChips({ kms, actual }) { export default function RidersSummary() { const [open, setOpen] = useState({}); - const [location, setLocation] = useState('all'); const [mapRider, setMapRider] = useState(null); const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); const totalAmount = ridersSummary.reduce((a, r) => a + r.amount, 0); - const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; + const headSx = { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', whiteSpace: 'nowrap', borderBottom: '2px solid', borderColor: 'grey.200' }; return ( <> setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location"> - All Locations - {locations.map((l) => {l})} - - } + action={} /> - + diff --git a/src/pages/riders/Riders.jsx b/src/pages/riders/Riders.jsx index fc03fad..f16ba67 100644 --- a/src/pages/riders/Riders.jsx +++ b/src/pages/riders/Riders.jsx @@ -20,7 +20,8 @@ import StatCard from '@/components/StatCard'; import StatusChip from '@/components/StatusChip'; import UserAvatar from '@/components/UserAvatar'; import TabLabelCount from '@/components/TabLabelCount'; -import { riders, riderLogs, locations } from '@/data/mock'; +import { riders, riderLogs } from '@/data/mock'; +import { useFilters } from '@/store/Filters'; import { inr } from '@/utils/format'; const TABS = [ @@ -32,7 +33,7 @@ export default function Riders() { const navigate = useNavigate(); const [tab, setTab] = useState(0); const [search, setSearch] = useState(''); - const [location, setLocation] = useState('all'); + const { location } = useFilters(); // global location — single source of truth const [page, setPage] = useState(0); const [rpp, setRpp] = useState(5); const [expanded, setExpanded] = useState(null); @@ -72,10 +73,6 @@ export default function Riders() { breadcrumbs={[{ label: 'Riders' }]} action={ - setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location"> - All Locations - {locations.map((l) => {l})} - diff --git a/src/pages/tracking/LiveTracking.jsx b/src/pages/tracking/LiveTracking.jsx index 2316290..6b27609 100644 --- a/src/pages/tracking/LiveTracking.jsx +++ b/src/pages/tracking/LiveTracking.jsx @@ -1,186 +1,224 @@ -import { useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Grid, Stack, Typography, Box, Avatar, Table, TableBody, TableCell, TableHead, TableRow, Button, Chip, TextField, InputAdornment, Divider } from '@mui/material'; -import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; -import PhotoCameraOutlinedIcon from '@mui/icons-material/PhotoCameraOutlined'; +import { Box, Stack, Typography, Button, TextField, InputAdornment } from '@mui/material'; import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; -import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; +import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined'; import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; -import PageHeader from '@/components/PageHeader'; -import StatCard from '@/components/StatCard'; -import MainCard from '@/components/MainCard'; -import StatusChip from '@/components/StatusChip'; -import LayerBanner from '@/components/LayerBanner'; -import MapPlaceholder from '@/components/MapPlaceholder'; +import TrackingControlBar from '@/components/tracking/TrackingControlBar'; +import ActiveRidersList from '@/components/tracking/ActiveRidersList'; +import FleetMap from '@/components/tracking/FleetMap'; +import RiderTimeline from '@/components/tracking/RiderTimeline'; import FormDialog from '@/components/FormDialog'; import Toast, { useToast } from '@/components/Toast'; -import { executionStages, executionFeed, ridersLive, orderTimeline } from '@/data/mock'; +import { activeDeliveries, fleetVehicles, riders } from '@/data/mock'; +import { snapRoutes } from '@/utils/osrm'; +import { useOps } from '@/store/OpsStore'; + +const now = () => new Date().toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); + +// Heavy children only re-render when their own props change (list runs at ~1fps, map at ~30fps). +const MemoFleetMap = memo(FleetMap); +const MemoActiveRiders = memo(ActiveRidersList); + +const smoothstep = (x) => { const t = Math.max(0, Math.min(1, x)); return t * t * (3 - 2 * t); }; +const STOP_WINDOW = 0.08; // how far ahead a vehicle starts braking for a stop (route fraction) + +// Speed envelope (0..1): eases away from the pickup, into the drop, and dips toward each +// in-route stop (signals / waypoints) so motion reads as real traffic rather than a constant glide. +function speedEnvelope(p, stops) { + let e = 0.22 + 0.78 * smoothstep(p / 0.12) * smoothstep((1 - p) / 0.12); + for (let i = 0; i < stops.length; i += 1) { + e *= 0.12 + 0.88 * smoothstep(Math.abs(p - stops[i]) / STOP_WINDOW); + } + return e; +} + +// Per-vehicle motion character, derived deterministically from the shipment id so every +// vehicle has its own cruising speed, traffic rhythm, stops and dwell — no two move identically. +const MOTION = activeDeliveries.reduce((acc, d) => { + let h = 0; + for (let i = 0; i < d.id.length; i += 1) h = (h * 31 + d.id.charCodeAt(i)) >>> 0; + const r = (h % 1000) / 1000; + const r2 = ((h >>> 10) % 1000) / 1000; + const r3 = ((h >>> 20) % 1000) / 1000; + acc[d.id] = { + speed: 0.5 + r * 0.65, // base cruising %/s + freq: 0.14 + r2 * 0.16, // traffic-rhythm frequency + phase: r * Math.PI * 2, + stops: [0.28 + r2 * 0.1, 0.62 + r3 * 0.12], // in-route halts (signals / drops) + dwell: [1.2 + r * 1.8, 1.0 + r3 * 1.8] // seconds paused at each halt + }; + return acc; +}, {}); + +// Build the live delivery view-model from a progress map (shared by the map & queue lanes). +function buildDeliveries(progress, assignments, exceptions, snapped) { + return activeDeliveries.map((d) => { + const a = assignments[d.id]; + const ex = exceptions[d.id]; + const pe = progress[d.id] ?? d.progress; + return { + ...d, + route: snapped[d.id] || d.route, + progress: Math.round(pe), + progressExact: pe, + rider: a ? a.riderName : d.rider, + status: ex ? 'Exception' : d.status, + etaStatus: ex ? 'delayed' : d.etaStatus, + flagged: Boolean(ex) + }; + }); +} export default function LiveTracking() { const navigate = useNavigate(); - const [track, setTrack] = useState(null); const [share, setShare] = useState(false); const [toast, showToast] = useToast(); - const trackLink = track ? `https://track.doormile.com/${track.id}` : 'https://track.doormile.com'; + const [selectedId, setSelectedId] = useState(null); + const { assignments, exceptions, assignOrder, rerouteOrder, raiseException } = useOps(); + const availableRiders = useMemo(() => riders.filter((r) => r.status !== 'offline'), []); + + // snap every shipment's route to real streets via OSRM (falls back to the drawn path on failure) + const [snapped, setSnapped] = useState({}); + useEffect(() => { + const ctrl = new AbortController(); + snapRoutes(activeDeliveries, { signal: ctrl.signal }).then(setSnapped).catch(() => {}); + return () => ctrl.abort(); + }, []); + + // live simulation — advance in-flight shipments along their routes with natural, eased motion. + // Two cadences: `progress` drives the map at ~30fps; `queueProgress` refreshes the list ~1×/s. + const [progress, setProgress] = useState(() => Object.fromEntries(activeDeliveries.map((d) => [d.id, d.progress]))); + const [queueProgress, setQueueProgress] = useState(progress); + const [updated, setUpdated] = useState(now); + const baseProgress = useRef(Object.fromEntries(activeDeliveries.map((d) => [d.id, d.progress]))); + + useEffect(() => { + const work = { ...baseProgress.current }; // local accumulator advanced every frame + const runtime = {}; // per-vehicle dwell state (stop timer + next-stop pointer) + let raf; + let last = performance.now(); + let elapsed = 0; + let sinceMap = 0; + let sinceQueue = 0; + let sinceClock = 0; + + const step = (t) => { + const dt = Math.min((t - last) / 1000, 0.05); + last = t; + elapsed += dt; + sinceMap += dt; + sinceQueue += dt; + sinceClock += dt; + + activeDeliveries.forEach((d) => { + if (d.status === 'Delivered') return; + const m = MOTION[d.id]; + const rt = runtime[d.id] || (runtime[d.id] = { dwell: 0, stopIdx: 0 }); + const cur = work[d.id] ?? d.progress; + + if (rt.dwell > 0) { rt.dwell -= dt; return; } // paused at a halt — hold position + + const p = cur / 100; + const traffic = 0.82 + 0.18 * Math.sin(elapsed * m.freq + m.phase); // gentle ±18% breathing + const v = m.speed * traffic * speedEnvelope(p, m.stops); // %/s with accel/decel + stop braking + let next = cur + v * dt; + + // arrive at the next scheduled halt → clamp and start its dwell + if (rt.stopIdx < m.stops.length && next / 100 >= m.stops[rt.stopIdx]) { + next = m.stops[rt.stopIdx] * 100; + rt.dwell = m.dwell[rt.stopIdx]; + rt.stopIdx += 1; + } + + work[d.id] = d.status === 'Exception' + ? Math.min(next, Math.min(baseProgress.current[d.id] + 6, 92)) + : Math.min(next, 100); // arrive and hold — vehicles never teleport back + }); + + if (sinceMap >= 0.033) { sinceMap = 0; setProgress({ ...work }); } + if (sinceQueue >= 1) { sinceQueue = 0; setQueueProgress({ ...work }); } + if (sinceClock >= 1) { sinceClock = 0; setUpdated(now()); } + raf = requestAnimationFrame(step); + }; + raf = requestAnimationFrame(step); + return () => cancelAnimationFrame(raf); + }, []); + + // map lane (fast) and queue lane (slow) view-models — each recomputed only when its inputs change + const mapDeliveries = useMemo(() => buildDeliveries(progress, assignments, exceptions, snapped), [progress, assignments, exceptions, snapped]); + const queueDeliveries = useMemo(() => buildDeliveries(queueProgress, assignments, exceptions, snapped), [queueProgress, assignments, exceptions, snapped]); + + // operator actions — every one commits to OpsStore + const actions = useMemo( + () => ({ + reassign: (id, rider) => { assignOrder(id, rider); showToast(`${id} reassigned to ${rider.name}`); }, + reroute: (id) => { rerouteOrder(id); showToast(`${id} rerouted · ETA protected`); }, + call: (d) => showToast(`Calling ${d.rider}…`), + message: (d) => showToast(`Message sent to ${d.rider}`), + flag: (id) => { raiseException(id, 'Flagged from control tower'); showToast(`${id} flagged as exception`, 'warning'); }, + open: (id) => navigate(`/orders/${id}`) + }), + [assignOrder, rerouteOrder, raiseException, showToast, navigate] + ); + + const toggleSelect = useCallback((id) => setSelectedId((cur) => (cur === id ? null : id)), []); + const selectedDelivery = useMemo(() => queueDeliveries.find((d) => d.id === selectedId) || null, [queueDeliveries, selectedId]); + const tracking = Boolean(selectedId); // Selected Tracking Mode + + const trackLink = selectedId ? `https://track.doormile.com/${selectedId}` : 'https://track.doormile.com'; const copyLink = () => { if (navigator.clipboard) navigator.clipboard.writeText(trackLink).catch(() => {}); showToast('Tracking link copied'); }; return ( - <> - } onClick={() => setShare(true)}>Share Tracking} - /> + + {/* compact header */} + + + Live Tracking + Fleet control tower · real-time operations + + + + + + + - + - - {executionStages.map((s) => ( - - - {s.count.toLocaleString('en-IN')} - {s.title} - - {s.items.slice(0, 3).map((it) => ( - • {it} - ))} - + {/* control tower → Selected Tracking Mode: queue collapses, map expands, feed → rider timeline */} + + {/* map — the primary surface; every rider marker stays visible & clickable */} + + + + + {/* right rail — Active Riders roster (top) + Selected Rider timeline (bottom) */} + + + + + {tracking && ( + + setSelectedId(null)} /> - - ))} - - - - ({ - x: ['28%', '52%', '70%', '40%'][i], - y: ['44%', '30%', '58%', '66%'][i], - active: r.active - }))} - /> - - - - - - - {executionFeed.map((e, i) => ( - - - - {stageIcon(e.stage)} - - - - {e.id} - {e.time} - - - - {e.proof && } label="Proof" sx={{ bgcolor: 'grey.100' }} />} - - {e.rider} · {e.loc} - {e.detail} - - - - ))} - - - - - - - - - - Shipment - Rider - Current Stage - Location - Update - Tracking - - - - {executionFeed.map((e) => ( - - {e.id} - {e.rider} - - {e.loc} - {e.detail} - - - - - ))} - -
-
-
-
- - {/* Tracking detail */} - setTrack(null)} - title={track ? `Tracking · ${track.id}` : 'Tracking'} - hideActions - > - {track && ( - - - - Updated {track.time} - - - Rider{track.rider} - Location{track.loc} - - {track.detail} - - - - - Journey - - {orderTimeline.map((t, i) => ( - - {t.done ? : } - {t.label} - {t.time} - - ))} - - - - - - - - - - )} - + )} +
+
{/* Share tracking */} setShare(false)} title="Share Live Tracking" hideActions> @@ -207,28 +245,6 @@ export default function LiveTracking() { - + ); } - -const mapStage = (stage) => { - const k = { 'In-Transit': 'in-transit', 'Picked Up': 'picked-up', Delivered: 'delivered', Exception: 'exception', Dispatched: 'dispatched' }; - return k[stage] || 'active'; -}; - -function stageStyle(stage) { - const m = { - 'In-Transit': { bgcolor: 'info.lighter', color: 'info.main' }, - 'Picked Up': { bgcolor: 'primary.lighter', color: 'primary.main' }, - Delivered: { bgcolor: 'success.lighter', color: 'success.main' }, - Exception: { bgcolor: 'error.lighter', color: 'error.main' }, - Dispatched: { bgcolor: 'grey.100', color: 'grey.700' } - }; - return m[stage] || m.Dispatched; -} - -function stageIcon(stage) { - if (stage === 'Exception') return ; - if (stage === 'In-Transit' || stage === 'Dispatched') return ; - return ; -} diff --git a/src/pages/tracking/ShipmentJourney.jsx b/src/pages/tracking/ShipmentJourney.jsx index c59cf27..f316b23 100644 --- a/src/pages/tracking/ShipmentJourney.jsx +++ b/src/pages/tracking/ShipmentJourney.jsx @@ -1,136 +1,179 @@ -import { Grid, Stack, Typography, Box, Avatar, Button, Chip, LinearProgress, Divider } from '@mui/material'; -import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; +import { useNavigate } from 'react-router-dom'; +import { + Grid, Stack, Typography, Box, Avatar, Button, Chip, LinearProgress, Divider, + Table, TableBody, TableCell, TableHead, TableRow +} from '@mui/material'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; import CheckIcon from '@mui/icons-material/Check'; import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; +import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; +import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'; +import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; import PageHeader from '@/components/PageHeader'; import MainCard from '@/components/MainCard'; -import MapPlaceholder from '@/components/MapPlaceholder'; +import MiniMap from '@/components/tracking/MiniMap'; import Toast, { useToast } from '@/components/Toast'; -import { shipmentJourney as j } from '@/data/mock'; +import { trip, cityCenters } from '@/data/mock'; -const HOP_ICONS = { order: ReceiptLongOutlinedIcon, agent: TwoWheelerOutlinedIcon, hub: WarehouseOutlinedIcon, truck: LocalShippingOutlinedIcon, done: HomeOutlinedIcon }; +const FROM = cityCenters[trip.from.city] || cityCenters.Coimbatore; +const TO = cityCenters[trip.to.city] || cityCenters.Bengaluru; +const ROUTE = [[FROM.lat, FROM.lng], [TO.lat, TO.lng]]; + +function Meta({ label, value, sub }) { + return ( + + {label} + {value} + {sub && {sub}} + + ); +} export default function ShipmentJourney() { + const navigate = useNavigate(); const [toast, showToast] = useToast(); - const doneCount = j.hops.filter((h) => h.status === 'done').length; + const doneCount = trip.milestones.filter((m) => m.status === 'done').length; return ( <> - - + + } /> - {/* Summary */} + {/* Trip summary — one vehicle, one corridor, one manifest */} - - {j.id} · {j.client} - + + + {trip.id} + + + - {j.from.city} - {j.from.area} + {trip.from.city} + {trip.from.hub} - + - {j.to.city} - {j.to.area} + {trip.to.city} + {trip.to.hub} - {j.product} · {j.mode} - - - - + + + + + Progress - {j.progress}% - + {trip.progress}% + - {/* Hop-by-hop timeline */} + {/* Manifest — every shipment on this trip (the scalable centerpiece) */} - - - {j.hops.map((h, i) => { - const Icon = HOP_ICONS[h.icon] || WarehouseOutlinedIcon; - const last = i === j.hops.length - 1; - const newMile = i === 0 || j.hops[i - 1].mile !== h.mile; - return ( - - {newMile && ( - - {h.mile} - - )} - - {/* rail */} - - - {h.status === 'done' ? : } - - {!last && } - - {/* content */} - - - {h.title} - {h.status === 'active' && } - - {h.node} - {h.handler} - {h.detail} - {h.time} - - - - ); - })} + + + Manifest + + + } + action={{trip.pieces} pieces · {trip.loadKg} kg} + noPadding + > + + + + + Shipment + Customer + Drop Area + Pcs + Weight + Status + + + + + {trip.manifest.map((s) => ( + navigate(`/orders/${s.id}`)}> + {s.id} + + {s.customer} + {s.client} + + {s.dropArea} + {s.pieces} + {s.weightKg} kg + + + + ))} + +
- {/* Map + live monitoring */} + {/* Map + trip milestones + monitoring — all describe the TRIP */} - - + + + + + + + + + {trip.milestones.map((m, i) => { + const last = i === trip.milestones.length - 1; + return ( + + + + {m.status === 'done' ? : } + + {!last && } + + + + {m.title} + {m.status === 'active' && } + + {m.node} + {m.detail} + {m.time} + + + ); + })} + } spacing={0}> - {j.events.map((e, i) => ( + {trip.events.map((e, i) => ( {e.type === 'reroute' ? : } @@ -163,12 +206,3 @@ export default function ShipmentJourney() { ); } - -function Summary({ label, value, small }) { - return ( - - {label} - {value} - - ); -} diff --git a/src/store/Filters.jsx b/src/store/Filters.jsx new file mode 100644 index 0000000..4c79d9f --- /dev/null +++ b/src/store/Filters.jsx @@ -0,0 +1,75 @@ +import { createContext, useContext, useState, useMemo, useEffect } from 'react'; +import dayjs from 'dayjs'; + +// ==============================|| GLOBAL FILTERS — single source of truth ||============================== // +// One place for the application-wide Location and Date Range. The header Location selector and every +// report read/write these, so there is exactly one source of truth (no duplicate, conflicting filters). +// Persisted to localStorage so the selection survives refresh, tab switches and navigation. +// +// Presets are anchored to the dataset's reference day (not the wall clock) so they filter the demo +// data sensibly — e.g. Last 7 Days ≠ Last 30 Days, Today ≠ This Month. + +export const DATA_TODAY = dayjs('2026-06-05'); +const fmt = (d) => d.format('MMM DD'); + +export function buildRange(key) { + const t = DATA_TODAY; + switch (key) { + case 'today': return { key, label: 'Today', start: t.startOf('day'), end: t.endOf('day') }; + case 'yesterday': { const y = t.subtract(1, 'day'); return { key, label: 'Yesterday', start: y.startOf('day'), end: y.endOf('day') }; } + case 'last7': return { key, label: 'Last 7 Days', start: t.subtract(6, 'day').startOf('day'), end: t.endOf('day') }; + case 'last30': return { key, label: 'Last 30 Days', start: t.subtract(29, 'day').startOf('day'), end: t.endOf('day') }; + case 'thisMonth': return { key, label: 'This Month', start: t.startOf('month'), end: t.endOf('month') }; + case 'prevMonth': { const p = t.subtract(1, 'month'); return { key, label: 'Previous Month', start: p.startOf('month'), end: p.endOf('month') }; } + default: return { key: 'last30', label: 'Last 30 Days', start: t.subtract(29, 'day').startOf('day'), end: t.endOf('day') }; + } +} + +export function customRange(start, end) { + return { key: 'custom', label: `${fmt(start)} – ${fmt(end)}`, start: start.startOf('day'), end: end.endOf('day') }; +} + +// inclusive, day-granular date-in-range test +export function inRange(dateStr, range) { + if (!range?.start || !range?.end) return true; + const d = dayjs(dateStr).startOf('day'); + return !d.isBefore(range.start.startOf('day')) && !d.isAfter(range.end.startOf('day')); +} + +const FilterContext = createContext(null); +const STORE_KEY = 'dm.filters'; + +export function FilterProvider({ children }) { + const [location, setLocation] = useState('all'); + const [range, setRange] = useState(() => buildRange('last30')); + + // hydrate once + useEffect(() => { + try { + const raw = localStorage.getItem(STORE_KEY); + if (!raw) return; + const s = JSON.parse(raw); + if (s.location) setLocation(s.location); + if (s.rangeKey === 'custom' && s.start && s.end) setRange(customRange(dayjs(s.start), dayjs(s.end))); + else if (s.rangeKey) setRange(buildRange(s.rangeKey)); + } catch { /* ignore corrupt storage */ } + }, []); + + // persist on change + useEffect(() => { + try { + localStorage.setItem(STORE_KEY, JSON.stringify({ + location, rangeKey: range.key, start: range.start?.toISOString(), end: range.end?.toISOString() + })); + } catch { /* storage unavailable */ } + }, [location, range]); + + const value = useMemo(() => ({ location, setLocation, range, setRange }), [location, range]); + return {children}; +} + +export function useFilters() { + const ctx = useContext(FilterContext); + if (!ctx) throw new Error('useFilters must be used within a FilterProvider'); + return ctx; +} diff --git a/src/store/OpsStore.jsx b/src/store/OpsStore.jsx new file mode 100644 index 0000000..c5437f9 --- /dev/null +++ b/src/store/OpsStore.jsx @@ -0,0 +1,75 @@ +import { createContext, useContext, useState, useCallback, useMemo } from 'react'; + +// ==============================|| OPS STORE — SESSION PERSISTENCE ||============================== // +// A lightweight in-memory store so operational actions actually COMMIT during a session (no backend). +// Holds: dispatch assignments (order → rider), reroutes, and exceptions. Future workflows (status +// advance, payments) hang off the same provider. State resets on full reload — intentional for a demo. + +const OpsContext = createContext(null); + +export function OpsProvider({ children }) { + // assignments: { [orderId]: { riderId, riderName, vehicle, at } } + const [assignments, setAssignments] = useState({}); + // reroutes: { [orderId]: at } + const [reroutes, setReroutes] = useState({}); + // exceptions: { [orderId]: { reason, at } } + const [exceptions, setExceptions] = useState({}); + + const assignOrder = useCallback((orderId, rider) => { + setAssignments((prev) => ({ + ...prev, + [orderId]: { riderId: rider.id, riderName: rider.name, vehicle: rider.vehicle, at: Date.now() } + })); + }, []); + + const unassignOrder = useCallback((orderId) => { + setAssignments((prev) => { + const next = { ...prev }; + delete next[orderId]; + return next; + }); + }, []); + + const rerouteOrder = useCallback((orderId) => { + setReroutes((prev) => ({ ...prev, [orderId]: Date.now() })); + }, []); + + const raiseException = useCallback((orderId, reason) => { + setExceptions((prev) => ({ ...prev, [orderId]: { reason, at: Date.now() } })); + }, []); + + const clearException = useCallback((orderId) => { + setExceptions((prev) => { + const next = { ...prev }; + delete next[orderId]; + return next; + }); + }, []); + + const assignmentOf = useCallback((orderId) => assignments[orderId] || null, [assignments]); + const exceptionOf = useCallback((orderId) => exceptions[orderId] || null, [exceptions]); + const isRerouted = useCallback((orderId) => Boolean(reroutes[orderId]), [reroutes]); + const riderLoad = useCallback( + (riderId) => Object.values(assignments).filter((a) => a.riderId === riderId).length, + [assignments] + ); + + const value = useMemo( + () => ({ + assignments, reroutes, exceptions, + assignOrder, unassignOrder, rerouteOrder, raiseException, clearException, + assignmentOf, exceptionOf, isRerouted, riderLoad, + assignedCount: Object.keys(assignments).length, + exceptionCount: Object.keys(exceptions).length + }), + [assignments, reroutes, exceptions, assignOrder, unassignOrder, rerouteOrder, raiseException, clearException, assignmentOf, exceptionOf, isRerouted, riderLoad] + ); + + return {children}; +} + +export function useOps() { + const ctx = useContext(OpsContext); + if (!ctx) throw new Error('useOps must be used within an OpsProvider'); + return ctx; +} diff --git a/src/utils/geo.js b/src/utils/geo.js new file mode 100644 index 0000000..990559e --- /dev/null +++ b/src/utils/geo.js @@ -0,0 +1,49 @@ +// ==============================|| GEO HELPERS — route interpolation & bearing ||============================== // +// Lightweight planar math (good enough for city-scale visualisation) used to place a vehicle +// along its route from a progress %, split a route into completed / remaining paths, and +// compute heading so markers can rotate to face their direction of travel. + +const seg = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]); + +export function bearing(a, b) { + const dLng = ((b[1] - a[1]) * Math.PI) / 180; + const lat1 = (a[0] * Math.PI) / 180; + const lat2 = (b[0] * Math.PI) / 180; + const y = Math.sin(dLng) * Math.cos(lat2); + const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng); + return (((Math.atan2(y, x) * 180) / Math.PI) + 360) % 360; +} + +// Returns { point: [lat,lng], bearing, completed: [...], remaining: [...] } for a progress 0..100. +export function pointAlongRoute(route, progress) { + if (!route || route.length === 0) return null; + if (route.length === 1) return { point: route[0], bearing: 0, completed: [route[0]], remaining: [route[0]] }; + + const lengths = []; + let total = 0; + for (let i = 0; i < route.length - 1; i++) { + const l = seg(route[i], route[i + 1]); + lengths.push(l); + total += l; + } + + const target = Math.min(Math.max(progress, 0), 100) / 100 * total; + let acc = 0; + for (let i = 0; i < lengths.length; i++) { + if (acc + lengths[i] >= target || i === lengths.length - 1) { + const t = lengths[i] === 0 ? 0 : (target - acc) / lengths[i]; + const a = route[i]; + const b = route[i + 1]; + const point = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]; + return { + point, + bearing: bearing(a, b), + completed: [...route.slice(0, i + 1), point], + remaining: [point, ...route.slice(i + 1)] + }; + } + acc += lengths[i]; + } + const last = route[route.length - 1]; + return { point: last, bearing: 0, completed: route, remaining: [last] }; +} diff --git a/src/utils/osrm.js b/src/utils/osrm.js new file mode 100644 index 0000000..b7ea8bd --- /dev/null +++ b/src/utils/osrm.js @@ -0,0 +1,43 @@ +// ==============================|| OSRM ROUTING — snap routes to real streets ||============================== // +// Turns a list of [lat,lng] waypoints into road-following geometry via the OSRM route service. +// Results are cached in-memory; any failure (offline / rate-limit) returns null so callers can +// fall back to their hand-drawn polyline. Uses the public demo server — swap OSRM_HOST for a +// self-hosted instance in production (the demo server is not for production traffic). + +const OSRM_HOST = 'https://router.project-osrm.org'; +const cache = new Map(); + +const key = (waypoints) => waypoints.map((p) => p.join(',')).join(';'); + +export async function snapRoute(waypoints, { signal } = {}) { + if (!waypoints || waypoints.length < 2) return null; + const k = key(waypoints); + if (cache.has(k)) return cache.get(k); + + const coords = waypoints.map(([lat, lng]) => `${lng},${lat}`).join(';'); + const url = `${OSRM_HOST}/route/v1/driving/${coords}?overview=full&geometries=geojson`; + + try { + const res = await fetch(url, { signal }); + if (!res.ok) throw new Error(`OSRM ${res.status}`); + const data = await res.json(); + if (data.code !== 'Ok' || !data.routes?.length) throw new Error(`OSRM ${data.code}`); + const line = data.routes[0].geometry.coordinates.map(([lng, lat]) => [lat, lng]); + cache.set(k, line); + return line; + } catch (err) { + if (err.name !== 'AbortError') cache.set(k, null); // remember the failure, don't hammer + return null; + } +} + +// Snap many routes at once. Returns { [id]: [[lat,lng], …] } only for the ones that succeeded. +export async function snapRoutes(items, { signal } = {}) { + const entries = await Promise.all( + items.map(async (it) => { + const line = await snapRoute(it.route, { signal }); + return line ? [it.id, line] : null; + }) + ); + return Object.fromEntries(entries.filter(Boolean)); +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..ac13fa5 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1291 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + +"@babel/core@^7.28.0": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" + integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== + dependencies: + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== + dependencies: + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" + integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== + dependencies: + "@babel/types" "^7.29.7" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz#c24424527858220624fd59a5b1eab4fa413c803a" + integrity sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz#5cf25a3689906b58e2f0a2f2b374789e6627b15f" + integrity sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.26.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" + integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== + +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + +"@babel/traverse@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" + integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.29.7": + version "7.29.7" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" + integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@^1.3.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz#e9ad47adff0b5c94c72db3669ce46de33edf28c0" + integrity sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw== + dependencies: + "@emotion/memoize" "^0.9.0" + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.11.4": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/styled@^11.11.5": + version "11.14.1" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz#8c34bed2948e83e1980370305614c20955aacd1c" + integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/is-prop-valid" "^1.3.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@floating-ui/core@^1.7.5": + version "1.7.5" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz#d4af157a03330af5a60e69da7a4692507ada0622" + integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== + dependencies: + "@floating-ui/utils" "^0.2.11" + +"@floating-ui/dom@^1.7.6": + version "1.7.6" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz#f915bba5abbb177e1f227cacee1b4d0634b187bf" + integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== + dependencies: + "@floating-ui/core" "^1.7.5" + "@floating-ui/utils" "^0.2.11" + +"@floating-ui/react-dom@^2.0.8", "@floating-ui/react-dom@^2.1.1": + version "2.1.8" + resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz#5fb5a20d10aafb9505f38c24f38d00c8e1598893" + integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A== + dependencies: + "@floating-ui/dom" "^1.7.6" + +"@floating-ui/utils@^0.2.11": + version "0.2.11" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" + integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mui/base@5.0.0-beta.40-1": + version "5.0.0-beta.40-1" + resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-1.tgz#6da6229e5e675e811f319149f6e29d7a77522851" + integrity sha512-agKXuNNy0bHUmeU7pNmoZwNFr7Hiyhojkb9+2PVyDG5+6RafYuyMgbrav8CndsB7KUc/U51JAw9vKNDLYBzaUA== + dependencies: + "@babel/runtime" "^7.23.9" + "@floating-ui/react-dom" "^2.0.8" + "@mui/types" "~7.2.15" + "@mui/utils" "^5.17.1" + "@popperjs/core" "^2.11.8" + clsx "^2.1.0" + prop-types "^15.8.1" + +"@mui/base@^5.0.0-beta.22": + version "5.0.0-beta.70" + resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.70.tgz#cd9755b8ae1b375fb3c985f06a4741505d793ecf" + integrity sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ== + dependencies: + "@babel/runtime" "^7.26.0" + "@floating-ui/react-dom" "^2.1.1" + "@mui/types" "~7.2.24" + "@mui/utils" "^6.4.8" + "@popperjs/core" "^2.11.8" + clsx "^2.1.1" + prop-types "^15.8.1" + +"@mui/core-downloads-tracker@^5.18.0": + version "5.18.0" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz#85019a8704b0f63305fc5600635ee663810f2b66" + integrity sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA== + +"@mui/icons-material@^5.15.20": + version "5.18.0" + resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz#97d87f1b7bee5fa7b9ba844518631de3112c1e57" + integrity sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg== + dependencies: + "@babel/runtime" "^7.23.9" + +"@mui/lab@^5.0.0-alpha.170": + version "5.0.0-alpha.177" + resolved "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.177.tgz#a02f43b36d7e204166706f2c27640f378e3d7c88" + integrity sha512-bdCxxtNjlWAgN9rtrwlmFydJ1qxA3IIbb6OlomGFsIXw0zGoHomLyjvh72q/R3yUAC0kvSef18cHY1UalLylyQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/base" "5.0.0-beta.40-1" + "@mui/system" "^5.18.0" + "@mui/types" "~7.2.15" + "@mui/utils" "^5.17.1" + clsx "^2.1.0" + prop-types "^15.8.1" + +"@mui/material@^5.15.20": + version "5.18.0" + resolved "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz#71e72d52338252edc6f8d9461e04fdf0d61905cd" + integrity sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/core-downloads-tracker" "^5.18.0" + "@mui/system" "^5.18.0" + "@mui/types" "~7.2.15" + "@mui/utils" "^5.17.1" + "@popperjs/core" "^2.11.8" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + react-is "^19.0.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.17.1": + version "5.17.1" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz#b4b6fbece27830754ef78186e3f1307dca42f295" + integrity sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/utils" "^5.17.1" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.18.0": + version "5.18.0" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz#914cca1385bb33ce0cde31721f529c8bd7fa301c" + integrity sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg== + dependencies: + "@babel/runtime" "^7.23.9" + "@emotion/cache" "^11.13.5" + "@emotion/serialize" "^1.3.3" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/system@^5.18.0": + version "5.18.0" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz#e55331203a40584b26c5a855a07949ac8973bfb6" + integrity sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/private-theming" "^5.17.1" + "@mui/styled-engine" "^5.18.0" + "@mui/types" "~7.2.15" + "@mui/utils" "^5.17.1" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/types@~7.2.15", "@mui/types@~7.2.24": + version "7.2.24" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2" + integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== + +"@mui/utils@^5.14.16", "@mui/utils@^5.17.1": + version "5.17.1" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz#72ba4ffa79f7bdf69d67458139390f18484b6e6b" + integrity sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/types" "~7.2.15" + "@types/prop-types" "^15.7.12" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.0.0" + +"@mui/utils@^6.4.8": + version "6.4.9" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz#b0df01daa254c7c32a1a30b30a5179e19ef071a7" + integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "~7.2.24" + "@types/prop-types" "^15.7.14" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.0.0" + +"@mui/x-date-pickers@^6.20.2": + version "6.20.2" + resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.20.2.tgz#b1b1e4862daafb750496cc77a1645caeac28a739" + integrity sha512-x1jLg8R+WhvkmUETRfX2wC+xJreMii78EXKLl6r3G+ggcAZlPyt0myID1Amf6hvJb9CtR7CgUo8BwR+1Vx9Ggw== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/base" "^5.0.0-beta.22" + "@mui/utils" "^5.14.16" + "@types/react-transition-group" "^4.4.8" + clsx "^2.0.0" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@react-leaflet/core@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d" + integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg== + +"@remix-run/router@1.23.3": + version "1.23.3" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz#957c098d4393d301a8aa7dccf3ef28ea5430e36a" + integrity sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q== + +"@rolldown/pluginutils@1.0.0-beta.27": + version "1.0.0-beta.27" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz#47d2bf4cef6d470b22f5831b420f8964e0bf755f" + integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA== + +"@rollup/rollup-android-arm-eabi@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz#ce83f259581a4f5e6255d92902249f0366a15dd3" + integrity sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA== + +"@rollup/rollup-android-arm64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz#1a763329ffbc2a19057128ac266a1a46782a5f17" + integrity sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw== + +"@rollup/rollup-darwin-arm64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz#ff9cffe102d29e052f49e5017fd036142f9bb7ef" + integrity sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA== + +"@rollup/rollup-darwin-x64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz#1ef1e8f5bd16865d8d2f377a58e7622820b3dda3" + integrity sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ== + +"@rollup/rollup-freebsd-arm64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz#cb7d041010788213879f663d3100c4320c0910d9" + integrity sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw== + +"@rollup/rollup-freebsd-x64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz#25d4b4d7e52bb1a144fd130209732e5d0518251a" + integrity sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw== + +"@rollup/rollup-linux-arm-gnueabihf@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz#9569a8dd884a22950df4461de8b26c750390531c" + integrity sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA== + +"@rollup/rollup-linux-arm-musleabihf@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz#33d0080b8cce62df8c3e6240875abf0d6c125cda" + integrity sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ== + +"@rollup/rollup-linux-arm64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz#9c66a9b4d1746595680eec691e136f8efcfe3d78" + integrity sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg== + +"@rollup/rollup-linux-arm64-musl@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz#e37aa97039af9dbb76a324148db06c6266acc9a0" + integrity sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w== + +"@rollup/rollup-linux-loong64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz#22951f5686b653968619d21a02b2572d5cbe51dc" + integrity sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ== + +"@rollup/rollup-linux-loong64-musl@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz#778c983d0050792d85a62e2c74009fff821e5606" + integrity sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ== + +"@rollup/rollup-linux-ppc64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz#74f34764b688a6081c8f75e155b58d2cdb39112f" + integrity sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g== + +"@rollup/rollup-linux-ppc64-musl@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz#821ccda4531dcdb42e56adddc178907615a6da07" + integrity sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw== + +"@rollup/rollup-linux-riscv64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz#849fa5c6b43fc6c4d257671fcebd701fe6947bd6" + integrity sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g== + +"@rollup/rollup-linux-riscv64-musl@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz#80a8711af6c316ce448b93294c4a0891c2ddacbe" + integrity sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ== + +"@rollup/rollup-linux-s390x-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz#90acba54363c128b73dbb310642b977b5e6b9daa" + integrity sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g== + +"@rollup/rollup-linux-x64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz#32085c3f532c59269824ed9239e13f5acbe182b9" + integrity sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q== + +"@rollup/rollup-linux-x64-musl@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz#becf0e9e29d77e7d04de841bda635e9f73b89dfb" + integrity sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw== + +"@rollup/rollup-openbsd-x64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz#d8478e23a575745f0febbde0867a133cf29fe164" + integrity sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg== + +"@rollup/rollup-openharmony-arm64@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz#b367bc49355b7ec2508cbad970721ac78b41bf0c" + integrity sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA== + +"@rollup/rollup-win32-arm64-msvc@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz#d7be3478b45d6434d13cbe62cce29c4fb6366948" + integrity sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g== + +"@rollup/rollup-win32-ia32-msvc@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz#7ec5801739cae3bf119f419b77a10cb0dc49f40f" + integrity sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ== + +"@rollup/rollup-win32-x64-gnu@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz#94a83572bf151772d945ffd4777ded305cd8c346" + integrity sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ== + +"@rollup/rollup-win32-x64-msvc@4.61.1": + version "4.61.1" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz#cb98a579ab6eec9940bda7a736df5e2b94eb0a27" + integrity sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/estree@1.0.9": + version "1.0.9" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prop-types@^15.7.12", "@types/prop-types@^15.7.14": + version "15.7.15" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.8": + version "4.4.12" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + +"@vitejs/plugin-react@^4.3.1": + version "4.7.0" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz#647af4e7bb75ad3add578e762ad984b90f4a24b9" + integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA== + dependencies: + "@babel/core" "^7.28.0" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-beta.27" + "@types/babel__core" "^7.20.5" + react-refresh "^0.17.0" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +baseline-browser-mapping@^2.10.12: + version "2.10.35" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz#f0f2232e0de2d2f82cc491bcf830b05ed05937c6" + integrity sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg== + +browserslist@^4.24.0: + version "4.28.2" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" + integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== + dependencies: + baseline-browser-mapping "^2.10.12" + caniuse-lite "^1.0.30001782" + electron-to-chromium "^1.5.328" + node-releases "^2.0.36" + update-browserslist-db "^1.2.3" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001782: + version "1.0.30001797" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz#1332709e1439f01ff92085dd17001e0a45897ec0" + integrity sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w== + +clsx@^2.0.0, clsx@^2.1.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +csstype@^3.0.2, csstype@^3.1.3: + version "3.2.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +dayjs@^1.11.11: + version "1.11.21" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + +debug@^4.1.0, debug@^4.3.1: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.5.328: + version "1.5.371" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz#fa5684f2a514c57368823f9e75553f9a7c5ef0be" + integrity sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w== + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +hasown@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.1: + version "2.16.2" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082" + integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== + dependencies: + hasown "^2.0.3" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +leaflet.markercluster@^1.5.3: + version "1.5.3" + resolved "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" + integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA== + +leaflet@^1.9.4: + version "1.9.4" + resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d" + integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.12: + version "3.3.12" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" + integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== + +node-releases@^2.0.36: + version "2.0.47" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz#521bb2786da8eb140b748841c0b3b3a75334ffc4" + integrity sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +postcss@^8.4.43: + version "8.5.15" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" + integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== + dependencies: + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^19.0.0: + version "19.2.7" + resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2" + integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A== + +react-leaflet@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780" + integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q== + dependencies: + "@react-leaflet/core" "^2.1.0" + +react-refresh@^0.17.0: + version "0.17.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53" + integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== + +react-router-dom@^6.23.1: + version "6.30.4" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz#f7167bf3da6c7d9132130ea985dd06def25e84d5" + integrity sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q== + dependencies: + "@remix-run/router" "1.23.3" + react-router "6.30.4" + +react-router@6.30.4: + version "6.30.4" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz#638f35176527bd243d96d81d35d33b757bad46c2" + integrity sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA== + dependencies: + "@remix-run/router" "1.23.3" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0: + version "1.22.12" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^4.20.0: + version "4.61.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz#4a053204912e9083e51cb3a0bf02ffdc397264fb" + integrity sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA== + dependencies: + "@types/estree" "1.0.9" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.61.1" + "@rollup/rollup-android-arm64" "4.61.1" + "@rollup/rollup-darwin-arm64" "4.61.1" + "@rollup/rollup-darwin-x64" "4.61.1" + "@rollup/rollup-freebsd-arm64" "4.61.1" + "@rollup/rollup-freebsd-x64" "4.61.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.61.1" + "@rollup/rollup-linux-arm-musleabihf" "4.61.1" + "@rollup/rollup-linux-arm64-gnu" "4.61.1" + "@rollup/rollup-linux-arm64-musl" "4.61.1" + "@rollup/rollup-linux-loong64-gnu" "4.61.1" + "@rollup/rollup-linux-loong64-musl" "4.61.1" + "@rollup/rollup-linux-ppc64-gnu" "4.61.1" + "@rollup/rollup-linux-ppc64-musl" "4.61.1" + "@rollup/rollup-linux-riscv64-gnu" "4.61.1" + "@rollup/rollup-linux-riscv64-musl" "4.61.1" + "@rollup/rollup-linux-s390x-gnu" "4.61.1" + "@rollup/rollup-linux-x64-gnu" "4.61.1" + "@rollup/rollup-linux-x64-musl" "4.61.1" + "@rollup/rollup-openbsd-x64" "4.61.1" + "@rollup/rollup-openharmony-arm64" "4.61.1" + "@rollup/rollup-win32-arm64-msvc" "4.61.1" + "@rollup/rollup-win32-ia32-msvc" "4.61.1" + "@rollup/rollup-win32-x64-gnu" "4.61.1" + "@rollup/rollup-win32-x64-msvc" "4.61.1" + fsevents "~2.3.2" + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +update-browserslist-db@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +vite@^5.3.1: + version "5.4.21" + resolved "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" + integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.10.0: + version "1.10.3" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3" + integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==