Finalize CRM transformation, mobile responsiveness, and Qdrant integration

This commit is contained in:
2026-06-06 13:31:31 +05:30
parent a162fa89e5
commit 59fc91f034
45 changed files with 2052 additions and 4430 deletions

26
backedn.txt Normal file
View File

@@ -0,0 +1,26 @@
Here is the expanded architectural design base, updated specifically to handle a rapidly growing database with many collections beyond just Auth and Clients.
1. The UX Flow (Built for Scale)
When you have dozens of collections, hardcoding every single view isn't scalable. The dashboard needs to act like a true "Command Center" with dynamic discovery:
Zone A: "Dedicated Modules" (The Custom Experiences)
Purpose: Highly tailored, customized views for your most important business logic.
Flow: The sidebar permanently pins Team Access (doormile_auth) and Field Operations (doormile_clients). These open the specialized Tables, Kanban boards, and GeoMaps we discussed earlier.
Zone B: "The Data Explorer" (For the remaining dozens of collections)
Purpose: A dynamic, auto-generating view to inspect any collection that exists on the server, even ones created yesterday.
Flow: Admin scrolls down the sidebar to a "Data Lake" section -> Uses a mini search bar to find a specific collection by name -> Clicks it -> The app dynamically generates a generic table based on whatever payload data is inside it.
2. The Design Components (The Base for Scale)
To build this massive multi-collection system, you will add these components to the base:
🗂️ For the Sidebar / Navigation
CollectionDiscovery Component: A scrollable list on the left menu that automatically asks Qdrant for a list of all collections and renders them.
SidebarFilter Component: A tiny search box sitting right above the collection list so you can instantly filter out collection names (e.g., typing "survey" shows survey_2024, survey_2025, survey_archived).
🧬 For the Generic / Unknown Collections (The Explorer)
Since you won't build a custom UI for every single minor collection, you need components that adapt to any data:
AdaptiveDataGrid Component: A smart table that looks at the first vector it fetches from Qdrant, reads the JSON payload keys, and automatically creates the table columns on the fly. (If a collection has item_name and price, the table creates those two columns automatically).
VectorInspector Modal: When you click a row in an unknown collection, this opens a sleek, dark-themed JSON code viewer. It shows the raw payload and the raw floating-point vector arrays perfectly formatted, similar to how a developer console looks.
ClusterHealthWidget: A component at the very top of the dashboard that shows the sum of all vectors across all collections, total RAM usage, and server uptime.
⚙️ The Updated Shared Components
ViewToggle Component: A simple switch at the top right of the screen that lets an admin toggle any collection between "Card View" (visual) and "Table View" (dense data).
GlobalSearch (Upgraded): The search bar now includes a dropdown filter to let you select which of the 50+ collections you actually want to search inside.

View File

@@ -12,7 +12,7 @@
href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800&family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<title>Doormile Console</title>
<title>Doormile CRM</title>
</head>
<body>
<div id="root"></div>

View File

@@ -5,6 +5,8 @@ import { Box, CircularProgress } from '@mui/material';
import MainLayout from '@/layout/MainLayout';
import MinimalLayout from '@/layout/MinimalLayout';
import AuthGuard from '@/components/AuthGuard';
const load = (factory) => {
const C = lazy(factory);
return (
@@ -24,39 +26,13 @@ export default function App() {
return (
<Routes>
{/* Shell pages */}
<Route element={<MainLayout />}>
<Route element={<AuthGuard><MainLayout /></AuthGuard>}>
<Route path="/dashboard" element={load(() => import('@/pages/Dashboard'))} />
<Route path="/orders" element={load(() => import('@/pages/orders/OrdersList'))} />
<Route path="/orders/create" element={load(() => import('@/pages/orders/CreateOrder'))} />
<Route path="/orders/create-multiple" element={load(() => import('@/pages/orders/CreateMultipleOrders'))} />
<Route path="/orders/assign" element={load(() => import('@/pages/orders/AssignOrders'))} />
<Route path="/orders/:id" element={load(() => import('@/pages/orders/OrderDetails'))} />
<Route path="/deliveries" element={load(() => import('@/pages/Deliveries'))} />
<Route path="/tenants" element={load(() => import('@/pages/tenants/Tenants'))} />
<Route path="/tenants/create" element={load(() => import('@/pages/tenants/CreateClient'))} />
<Route path="/customers" element={load(() => import('@/pages/customers/Customers'))} />
<Route path="/customers/create" element={load(() => import('@/pages/customers/CreateCustomer'))} />
<Route path="/team-users" element={load(() => import('@/pages/team/TeamUsers'))} />
<Route path="/pricing" element={load(() => import('@/pages/Pricing'))} />
<Route path="/riders" element={load(() => import('@/pages/riders/Riders'))} />
<Route path="/riders/create" element={load(() => import('@/pages/riders/CreateRider'))} />
<Route path="/riders/:id/edit" element={load(() => import('@/pages/riders/EditRider'))} />
<Route path="/reports/orders-summary" element={load(() => import('@/pages/reports/OrdersSummary'))} />
<Route path="/reports/orders-details" element={load(() => import('@/pages/reports/OrdersDetails'))} />
<Route path="/reports/riders-summary" element={load(() => import('@/pages/reports/RidersSummary'))} />
<Route path="/reports/riders-logs" element={load(() => import('@/pages/reports/RidersLogs'))} />
<Route path="/invoice" element={load(() => import('@/pages/invoice/Invoices'))} />
<Route path="/invoice/:id" element={load(() => import('@/pages/invoice/InvoicePreview'))} />
<Route path="/requests" element={load(() => import('@/pages/Requests'))} />
<Route path="/profile" element={load(() => import('@/pages/Profile'))} />
<Route path="/settings" element={load(() => import('@/pages/Settings'))} />
</Route>

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

View File

@@ -0,0 +1,11 @@
import { Navigate, Outlet } from 'react-router-dom';
export default function AuthGuard({ children }) {
const token = localStorage.getItem('auth_token');
if (!token) {
return <Navigate to="/login" replace />;
}
return children || <Outlet />;
}

View File

@@ -1,17 +0,0 @@
import { Card, CardHeader, CardContent, Divider, Box } from '@mui/material';
// ==============================|| MAIN CARD (titled surface) ||============================== //
export default function MainCard({ title, action, children, divider = true, contentSx, sx, noPadding = false }) {
return (
<Card sx={sx}>
{title && (
<>
<CardHeader title={title} action={action} />
{divider && <Divider />}
</>
)}
{noPadding ? <Box>{children}</Box> : <CardContent sx={contentSx}>{children}</CardContent>}
</Card>
);
}

View File

@@ -1,59 +0,0 @@
import { Box, Typography, Chip } from '@mui/material';
import RoomIcon from '@mui/icons-material/Room';
import TwoWheelerIcon from '@mui/icons-material/TwoWheeler';
// Stylised static "map" surface — pins + red route line. Swap for Leaflet/Google later.
export default function MapPlaceholder({ height = 360, pins = [], showRoute = true, label = 'Live Tracking', riders = [] }) {
return (
<Box
sx={{
position: 'relative',
height,
borderRadius: 2,
overflow: 'hidden',
bgcolor: '#EAEEF3',
backgroundImage:
'linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px)',
backgroundSize: '32px 32px',
border: '1px solid',
borderColor: 'grey.200'
}}
>
{/* faux roads */}
<Box sx={{ position: 'absolute', top: '30%', left: 0, right: 0, height: 8, bgcolor: '#fff', opacity: 0.85 }} />
<Box sx={{ position: 'absolute', top: 0, bottom: 0, left: '60%', width: 8, bgcolor: '#fff', opacity: 0.85 }} />
<Box sx={{ position: 'absolute', top: '65%', left: 0, right: 0, height: 6, bgcolor: '#fff', opacity: 0.7 }} />
{showRoute && (
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<path d="M 18% 78% Q 45% 50% 78% 22%" fill="none" stroke="#C01227" strokeWidth="3" strokeDasharray="2 8" strokeLinecap="round" />
</svg>
)}
{(pins.length ? pins : [
{ x: '18%', y: '78%', label: 'Pickup', color: '#00A854' },
{ x: '78%', y: '22%', label: 'Drop', color: '#C01227' }
]).map((p, i) => (
<Box key={i} sx={{ position: 'absolute', left: p.x, top: p.y, transform: 'translate(-50%, -100%)', textAlign: 'center' }}>
<RoomIcon sx={{ color: p.color, fontSize: 34, filter: 'drop-shadow(0 2px 3px rgba(0,0,0,0.3))' }} />
{p.label && (
<Chip size="small" label={p.label} sx={{ bgcolor: '#fff', fontWeight: 600, mt: -0.5 }} />
)}
</Box>
))}
{riders.map((r, i) => (
<Box key={i} sx={{ position: 'absolute', left: r.x, top: r.y, transform: 'translate(-50%, -50%)' }}>
<Box sx={{ width: 30, height: 30, borderRadius: '50%', bgcolor: r.active ? '#C01227' : '#8C8C8C', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 6px rgba(0,0,0,0.3)' }}>
<TwoWheelerIcon sx={{ color: '#fff', fontSize: 18 }} />
</Box>
</Box>
))}
<Chip label={label} size="small" sx={{ position: 'absolute', top: 12, left: 12, bgcolor: '#fff', fontWeight: 600 }} />
<Typography variant="caption" sx={{ position: 'absolute', bottom: 8, right: 12, color: 'grey.500' }}>
Map data © Doormile demo
</Typography>
</Box>
);
}

View File

@@ -4,46 +4,74 @@ import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded';
// ==============================|| STAT / KPI CARD ||============================== //
export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption }) {
export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption, accent = false }) {
const trendUp = typeof trend === 'number' ? trend >= 0 : null;
return (
<Card sx={{ height: '100%' }}>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Box>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
{title}
</Typography>
<Typography variant="h3" sx={{ mt: 0.75, fontWeight: 700, color: 'grey.800' }}>
{value}
</Typography>
</Box>
<Card
sx={{
height: '100%',
position: 'relative',
overflow: 'hidden',
borderRadius: 3,
boxShadow: '0 8px 24px rgba(0,0,0,0.03)',
border: '1px solid rgba(0,0,0,0.04)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': { transform: 'translateY(-4px)', boxShadow: '0 16px 32px rgba(0,0,0,0.06)' },
background: (theme) => `linear-gradient(145deg, ${theme.palette.background.paper} 0%, ${theme.palette[color].lighter}15 100%)`
}}
>
{Icon && (
<Box sx={{ position: 'absolute', right: -15, bottom: -15, opacity: 0.04, transform: 'rotate(-15deg)', pointerEvents: 'none' }}>
<Icon sx={{ fontSize: 110 }} />
</Box>
)}
<CardContent sx={{ p: 2.5, '&:last-child': { pb: 2.5 }, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 1.5, position: 'relative', zIndex: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
{title}
</Typography>
{Icon && (
<Avatar
variant="rounded"
sx={{ bgcolor: `${color}.lighter`, color: `${color}.main`, width: 44, height: 44 }}
<Box
sx={{
width: 38,
height: 38,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: `${color}.lighter`,
color: `${color}.main`,
boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.05)'
}}
>
<Icon fontSize="small" />
</Avatar>
</Box>
)}
</Stack>
<Box sx={{ flexGrow: 1, position: 'relative', zIndex: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 800, color: 'grey.900', letterSpacing: '-0.5px', lineHeight: 1 }}>
{value}
</Typography>
</Box>
{(trendUp !== null || caption) && (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1.5 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 2, pt: 1.5, borderTop: '1px dashed rgba(0,0,0,0.06)', position: 'relative', zIndex: 2 }}>
{trendUp !== null && (
<>
<Stack direction="row" spacing={0.25} alignItems="center" sx={{ bgcolor: trendUp ? 'success.lighter' : 'error.lighter', px: 0.75, py: 0.25, borderRadius: 1 }}>
{trendUp ? (
<ArrowUpwardRoundedIcon sx={{ fontSize: 16, color: 'success.main' }} />
<ArrowUpwardRoundedIcon sx={{ fontSize: 14, color: 'success.dark' }} />
) : (
<ArrowDownwardRoundedIcon sx={{ fontSize: 16, color: 'error.main' }} />
<ArrowDownwardRoundedIcon sx={{ fontSize: 14, color: 'error.dark' }} />
)}
<Typography variant="caption" sx={{ fontWeight: 600, color: trendUp ? 'success.main' : 'error.main' }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: trendUp ? 'success.dark' : 'error.dark' }}>
{Math.abs(trend)}%
</Typography>
</>
</Stack>
)}
{caption && (
<Typography variant="caption" color="text.secondary">
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
{caption}
</Typography>
)}

View File

@@ -18,6 +18,16 @@ const MAP = {
skipped: { color: 'warning', label: 'Skipped' },
failed: { color: 'error', label: 'Failed' },
cancelled: { color: 'error', label: 'Cancelled' },
// clients (doormile_clients)
newclient: { color: 'info', label: 'New Client' },
contacted: { color: 'warning', label: 'Contacted' },
onboarded: { color: 'success', label: 'Onboarded' },
lost: { color: 'error', label: 'Lost' },
// team users (doormile_auth)
admin: { color: 'primary', label: 'Admin' },
rep: { color: 'info', label: 'Rep' },
manager: { color: 'success', label: 'Manager' },
support: { color: 'warning', label: 'Support' },
// riders / tenants
online: { color: 'success', label: 'Online' },
offline: { color: 'default', label: 'Offline' },
@@ -42,7 +52,8 @@ const TONE = {
export default function StatusChip({ status, size = 'small', label, sx }) {
const key = String(status || '').toLowerCase().replace(/\s+/g, '-');
const cfg = MAP[key] || { color: 'default', label: status };
const humanized = String(status || '').replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
const cfg = MAP[key] || { color: 'default', label: humanized || status };
const tone = TONE[cfg.color] || TONE.default;
return (
<Chip

View File

@@ -1,66 +0,0 @@
import { Box, useTheme } from '@mui/material';
// Lightweight dependency-free area/line chart.
// series: [{ name, color, data: number[] }], labels: string[]
export default function AreaChart({ series = [], labels = [], height = 260 }) {
const theme = useTheme();
const W = 720;
const H = height;
const pad = { l: 40, r: 16, t: 16, b: 28 };
const innerW = W - pad.l - pad.r;
const innerH = H - pad.t - pad.b;
const all = series.flatMap((s) => s.data);
const max = Math.max(...all, 1);
const min = 0;
const n = labels.length;
const x = (i) => pad.l + (n <= 1 ? 0 : (i / (n - 1)) * innerW);
const y = (v) => pad.t + innerH - ((v - min) / (max - min)) * innerH;
const ticks = 4;
return (
<Box sx={{ width: '100%', overflow: 'hidden' }}>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} preserveAspectRatio="xMidYMid meet">
{Array.from({ length: ticks + 1 }).map((_, i) => {
const gy = pad.t + (i / ticks) * innerH;
const val = Math.round(max - (i / ticks) * (max - min));
return (
<g key={i}>
<line x1={pad.l} y1={gy} x2={W - pad.r} y2={gy} stroke={theme.palette.grey[200]} strokeWidth="1" />
<text x={pad.l - 8} y={gy + 4} textAnchor="end" fontSize="11" fill={theme.palette.grey[500]}>
{val}
</text>
</g>
);
})}
{labels.map((lb, i) => (
<text key={lb} x={x(i)} y={H - 8} textAnchor="middle" fontSize="11" fill={theme.palette.grey[500]}>
{lb}
</text>
))}
{series.map((s) => {
const line = s.data.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)}`).join(' ');
const area = `${line} L ${x(s.data.length - 1)} ${pad.t + innerH} L ${x(0)} ${pad.t + innerH} Z`;
const gid = `grad-${s.name.replace(/\s/g, '')}`;
return (
<g key={s.name}>
<defs>
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={s.color} stopOpacity="0.28" />
<stop offset="100%" stopColor={s.color} stopOpacity="0" />
</linearGradient>
</defs>
{s.fill !== false && <path d={area} fill={`url(#${gid})`} />}
<path d={line} fill="none" stroke={s.color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
{s.data.map((v, i) => (
<circle key={i} cx={x(i)} cy={y(v)} r="3" fill="#fff" stroke={s.color} strokeWidth="2" />
))}
</g>
);
})}
</svg>
</Box>
);
}

View File

@@ -1,181 +0,0 @@
// ==============================|| DOORMILE - MOCK DATA ||============================== //
// Static demo data powering every screen.
export const locations = ['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune'];
export const tenantsList = [
'Freshly Foods',
'UrbanCart',
'MediQuick Pharma',
'BloomBox Florists',
'TechNova Retail',
'GreenLeaf Organics'
];
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-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' }
];
export const orderTimeline = [
{ label: 'Order Placed', time: '09:12 AM', done: true },
{ label: 'Picked Up', time: '09:48 AM', done: true },
{ label: 'In Transit', time: '10:05 AM', done: true },
{ label: 'Delivered', time: '—', done: false }
];
export const deliveries = orders.map((o, i) => ({
...o,
rider: ['Mohan Das', 'Imran Sheikh', 'Ravi Teja', 'Sandeep Roy', 'Faisal Khan'][i % 5],
amount: o.charges,
products: [
{ name: 'Item A', description: 'Standard pack', qty: o.qty, cost: 40, price: 60, tax: 5, amount: 60 * o.qty }
]
}));
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-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 }
];
export const riderLogs = [
{ location: '12.9352, 77.6245', battery: '82%', charging: 'No', speed: '24 km/h', accuracy: '5 m', time: '10:42 AM', order: 'DM-10242', status: 'active' },
{ location: '12.9298, 77.6848', battery: '80%', charging: 'No', speed: '0 km/h', accuracy: '8 m', time: '10:38 AM', order: 'DM-10242', status: 'arrived' },
{ location: '12.9101, 77.6446', battery: '84%', charging: 'No', speed: '31 km/h', accuracy: '4 m', time: '10:25 AM', order: 'DM-10242', status: 'picked' }
];
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' }
];
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: 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 }
];
export const tenantPricing = [
{ date: '2025-04-01', slab: '0-5 km', basePrice: 40, minKms: 2, pricePerKm: 9, otherCharges: 0 },
{ date: '2025-04-01', slab: '5-10 km', basePrice: 60, minKms: 5, pricePerKm: 8, otherCharges: 10 },
{ date: '2025-04-01', slab: '10+ km', basePrice: 90, minKms: 10, pricePerKm: 7, otherCharges: 15 }
];
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 }
];
export const invoices = [
{ id: 1, invoiceId: 'INV-2026-0041', client: 'Freshly Foods', invoiceDate: '2026-06-01', dueDate: '2026-06-15', period: 'May 2026', count: 1240, amount: 106800, status: 'paid' },
{ id: 2, invoiceId: 'INV-2026-0042', client: 'UrbanCart', invoiceDate: '2026-06-01', dueDate: '2026-06-15', period: 'May 2026', count: 980, amount: 84300, status: 'open' },
{ id: 3, invoiceId: 'INV-2026-0043', client: 'MediQuick Pharma', invoiceDate: '2026-05-01', dueDate: '2026-05-15', period: 'Apr 2026', count: 410, amount: 35200, status: 'overdue' },
{ id: 4, invoiceId: 'INV-2026-0044', client: 'TechNova Retail', invoiceDate: '2026-06-01', dueDate: '2026-06-20', period: 'May 2026', count: 760, amount: 65400, status: 'open' },
{ id: 5, invoiceId: 'INV-2026-0045', client: 'GreenLeaf Organics', invoiceDate: '2026-05-01', dueDate: '2026-05-15', period: 'Apr 2026', count: 540, amount: 46900, status: 'paid' }
];
export const invoiceLineItems = [
{ particulars: 'Standard delivery (0-5 km)', unit: 'order', qty: 720, rate: 74, other: 0, amount: 53280 },
{ particulars: 'Express delivery (5-10 km)', unit: 'order', qty: 380, rate: 108, other: 0, amount: 41040 },
{ particulars: 'Bulk delivery (10+ km)', unit: 'order', qty: 140, rate: 142, other: 8, amount: 21000 },
{ particulars: 'COD handling fee', unit: 'order', qty: 260, rate: 6, other: 0, amount: 1560 }
];
export const requests = [
{ id: 1, requestor: 'Freshly Foods', bank: 'HDFC Bank', ifsc: 'HDFC0001234', refNo: 'RQ-88121', amount: 24500, reason: 'Weekly settlement payout', contact: 'Anil Gupta', address: 'Koramangala 4th Block', city: 'Bengaluru', zip: '560034', accountNo: '5012 3344 7788', pricing: [{ category: 'Standard', skill: 'Bike', cost: 9 }, { category: 'Express', skill: 'Bike', cost: 12 }] },
{ id: 2, requestor: 'UrbanCart', bank: 'ICICI Bank', ifsc: 'ICIC0004567', refNo: 'RQ-88122', amount: 18900, reason: 'Fuel reimbursement', contact: 'Meera Nair', address: 'Indiranagar', city: 'Bengaluru', zip: '560038', accountNo: '6022 1199 4455', pricing: [{ category: 'Standard', skill: 'Bike', cost: 8 }] },
{ id: 3, requestor: 'MediQuick Pharma', bank: 'Axis Bank', ifsc: 'UTIB0007788', refNo: 'RQ-88123', amount: 9700, reason: 'Adjustment - April', contact: 'Rohit Sen', address: 'Andheri West', city: 'Mumbai', zip: '400058', accountNo: '7033 5566 1122', pricing: [{ category: 'Standard', skill: 'Bike', cost: 10 }] }
];
// reports
export const ordersSummary = tenants.map((t, i) => ({
id: t.id,
tenant: t.name,
location: t.city,
orders: { pending: [2, 1, 0, 3, 1][i], cancelled: [1, 0, 2, 0, 1][i], completed: [38, 27, 11, 4, 22][i] },
deliveries: { pending: [1, 1, 0, 2, 0][i], cancelled: [0, 0, 1, 0, 1][i], completed: [38, 26, 10, 4, 22][i] },
collection: [12400, 8800, 3200, 900, 6700][i],
kms: [184, 142, 66, 28, 121][i],
actualKms: [190, 148, 70, 30, 124][i],
amount: [4280, 3120, 1420, 520, 2410][i],
riders: [
{ rider: 'Mohan Das', orders: 14, deliveries: 14, pending: 1, cancelled: 0, completed: 13, collection: 5200, kms: 64, actualKms: 66, charges: 1480 },
{ rider: 'Imran Sheikh', orders: 11, deliveries: 10, pending: 0, cancelled: 1, completed: 9, collection: 4100, kms: 58, actualKms: 60, charges: 1280 }
]
}));
export const ordersDetailReport = orders.map((o, i) => ({
id: o.id,
client: o.tenant,
pickup: o.pickup,
drop: o.drop,
status: o.status,
assigned: '09:12',
accepted: i % 5 === 3 ? '—' : '09:14',
arrived: ['09:40', '—', '09:35', '—', '—', '—', '09:50', '—'][i],
picked: ['09:48', '—', '09:42', '—', '—', '—', '09:58', '—'][i],
active: ['10:05', '—', '10:00', '—', '—', '—', '10:12', '—'][i],
delivered: o.status === 'delivered' ? '10:30' : '—',
cancelled: o.status === 'cancelled' ? '09:20' : '—',
notes: o.notes,
kms: o.kms,
charges: o.charges
}));
export const ridersSummary = riders.map((r, i) => ({
id: r.id,
rider: r.name,
orders: r.deliveries + [2, 1, 1, 0, 1][i],
pending: [1, 0, 1, 0, 0][i],
cancelled: [0, 1, 0, 0, 1][i],
delivered: r.deliveries,
kms: [64, 58, 47, 0, 39][i],
actualKms: [66, 60, 49, 0, 41][i],
amount: [1480, 1280, 1120, 0, 980][i],
clients: [
{ client: 'Freshly Foods', all: 8, pending: 1, completed: 7, cancelled: 0, kms: 32, actualKms: 33, amount: 740 },
{ client: 'UrbanCart', all: 6, pending: 0, completed: 6, cancelled: 0, kms: 32, actualKms: 33, amount: 740 }
]
}));
export const ridersLive = riders.map((r, i) => ({
...r,
userid: r.userId,
lastLog: ['10:42 AM', '10:39 AM', '10:40 AM', 'Yesterday', '10:35 AM'][i],
active: r.status !== 'offline',
lat: [12.9352, 19.1351, 17.4435, 28.6315, 13.0067][i],
lng: [77.6245, 72.829, 78.3772, 77.2167, 80.2206][i]
}));
// dashboard chart series (monthly orders)
export const ordersTrend = [
{ m: 'Jan', orders: 820, delivered: 760 },
{ m: 'Feb', orders: 932, delivered: 880 },
{ m: 'Mar', orders: 1010, delivered: 965 },
{ m: 'Apr', orders: 1180, delivered: 1120 },
{ m: 'May', orders: 1290, delivered: 1240 },
{ m: 'Jun', orders: 1402, delivered: 1330 }
];
export const statusBreakdown = [
{ label: 'Delivered', value: 1240, color: '#00A854' },
{ label: 'In Transit', value: 96, color: '#00A2AE' },
{ label: 'Pending', value: 48, color: '#FFBF00' },
{ label: 'Cancelled', value: 18, color: '#F04134' }
];

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
@@ -6,72 +6,78 @@ import {
IconButton,
Box,
InputBase,
Badge,
Avatar,
Typography,
Stack,
Menu,
MenuItem,
Divider,
ListItemIcon,
ListItemText,
Tooltip,
Button,
Stack,
Popper,
Paper,
ClickAwayListener,
CircularProgress,
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 Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import Logo from '@/components/Logo';
import UserAvatar from '@/components/UserAvatar';
import { fetchPoints, COLLECTIONS } from '@/utils/qdrant';
const RED = '#C01227';
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: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false },
{ id: 4, icon: AssignmentOutlinedIcon, title: '3 new onboarding requests', time: '3 hrs ago', to: '/requests', 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 unread = notifications.filter((n) => !n.read).length;
// Live client search for the top bar.
const searchRef = useRef(null);
const [clients, setClients] = useState([]);
const [loadedClients, setLoadedClients] = useState(false);
const [loadingClients, setLoadingClients] = useState(false);
const [openResults, setOpenResults] = useState(false);
const openNotif = (e) => setNotifAnchor(e.currentTarget);
const closeNotif = () => setNotifAnchor(null);
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
const onNotifClick = (n) => {
setNotifications((prev) => prev.map((x) => (x.id === n.id ? { ...x, read: true } : x)));
closeNotif();
navigate(n.to);
const ensureClients = () => {
if (loadedClients || loadingClients) return;
setLoadingClients(true);
fetchPoints(COLLECTIONS.clients)
.then((points) => setClients(points.map((p) => ({
id: p.id,
name: p.payload?.name || '—',
city: p.payload?.city || '',
businessType: p.payload?.businessType || '',
phone: p.payload?.phone || ''
}))))
.catch(() => {})
.finally(() => { setLoadedClients(true); setLoadingClients(false); });
};
const q = search.trim().toLowerCase();
const results = q
? clients.filter((c) => [c.name, c.city, c.businessType, c.phone].join(' ').toLowerCase().includes(q)).slice(0, 6)
: [];
const onSearchChange = (e) => {
setSearch(e.target.value);
ensureClients();
setOpenResults(true);
};
const goToClients = (term) => {
navigate(`/tenants?q=${encodeURIComponent(term)}`);
setSearch('');
setOpenResults(false);
};
const submitSearch = (e) => {
e.preventDefault();
const q = search.trim();
if (q) navigate(`/orders?q=${encodeURIComponent(q)}`);
const term = search.trim();
if (term) goToClients(term);
};
return (
@@ -95,55 +101,82 @@ export default function Header({ onToggle }) {
<Box sx={{ flexGrow: 1 }} />
{/* Search — moved to the right */}
<Box
component="form"
onSubmit={submitSearch}
sx={{
display: { xs: 'none', sm: 'flex' },
alignItems: 'center',
bgcolor: alpha('#fff', 0.16),
borderRadius: 2,
px: 1.5,
py: 0.5,
width: { sm: 240, md: 320 },
'&:hover': { bgcolor: alpha('#fff', 0.22) },
'&:focus-within': { bgcolor: alpha('#fff', 0.26) }
}}
>
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} />
<InputBase
value={search}
onChange={(e) => 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' }}
/>
</Box>
{/* Search — live client lookup */}
<ClickAwayListener onClickAway={() => setOpenResults(false)}>
<Box sx={{ display: { xs: 'none', sm: 'block' }, position: 'relative' }}>
<Box
ref={searchRef}
component="form"
onSubmit={submitSearch}
sx={{
display: 'flex',
alignItems: 'center',
bgcolor: alpha('#fff', 0.16),
borderRadius: 2,
px: 1.5,
py: 0.5,
width: { sm: 240, md: 320 },
'&:hover': { bgcolor: alpha('#fff', 0.22) },
'&:focus-within': { bgcolor: alpha('#fff', 0.26) }
}}
>
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} />
<InputBase
value={search}
onChange={onSearchChange}
onFocus={() => { ensureClients(); if (search.trim()) setOpenResults(true); }}
placeholder="Search clients…"
sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }}
inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }}
/>
</Box>
<Tooltip title="Messages">
<IconButton color="inherit" onClick={(e) => setMsgAnchor(e.currentTarget)}>
<Badge badgeContent={MESSAGES.length} color="warning">
<ChatBubbleOutlineIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Notifications">
<IconButton color="inherit" onClick={openNotif}>
<Badge badgeContent={unread} color="warning">
<NotificationsNoneIcon />
</Badge>
</IconButton>
</Tooltip>
<Popper
open={openResults && !!q}
anchorEl={searchRef.current}
placement="bottom-start"
style={{ zIndex: 1400, width: searchRef.current?.offsetWidth }}
>
<Paper sx={{ mt: 1, borderRadius: 2, overflow: 'hidden', boxShadow: '0 8px 24px rgba(0,0,0,0.18)' }}>
{loadingClients && results.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2.5 }}><CircularProgress size={20} /></Box>
) : results.length === 0 ? (
<Box sx={{ px: 2, py: 2 }}>
<Typography variant="body2" color="text.secondary">No clients match {search.trim()}.</Typography>
</Box>
) : (
<>
{results.map((c) => (
<MenuItem key={c.id} onClick={() => goToClients(c.name)} sx={{ py: 1, gap: 1.25 }}>
<UserAvatar name={c.name} size={30} />
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }} noWrap>{c.name}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{[c.businessType, c.city].filter(Boolean).join(' · ') || c.phone}
</Typography>
</Box>
</MenuItem>
))}
<Divider />
<MenuItem onClick={() => goToClients(search.trim())} sx={{ py: 1.25, color: 'primary.main', fontWeight: 600 }}>
<SearchIcon fontSize="small" sx={{ mr: 1 }} />
See all results for {search.trim()}
</MenuItem>
</>
)}
</Paper>
</Popper>
</Box>
</ClickAwayListener>
<Box
onClick={(e) => 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) } }}
>
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>AD</Avatar>
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>A</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}>
Aman Deshmukh
Admin
</Typography>
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}>
Operations Admin
@@ -151,87 +184,6 @@ export default function Header({ onToggle }) {
</Box>
</Box>
{/* Notifications dropdown */}
<Menu
anchorEl={notifAnchor}
open={Boolean(notifAnchor)}
onClose={closeNotif}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 360, maxWidth: '90vw' } }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ px: 2, py: 1.25 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
Notifications
</Typography>
<Button size="small" startIcon={<DoneAllIcon fontSize="small" />} onClick={markAllRead} disabled={unread === 0}>
Mark all read
</Button>
</Stack>
<Divider />
{notifications.length === 0 && (
<MenuItem disabled>
<ListItemText primary="No notifications" />
</MenuItem>
)}
{notifications.map((n) => {
const Icon = n.icon;
return (
<MenuItem key={n.id} onClick={() => onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.200' : alpha(RED, 0.12), color: RED }}>
<Icon fontSize="small" />
</Avatar>
</ListItemIcon>
<ListItemText
primary={n.title}
secondary={n.time}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: n.read ? 500 : 700 }}
secondaryTypographyProps={{ fontSize: '0.75rem' }}
/>
{!n.read && <Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: RED, mt: 1, ml: 0.5 }} />}
</MenuItem>
);
})}
<Divider />
<MenuItem onClick={() => { closeNotif(); navigate('/requests'); }} sx={{ justifyContent: 'center', color: 'primary.main', fontWeight: 600 }}>
View all activity
</MenuItem>
</Menu>
{/* Messages dropdown */}
<Menu
anchorEl={msgAnchor}
open={Boolean(msgAnchor)}
onClose={() => setMsgAnchor(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }}
>
<Typography variant="subtitle1" sx={{ fontWeight: 700, px: 2, py: 1.25 }}>
Messages
</Typography>
<Divider />
{MESSAGES.map((m) => (
<MenuItem key={m.id} onClick={() => setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, fontSize: '0.8rem' }}>
{m.initials}
</Avatar>
</ListItemIcon>
<ListItemText
primary={m.name}
secondary={m.text}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 700 }}
secondaryTypographyProps={{ fontSize: '0.8rem' }}
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, mt: 0.5, flexShrink: 0 }}>
{m.time}
</Typography>
</MenuItem>
))}
</Menu>
{/* Account dropdown */}
<Menu
anchorEl={account}
@@ -239,12 +191,18 @@ export default function Header({ onToggle }) {
onClose={() => setAccount(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, minWidth: 200 } }}
PaperProps={{ sx: { mt: 1, minWidth: 220 } }}
>
<MenuItem onClick={() => { setAccount(null); navigate('/profile'); }}>
<ListItemIcon><PersonOutlineIcon fontSize="small" /></ListItemIcon>
View Profile
</MenuItem>
<Box sx={{ px: 2, py: 1.5 }}>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 38, height: 38, bgcolor: RED, color: '#fff', fontWeight: 700 }}>A</Avatar>
<Box sx={{ lineHeight: 1.2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Admin</Typography>
<Typography variant="caption" color="text.secondary">Operations Admin</Typography>
</Box>
</Stack>
</Box>
<Divider />
<MenuItem onClick={() => { setAccount(null); navigate('/settings'); }}>
<ListItemIcon><SettingsOutlinedIcon fontSize="small" /></ListItemIcon>
Settings

View File

@@ -157,7 +157,7 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
{expanded && (
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.12)' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.55)' }}>
Doormile Console v1.0
Doormile CRM v1.0
</Typography>
</Box>
)}

View File

@@ -1,53 +1,23 @@
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 AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined';
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
// ==============================|| DOORMILE - SIDEBAR NAV CONFIG ||============================== //
const navItems = [
{
group: 'Operations',
group: 'CRM',
items: [
{ id: 'dashboard', title: 'Dashboard', url: '/dashboard', icon: DashboardOutlinedIcon },
{ id: 'orders', title: 'Orders', url: '/orders', icon: Inventory2OutlinedIcon },
{ id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon }
{ id: 'tenants', title: 'Clients', url: '/tenants', icon: ApartmentOutlinedIcon },
{ id: 'team-users', title: 'App Users', url: '/team-users', icon: GroupsOutlinedIcon }
]
},
{
group: 'Network',
group: 'System',
items: [
{ id: 'tenants', title: 'Tenants', url: '/tenants', icon: ApartmentOutlinedIcon },
{ id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon },
{ id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon },
{ id: 'riders', title: 'Riders', url: '/riders', icon: TwoWheelerOutlinedIcon }
]
},
{
group: 'Finance & Insights',
items: [
{
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 }
]
},
{ id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon },
{ id: 'requests', title: 'Requests', url: '/requests', icon: AssignmentOutlinedIcon }
{ id: 'settings', title: 'Settings', url: '/settings', icon: SettingsOutlinedIcon }
]
}
];

View File

@@ -1,124 +1,260 @@
import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material';
import { useState, useEffect, useMemo } from 'react';
import {
Grid, Card, Box, Stack, Typography, Button, Divider, LinearProgress, CircularProgress, Alert,
Table, TableBody, TableCell, TableHead, TableRow, TableContainer
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import HandshakeOutlinedIcon from '@mui/icons-material/HandshakeOutlined';
import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined';
import DonutLargeOutlinedIcon from '@mui/icons-material/DonutLargeOutlined';
import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
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 { ordersTrend, statusBreakdown, orders, riders } from '@/data/mock';
import { inr } from '@/utils/format';
import EmptyState from '@/components/EmptyState';
import { fetchPoints, COLLECTIONS } from '@/utils/qdrant';
import bgImage from '@/assets/premium_logistics_bg.png';
const titleCase = (s) =>
String(s || '').replace(/[_-]+/g, ' ').replace(/([a-z\d])([A-Z])/g, '$1 $2').replace(/\b\w/g, (c) => c.toUpperCase()).trim();
const STATUS_COLOR = { newclient: '#00A2AE', contacted: '#FFBF00', onboarded: '#00A854', lost: '#F04134' };
const statusColor = (s) => STATUS_COLOR[String(s || '').toLowerCase()] || '#8C8C8C';
const BAR_COLORS = ['#C01227', '#00A2AE', '#00A854', '#FFBF00', '#9E0E20', '#8C8C8C', '#D6515C'];
const generateLogicalId = (id) => {
const str = String(id).replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
return 'CLI-' + str.substring(0, 6).padStart(6, '0');
};
function toClient(point) {
const p = point.payload || {};
return {
id: point.id,
logicalId: generateLogicalId(point.id),
name: p.name || '—',
businessType: p.businessType || '',
city: p.city || '',
businessState: p.businessState || '',
status: p.status || 'unknown',
parcelVolume: Number(p.parcelVolume) || 0,
activeContracts: Number(p.activeContracts) || 0,
lastUpdated: p.lastUpdated || ''
};
}
function toUser(point) {
const p = point.payload || {};
return { id: point.id, name: p.name || '—', email: p.email || '', role: p.role || 'unknown' };
}
// Card with a tinted icon header band.
function Panel({ icon: Icon, title, action, color = 'primary', noPadding = false, children }) {
return (
<Card sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: 4,
boxShadow: '0 10px 30px rgba(0,0,0,0.03)',
border: '1px solid rgba(0,0,0,0.04)'
}}>
<Stack
direction="row" spacing={1.5} alignItems="center"
sx={{
px: 3, py: 2, borderBottom: 1, borderColor: 'divider',
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, transparent 100%)`
}}
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: `${color}.lighter`, color: `${color}.main`, boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.05)' }}>
<Icon fontSize="small" />
</Box>
<Typography variant="h6" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1, letterSpacing: '-0.3px' }}>{title}</Typography>
{action}
</Stack>
<Box sx={{ p: noPadding ? 0 : 3, flexGrow: 1 }}>{children}</Box>
</Card>
);
}
export default function Dashboard() {
const [clients, setClients] = useState([]);
const [team, setTeam] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const load = () => {
setLoading(true);
setError(null);
Promise.all([fetchPoints(COLLECTIONS.clients), fetchPoints(COLLECTIONS.teamUsers)])
.then(([cs, us]) => {
setClients(cs.map(toClient));
setTeam(us.map(toUser));
})
.catch((e) => setError(e.message || 'Failed to load dashboard data'))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
const stats = useMemo(() => ({
total: clients.length,
newCount: clients.filter((c) => c.status === 'newClient').length,
parcels: clients.reduce((s, c) => s + c.parcelVolume, 0),
contracts: clients.reduce((s, c) => s + c.activeContracts, 0)
}), [clients]);
const statusData = useMemo(() => {
const m = {};
clients.forEach((c) => { m[c.status] = (m[c.status] || 0) + 1; });
return Object.entries(m).sort((a, b) => b[1] - a[1]).map(([status, value]) => ({ label: titleCase(status), value, color: statusColor(status) }));
}, [clients]);
const byType = useMemo(() => {
const m = {};
clients.forEach((c) => { const t = c.businessType || 'other'; m[t] = (m[t] || 0) + 1; });
return Object.entries(m).sort((a, b) => b[1] - a[1]);
}, [clients]);
const maxType = byType[0]?.[1] || 1;
const recent = useMemo(
() => [...clients].sort((a, b) => String(b.lastUpdated).localeCompare(String(a.lastUpdated))).slice(0, 6),
[clients]
);
const today = new Date().toLocaleDateString('en-IN', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
if (loading) {
return (
<>
<PageHeader title="Dashboard" breadcrumbs={[{ label: 'Dashboard' }]} />
<Box sx={{ display: 'flex', justifyContent: 'center', py: 12 }}><CircularProgress /></Box>
</>
);
}
return (
<>
<PageHeader
title="Dashboard"
breadcrumbs={[{ label: 'Dashboard' }]}
action={
<Stack direction="row" spacing={1.5}>
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }}>
<MenuItem value="all">All Locations</MenuItem>
<MenuItem value="blr">Bengaluru</MenuItem>
<MenuItem value="mum">Mumbai</MenuItem>
</TextField>
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />}>Export</Button>
</Stack>
}
action={<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load}>Refresh</Button>}
/>
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Orders" value="1,402" icon={Inventory2OutlinedIcon} trend={8.4} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered" value="1,330" icon={LocalShippingOutlinedIcon} color="success" trend={6.1} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Riders" value="48" icon={TwoWheelerOutlinedIcon} color="info" trend={-2.3} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Revenue" value={inr(384200)} icon={CurrencyRupeeIcon} color="warning" trend={11.7} caption="vs last month" /></Grid>
{error && <Alert severity="error" sx={{ mb: 2.5 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
<Grid container spacing={3}>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Clients" value={stats.total} icon={ApartmentOutlinedIcon} color="primary" caption="All registered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="New Clients" value={stats.newCount} icon={FiberNewOutlinedIcon} color="primary" caption="Awaiting onboarding" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Parcel Volume" value={stats.parcels.toLocaleString('en-IN')} icon={Inventory2OutlinedIcon} color="primary" caption="Across all clients" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Active Contracts" value={stats.contracts} icon={HandshakeOutlinedIcon} color="primary" caption="Currently running" /></Grid>
<Grid item xs={12} lg={8}>
<MainCard
title="Orders Overview"
action={<Stack direction="row" spacing={2}><Legend color="#C01227" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
>
<AreaChart
labels={ordersTrend.map((d) => d.m)}
series={[
{ name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) },
{ name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) }
]}
/>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="Order Status">
<Box sx={{ py: 2 }}>
<DonutChart data={statusBreakdown} centerValue="1,402" centerLabel="Orders" />
</Box>
</MainCard>
<Panel icon={HistoryOutlinedIcon} title="Recent Clients" color="primary" noPadding>
{recent.length === 0 ? (
<EmptyState title="No clients yet" caption="Add a client to see it here." />
) : (
<TableContainer>
<Table sx={{ minWidth: 600 }}>
<TableHead>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell>Client</TableCell>
<TableCell>Type</TableCell>
<TableCell>Location</TableCell>
<TableCell align="right">Parcels</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{recent.map((c) => (
<TableRow key={c.id} hover>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={c.name} size={32} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{c.name}</Typography>
</Stack>
</TableCell>
<TableCell><Typography variant="body2">{titleCase(c.businessType) || '—'}</Typography></TableCell>
<TableCell><Typography variant="body2">{c.city || '—'}{c.businessState ? `, ${c.businessState}` : ''}</Typography></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{c.parcelVolume.toLocaleString('en-IN')}</TableCell>
<TableCell><StatusChip status={c.status} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Panel>
</Grid>
<Grid item xs={12} lg={7}>
<MainCard title="Recent Orders" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Order ID</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Route</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.slice(0, 6).map((o) => (
<TableRow key={o.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell>
<TableCell>{o.customer}</TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary">{o.pickup} {o.drop}</Typography>
</TableCell>
<TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</MainCard>
<Grid item xs={12} lg={4}>
<Panel icon={DonutLargeOutlinedIcon} title="Clients by Status" color="primary">
<Box sx={{ py: 1.5 }}>
{statusData.length === 0
? <EmptyState title="No data" />
: <DonutChart data={statusData} centerValue={stats.total} centerLabel="Clients" />}
</Box>
</Panel>
</Grid>
<Grid item xs={12} lg={5}>
<MainCard title="Top Riders Today">
<Stack divider={<Divider />} spacing={0}>
{riders.slice(0, 5).map((r, i) => (
<Stack key={r.id} direction="row" spacing={2} alignItems="center" sx={{ py: 1.25 }}>
<Typography variant="subtitle2" color="text.secondary" sx={{ width: 18 }}>{i + 1}</Typography>
<UserAvatar name={r.name} size={36} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2">{r.name}</Typography>
<Typography variant="caption" color="text.secondary">{r.vehicle} · {r.rating}</Typography>
<Grid item xs={12} lg={8}>
<Panel icon={CategoryOutlinedIcon} title="Clients by Business Type" color="primary">
{byType.length === 0 ? (
<EmptyState title="No data" />
) : (
<Stack spacing={2.25}>
{byType.map(([type, count], i) => (
<Box key={type}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.75 }}>
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: BAR_COLORS[i % BAR_COLORS.length] }} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{titleCase(type)}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary">
{count} · {Math.round((count / clients.length) * 100)}%
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={(count / maxType) * 100}
sx={{ height: 8, borderRadius: 4, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { borderRadius: 4, backgroundColor: BAR_COLORS[i % BAR_COLORS.length] } }}
/>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="subtitle2">{r.deliveries}</Typography>
<Typography variant="caption" color="text.secondary">deliveries</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
))}
</Stack>
)}
</Panel>
</Grid>
<Grid item xs={12} lg={4}>
<Panel icon={GroupsOutlinedIcon} title={`App Users · ${team.length}`} color="primary">
{team.length === 0 ? (
<EmptyState title="No team users" />
) : (
<Stack divider={<Divider />} spacing={0}>
{team.slice(0, 6).map((u) => (
<Stack key={u.id} direction="row" spacing={1.5} alignItems="center" sx={{ py: 1.25 }}>
<UserAvatar name={u.name} size={36} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{u.name}</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{u.email}</Typography>
</Box>
<StatusChip status={u.role} />
</Stack>
))}
</Stack>
)}
</Panel>
</Grid>
</Grid>
</>
);
}
function Legend({ color, label }) {
return (
<Stack direction="row" spacing={0.75} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: color }} />
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Stack>
);
}

View File

@@ -1,279 +0,0 @@
import { useState, useMemo, Fragment } from 'react';
import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Typography, Collapse, Menu
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined';
import SwapHorizOutlinedIcon from '@mui/icons-material/SwapHorizOutlined';
import EditLocationAltOutlinedIcon from '@mui/icons-material/EditLocationAltOutlined';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import PageHeader from '@/components/PageHeader';
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 { inr } from '@/utils/format';
const TABS = [
{ key: 'assigned', label: 'Assigned' },
{ key: 'accepted', label: 'Accepted' },
{ key: 'arrived', label: 'Arrived' },
{ key: 'picked', label: 'Picked' },
{ key: 'active', label: 'Active' },
{ key: 'skipped', label: 'Skipped' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'cancelled', label: 'Cancelled' }
];
function ProductTable({ products = [] }) {
return (
<Box sx={{ m: 2, borderRadius: 1, border: 1, borderColor: 'divider', overflow: 'hidden' }}>
<Typography variant="subtitle2" sx={{ px: 2, py: 1.25, bgcolor: 'grey.50', color: 'grey.800' }}>
Products
</Typography>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Product Name</TableCell>
<TableCell>Description</TableCell>
<TableCell align="center">Quantity</TableCell>
<TableCell align="right">Cost</TableCell>
<TableCell align="right">Price</TableCell>
<TableCell align="right">Tax</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{products.map((p, i) => (
<TableRow key={i}>
<TableCell>{i + 1}</TableCell>
<TableCell>{p.name}</TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary">{p.description}</Typography>
</TableCell>
<TableCell align="center">{p.qty}</TableCell>
<TableCell align="right">{inr(p.cost)}</TableCell>
<TableCell align="right">{inr(p.price)}</TableCell>
<TableCell align="right">{p.tax}%</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(p.amount)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
);
}
function DeliveryRow({ row, index }) {
const [open, setOpen] = useState(false);
const [anchor, setAnchor] = useState(null);
return (
<Fragment>
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
<TableCell padding="checkbox">
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{index + 1}</TableCell>
<TableCell>{row.tenant}</TableCell>
<TableCell>{row.location}</TableCell>
<TableCell>{row.pickup}</TableCell>
<TableCell>{row.drop}</TableCell>
<TableCell>
<Stack direction="row" spacing={1} alignItems="center">
<UserAvatar name={row.rider} size={26} />
<Typography variant="body2">{row.rider}</Typography>
</Stack>
</TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary" noWrap>{row.notes || '—'}</Typography>
</TableCell>
<TableCell align="center">{row.qty}</TableCell>
<TableCell align="right">{row.cod ? inr(row.cod) : '—'}</TableCell>
<TableCell align="right">{row.kms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
<TableCell align="center">
<Tooltip title="Actions">
<IconButton size="small" onClick={(e) => setAnchor(e.currentTarget)}>
<MoreVertIcon fontSize="small" />
</IconButton>
</Tooltip>
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>
<MenuItem onClick={() => setAnchor(null)}>
<NotificationsActiveOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Notify Rider
</MenuItem>
<MenuItem onClick={() => setAnchor(null)}>
<SwapHorizOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Change Rider
</MenuItem>
<MenuItem onClick={() => setAnchor(null)}>
<EditLocationAltOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Update Delivery Status
</MenuItem>
<MenuItem onClick={() => setAnchor(null)} sx={{ color: 'error.main' }}>
<CancelOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Cancel Delivery
</MenuItem>
</Menu>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={13} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
<Collapse in={open} timeout="auto" unmountOnExit>
<ProductTable products={row.products} />
</Collapse>
</TableCell>
</TableRow>
</Fragment>
);
}
export default function Deliveries() {
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);
const tabKey = TABS[tab].key;
const counts = useMemo(() => {
const c = {};
TABS.forEach((t) => { c[t.key] = deliveries.filter((d) => d.status === t.key).length; });
return c;
}, []);
const stats = useMemo(() => ({
created: deliveries.length,
pending: deliveries.filter((d) => d.status === 'pending').length,
delivered: deliveries.filter((d) => d.status === 'delivered').length,
cancelled: deliveries.filter((d) => d.status === 'cancelled').length
}), []);
const filtered = useMemo(
() =>
deliveries.filter((d) => {
const matchTab = d.status === tabKey;
const matchTenant = tenant === 'all' || d.tenant === tenant;
const matchLocation = location === 'all' || d.location === location;
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;
}),
[tabKey, tenant, location, rider, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Deliveries"
breadcrumbs={[{ label: 'Deliveries' }]}
action={
<TextField
select size="small" value={headerLocation} onChange={(e) => setHeaderLocation(e.target.value)}
sx={{ minWidth: 180 }} label="Location"
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Created Orders" value={stats.created} icon={Inventory2OutlinedIcon} caption="100%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending Orders" value={stats.pending} icon={HourglassEmptyOutlinedIcon} color="warning" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered Orders" value={stats.delivered} icon={CheckCircleOutlineIcon} color="success" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cancelled Orders" value={stats.cancelled} icon={CancelOutlinedIcon} color="error" caption="12.5%" /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={tenant} onChange={(e) => { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
<TextField select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<TextField select size="small" value={rider} onChange={(e) => { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider">
<MenuItem value="all">All Riders</MenuItem>
{riders.map((r) => <MenuItem key={r.id} value={r.name}>{r.name}</MenuItem>)}
</TextField>
<Box sx={{ flexGrow: 1 }} />
<TextField
size="small" placeholder="Search deliveries…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 220 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox" />
<TableCell>S.No</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Order Location</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Notes</TableCell>
<TableCell align="center">Qty</TableCell>
<TableCell align="right">COD</TableCell>
<TableCell align="right">Kms</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.length === 0 ? (
<TableRow>
<TableCell colSpan={13} sx={{ border: 'none' }}>
<EmptyState title="No deliveries found" caption="Try adjusting filters or switching tabs." />
</TableCell>
</TableRow>
) : (
paged.map((row, i) => <DeliveryRow key={row.id} row={row} index={page * rpp + i} />)
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
</>
);
}

View File

@@ -1,93 +0,0 @@
import { useState, useMemo } from 'react';
import {
Autocomplete, TextField, Box,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, Typography
} from '@mui/material';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import EmptyState from '@/components/EmptyState';
import { pricing, locations } from '@/data/mock';
import { inr } from '@/utils/format';
const SLAB_COLORS = { '0-5 km': 'info', '5-10 km': 'warning', '10+ km': 'success' };
const NAME_COLORS = { Standard: 'primary', Express: 'warning', Bulk: 'success' };
export default function Pricing() {
const [location, setLocation] = useState(null);
const filtered = useMemo(
() => (location ? pricing.filter((p) => p.location === location) : pricing),
[location]
);
return (
<>
<PageHeader
title="Pricing"
breadcrumbs={[{ label: 'Pricing' }]}
action={
<Autocomplete
size="small"
options={locations}
value={location}
onChange={(_, v) => setLocation(v)}
sx={{ minWidth: 240 }}
renderInput={(params) => <TextField {...params} label="Filter by Location" />}
/>
}
/>
<MainCard noPadding>
{filtered.length === 0 ? (
<EmptyState title="No Pricing List" caption="No pricing configured for the selected location." />
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Location</TableCell>
<TableCell>Pricing Id</TableCell>
<TableCell>Name</TableCell>
<TableCell>Slab</TableCell>
<TableCell align="right">Base Price</TableCell>
<TableCell align="right">MinKm</TableCell>
<TableCell align="right">Price/Km</TableCell>
<TableCell align="right">MaxKm</TableCell>
<TableCell align="center">Min Orders</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filtered.map((p, i) => (
<TableRow key={p.id} hover>
<TableCell>{i + 1}</TableCell>
<TableCell>
<Chip size="small" label={p.location} color="info" variant="outlined" />
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'primary.main' }}>{p.pricingId}</Typography>
</TableCell>
<TableCell>
<Chip size="small" label={p.name} color={NAME_COLORS[p.name] || 'primary'} />
</TableCell>
<TableCell>
<Chip size="small" label={p.slab} color={SLAB_COLORS[p.slab] || 'default'} variant="outlined" />
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(p.basePrice)}</TableCell>
<TableCell align="right">{p.minKm}</TableCell>
<TableCell align="right">{inr(p.pricePerKm)}</TableCell>
<TableCell align="right">{p.maxKm}</TableCell>
<TableCell align="center">
<Chip size="small" label={p.minOrders} color="warning" variant="outlined" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
</>
);
}

View File

@@ -1,69 +0,0 @@
import { Grid, Card, CardContent, Stack, Typography, TextField } from '@mui/material';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import UserAvatar from '@/components/UserAvatar';
const PROFILE = {
userId: 'U1001',
userName: 'Aarav Menon',
appLocation: 'Bengaluru',
authName: 'admin@doormile.in',
contactNo: '+91 98450 11223',
email: 'aarav.menon@doormile.in',
address: 'No. 7, Brigade Road, MG Road Area',
location: 'Bengaluru',
city: 'Bengaluru',
state: 'Karnataka',
postcode: '560001',
role: 'Operations Administrator'
};
const ro = (value) => ({
fullWidth: true,
variant: 'standard',
value,
InputProps: { readOnly: true }
});
export default function Profile() {
return (
<>
<PageHeader title="User Profile" breadcrumbs={[{ label: 'User Profile' }]} />
<Grid container spacing={2.5}>
<Grid item xs={12}>
<Card>
<CardContent>
<Stack direction="row" spacing={2.5} alignItems="center">
<UserAvatar name={PROFILE.userName} size={72} />
<div>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.800' }}>{PROFILE.userName}</Typography>
<Typography variant="body2" color="text.secondary">{PROFILE.role}</Typography>
</div>
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<MainCard title="Account Information">
<Grid container spacing={3}>
<Grid item xs={12} sm={6} md={4}><TextField label="User ID" {...ro(PROFILE.userId)} /></Grid>
<Grid item xs={12} sm={6} md={4}><TextField label="User Name" {...ro(PROFILE.userName)} /></Grid>
<Grid item xs={12} sm={6} md={4}><TextField label="App Location" {...ro(PROFILE.appLocation)} /></Grid>
<Grid item xs={12} sm={6} md={4}><TextField label="Auth Name" {...ro(PROFILE.authName)} /></Grid>
<Grid item xs={12} sm={6} md={4}><TextField label="Contact No" {...ro(PROFILE.contactNo)} /></Grid>
<Grid item xs={12} sm={6} md={4}><TextField label="E-Mail" {...ro(PROFILE.email)} /></Grid>
<Grid item xs={12}><TextField label="Address" {...ro(PROFILE.address)} /></Grid>
<Grid item xs={12} sm={6} md={3}><TextField label="Location" {...ro(PROFILE.location)} /></Grid>
<Grid item xs={12} sm={6} md={3}><TextField label="City" {...ro(PROFILE.city)} /></Grid>
<Grid item xs={12} sm={6} md={3}><TextField label="State" {...ro(PROFILE.state)} /></Grid>
<Grid item xs={12} sm={6} md={3}><TextField label="Postcode" {...ro(PROFILE.postcode)} /></Grid>
</Grid>
</MainCard>
</Grid>
</Grid>
</>
);
}

View File

@@ -1,162 +0,0 @@
import { useState } from 'react';
import {
Card, Stack, Button, Box, Collapse, Tabs, Tab, Typography, Grid,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import PageHeader from '@/components/PageHeader';
import { requests } from '@/data/mock';
import { inr } from '@/utils/format';
function RequestRow({ row, index }) {
const [open, setOpen] = useState(false);
const [tab, setTab] = useState(0);
return (
<>
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
<TableCell>{index + 1}</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{row.requestor}</TableCell>
<TableCell>{row.bank}</TableCell>
<TableCell>{row.ifsc}</TableCell>
<TableCell>{row.refNo}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
<TableCell>{row.reason}</TableCell>
<TableCell align="center">
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ py: 0, borderBottom: open ? 1 : 0, borderColor: 'divider' }} colSpan={8}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ m: 2 }}>
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
<Tab label="Client Details" />
<Tab label="Client Pricing" />
</Tabs>
{tab === 0 && (
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={3}>
<Typography variant="caption" color="text.secondary">Contact Name</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.contact}</Typography>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Typography variant="caption" color="text.secondary">Address</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.address}</Typography>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Typography variant="caption" color="text.secondary">City</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.city}</Typography>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Typography variant="caption" color="text.secondary">Zip Code</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.zip}</Typography>
</Grid>
</Grid>
)}
{tab === 1 && (
<Table size="small">
<TableHead>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700 } }}>
<TableCell>#</TableCell>
<TableCell>Category</TableCell>
<TableCell>Skill</TableCell>
<TableCell align="right">Cost/Hr</TableCell>
</TableRow>
</TableHead>
<TableBody>
{row.pricing.map((p, i) => (
<TableRow key={i}>
<TableCell>{i + 1}</TableCell>
<TableCell>{p.category}</TableCell>
<TableCell>{p.skill}</TableCell>
<TableCell align="right">{inr(p.cost)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
</>
);
}
export default function Requests() {
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const [open, setOpen] = useState(false);
const paged = requests.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Requests"
breadcrumbs={[{ label: 'Requests' }]}
action={
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpen(true)}>
Create Request
</Button>
}
/>
<Card>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Requestor</TableCell>
<TableCell>Bank</TableCell>
<TableCell>IFSC</TableCell>
<TableCell>Ref No</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell>Reason</TableCell>
<TableCell align="center" />
</TableRow>
</TableHead>
<TableBody>
{paged.map((row, idx) => (
<RequestRow key={row.id} row={row} index={page * rpp + idx} />
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={requests.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]}
/>
</Card>
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Create Request</DialogTitle>
<DialogContent>
<Grid container spacing={2.5} sx={{ mt: 0 }}>
<Grid item xs={12} sm={6}><TextField label="Reference No" type="number" fullWidth /></Grid>
<Grid item xs={12} sm={6}><TextField label="Requestor" fullWidth /></Grid>
<Grid item xs={12} sm={6}><TextField label="Bank Name" fullWidth /></Grid>
<Grid item xs={12} sm={6}><TextField label="Amount" type="number" fullWidth /></Grid>
<Grid item xs={12} sm={6}><TextField label="Account No" type="number" fullWidth /></Grid>
<Grid item xs={12} sm={6}><TextField label="IFSC Code" fullWidth /></Grid>
<Grid item xs={12}><TextField label="Reason" fullWidth multiline minRows={3} /></Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setOpen(false)} color="inherit">Close</Button>
<Button variant="contained" onClick={() => setOpen(false)}>Update</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,72 +1,153 @@
import { useState } from 'react';
import {
Grid,
Tabs,
Tab,
Box,
Stack,
TextField,
MenuItem,
Switch,
FormControlLabel,
Button,
Typography,
Divider,
Snackbar,
Alert
Grid, Card, Box, Stack, TextField, MenuItem, Switch, Button, Typography, Divider,
Snackbar, Alert, Chip, IconButton, InputAdornment, LinearProgress, Avatar
} from '@mui/material';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined';
import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)'];
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
function TabPanel({ value, index, children }) {
if (value !== index) return null;
return <Box sx={{ pt: 1 }}>{children}</Box>;
const INITIAL_GENERAL = {
orgName: 'Doormile Logistics Pvt. Ltd.',
supportEmail: 'support@doormile.in',
contact: '+91 63749 46729',
timezone: TIMEZONES[0],
language: LANGUAGES[0]
};
const INITIAL_NOTIFY = {
newClient: true, contractStatus: true, teamAccess: false, databaseSync: true, emailAlerts: true, smsAlerts: false
};
const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false };
const NAV = [
{ icon: TuneOutlinedIcon, label: 'General', desc: 'Organisation profile' },
{ icon: NotificationsNoneIcon, label: 'Notifications', desc: 'Alerts & channels' },
{ icon: LockOutlinedIcon, label: 'Security', desc: 'Password & 2FA' }
];
const NOTIFY_ROWS = [
{ k: 'newClient', t: 'New client onboarded', d: 'Notify when a new client is added to the system' },
{ k: 'contractStatus', t: 'Contract status', d: 'When a client contract becomes active or expires' },
{ k: 'teamAccess', t: 'Team access', d: 'When a new team member is granted or revoked access' },
{ k: 'databaseSync', t: 'Database sync', d: 'Alerts for vector database synchronization events' }
];
const STRENGTH = [
{ label: 'Too weak', color: 'error' },
{ label: 'Weak', color: 'error' },
{ label: 'Fair', color: 'warning' },
{ label: 'Good', color: 'info' },
{ label: 'Strong', color: 'success' }
];
const scorePassword = (pw) => {
let s = 0;
if (pw.length >= 8) s++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
if (/\d/.test(pw)) s++;
if (/[^A-Za-z0-9]/.test(pw)) s++;
return s;
};
// Section surface with a tinted icon header band.
function Section({ icon: Icon, title, subtitle, color = 'primary', danger = false, children }) {
return (
<Card sx={danger ? { borderColor: 'error.light' } : undefined}>
<Stack
direction="row" spacing={1.75} alignItems="center"
sx={{
px: 3, py: 2.25, borderBottom: 1, borderColor: 'divider',
background: (theme) => `linear-gradient(90deg, ${theme.palette[color].lighter}66 0%, ${theme.palette.background.paper} 72%)`
}}
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: `${color}.lighter`, color: `${color}.main` }}>
<Icon fontSize="small" />
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.3 }}>{title}</Typography>
{subtitle && <Typography variant="caption" color="text.secondary">{subtitle}</Typography>}
</Box>
</Stack>
<Box sx={{ px: 3 }}>{children}</Box>
</Card>
);
}
// Two-column row: label + helper on the left, control on the right.
function Row({ label, description, children, align = 'center' }) {
return (
<Grid
container spacing={2} alignItems={align}
sx={{ py: 2.5, borderRadius: 2, transition: 'background-color .15s', '&:hover': { bgcolor: 'grey.50' } }}
>
<Grid item xs={12} sm={5}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'grey.800' }}>{label}</Typography>
{description && <Typography variant="caption" color="text.secondary">{description}</Typography>}
</Grid>
<Grid item xs={12} sm={7}>{children}</Grid>
</Grid>
);
}
const rightAlign = { display: 'flex', justifyContent: { sm: 'flex-end' } };
function PasswordField({ label, value, onChange, autoComplete }) {
const [show, setShow] = useState(false);
return (
<TextField
fullWidth size="small" type={show ? 'text' : 'password'} label={label} value={value} onChange={onChange} autoComplete={autoComplete}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
{show ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
</IconButton>
</InputAdornment>
)
}}
/>
);
}
export default function Settings() {
const [tab, setTab] = useState(0);
const [toast, setToast] = useState(false);
const [dirty, setDirty] = 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]
});
const [general, setGeneral] = useState(INITIAL_GENERAL);
const [notify, setNotify] = useState(INITIAL_NOTIFY);
const [security, setSecurity] = useState(INITIAL_SECURITY);
// Notifications
const [notify, setNotify] = useState({
newOrders: true,
riderStatus: true,
invoicePaid: true,
weeklyDigest: false,
emailAlerts: true,
smsAlerts: false
});
const setG = (k) => (e) => { setGeneral((p) => ({ ...p, [k]: e.target.value })); setDirty(true); };
const setN = (k) => (e) => { setNotify((p) => ({ ...p, [k]: e.target.checked })); setDirty(true); };
const setSText = (k) => (e) => { setSecurity((p) => ({ ...p, [k]: e.target.value })); setDirty(true); };
// Security
const [security, setSecurity] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactor: false
});
const save = () => { setToast(true); setDirty(false); };
const discard = () => {
setGeneral(INITIAL_GENERAL);
setNotify(INITIAL_NOTIFY);
setSecurity(INITIAL_SECURITY);
setDirty(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 pwScore = scorePassword(security.newPassword);
const pwMeta = STRENGTH[pwScore];
const mismatch = security.confirmPassword && security.newPassword !== security.confirmPassword;
return (
<>
@@ -74,120 +155,190 @@ export default function Settings() {
title="Settings"
breadcrumbs={[{ label: 'Settings' }]}
action={
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>
Save Changes
</Button>
<Stack direction="row" spacing={1.5} alignItems="center">
{dirty && <Chip size="small" label="Unsaved changes" sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />}
<Button variant="outlined" onClick={discard}>Discard</Button>
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>Save Changes</Button>
</Stack>
}
/>
<Grid container spacing={2.5}>
{/* Sidebar */}
<Grid item xs={12} md={3}>
<MainCard noPadding>
<Tabs
orientation="vertical"
value={tab}
onChange={(_, v) => setTab(v)}
sx={{
'& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 }
}}
>
<Tab icon={<TuneOutlinedIcon fontSize="small" />} iconPosition="start" label="General" />
<Tab icon={<NotificationsNoneIcon fontSize="small" />} iconPosition="start" label="Notifications" />
<Tab icon={<LockOutlinedIcon fontSize="small" />} iconPosition="start" label="Security" />
</Tabs>
</MainCard>
<Stack spacing={2.5} sx={{ position: { md: 'sticky' }, top: { md: 88 } }}>
<Card sx={{ p: 1.5 }}>
<Typography variant="overline" sx={{ px: 1, color: 'text.secondary', fontWeight: 700, letterSpacing: 0.6 }}>Preferences</Typography>
<Stack spacing={0.25} sx={{ mt: 0.5 }}>
{NAV.map((item, i) => {
const active = tab === i;
const Icon = item.icon;
return (
<Stack
key={item.label}
direction="row" spacing={1.5} alignItems="center"
onClick={() => setTab(i)}
sx={{
px: 1.5, py: 1.25, borderRadius: 2, cursor: 'pointer', position: 'relative',
bgcolor: active ? 'primary.lighter' : 'transparent',
transition: 'background-color .15s',
'&:hover': { bgcolor: active ? 'primary.lighter' : 'grey.50' },
'&::before': active ? { content: '""', position: 'absolute', left: 0, top: 9, bottom: 9, width: 3, borderRadius: 3, bgcolor: 'primary.main' } : {}
}}
>
<Box sx={{ width: 34, height: 34, borderRadius: 1.5, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: active ? 'primary.main' : 'grey.100', color: active ? '#fff' : 'grey.600' }}>
<Icon fontSize="small" />
</Box>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: active ? 'primary.main' : 'grey.800' }}>{item.label}</Typography>
<Typography variant="caption" color="text.secondary">{item.desc}</Typography>
</Box>
<ChevronRightIcon sx={{ fontSize: 18, color: active ? 'primary.main' : 'grey.300' }} />
</Stack>
);
})}
</Stack>
</Card>
<Card sx={{ p: 2.5, bgcolor: 'primary.lighter', borderColor: 'primary.100' }}>
<Stack spacing={1.25}>
<Box sx={{ width: 38, height: 38, borderRadius: 2, bgcolor: 'primary.main', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<HelpOutlineRoundedIcon fontSize="small" />
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.dark' }}>Need a hand?</Typography>
<Typography variant="caption" sx={{ color: 'primary.dark', opacity: 0.85 }}>Our team is available 24/7 for operational support.</Typography>
</Box>
<Button size="small" variant="contained" sx={{ alignSelf: 'flex-start' }}>Contact support</Button>
</Stack>
</Card>
</Stack>
</Grid>
{/* Content */}
<Grid item xs={12} md={9}>
{/* General */}
<TabPanel value={tab} index={0}>
<MainCard title="Organisation">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Organisation Name" value={general.orgName} onChange={setG('orgName')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Support Email" value={general.supportEmail} onChange={setG('supportEmail')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Contact Number" value={general.contact} onChange={setG('contact')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Timezone" value={general.timezone} onChange={setG('timezone')}>
{TIMEZONES.map((t) => (
<MenuItem key={t} value={t}>{t}</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Language" value={general.language} onChange={setG('language')}>
{LANGUAGES.map((l) => (
<MenuItem key={l} value={l}>{l}</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</MainCard>
</TabPanel>
{/* Notifications */}
<TabPanel value={tab} index={1}>
<MainCard title="Notification Preferences">
<Stack divider={<Divider flexItem />} 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) => (
<Stack key={row.k} direction="row" alignItems="center" justifyContent="space-between" sx={{ py: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{row.t}</Typography>
<Typography variant="caption" color="text.secondary">{row.d}</Typography>
</Box>
<Switch checked={notify[row.k]} onChange={setN(row.k)} />
</Stack>
))}
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Channels</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<FormControlLabel control={<Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} />} label="Email alerts" />
<FormControlLabel control={<Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} />} label="SMS alerts" />
</Stack>
</MainCard>
</TabPanel>
{/* Security */}
<TabPanel value={tab} index={2}>
{tab === 0 && (
<Stack spacing={2.5}>
<MainCard title="Change Password">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Current Password" value={security.currentPassword} onChange={setS('currentPassword')} />
</Grid>
<Grid item xs={12} />
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="New Password" value={security.newPassword} onChange={setS('newPassword')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Confirm New Password" value={security.confirmPassword} onChange={setS('confirmPassword')} />
</Grid>
</Grid>
</MainCard>
<MainCard title="Two-Factor Authentication">
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>Authenticator app</Typography>
<Typography variant="caption" color="text.secondary">
Require a one-time code at sign-in for extra security.
</Typography>
{/* Organisation identity banner */}
<Card sx={{ overflow: 'hidden' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }} spacing={2.5} alignItems={{ sm: 'center' }}
sx={{ p: 3, background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)` }}
>
<Avatar variant="rounded" sx={{ width: 64, height: 64, bgcolor: 'primary.main', color: '#fff' }}>
<BusinessOutlinedIcon />
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800' }}>{general.orgName}</Typography>
<Chip size="small" icon={<VerifiedOutlinedIcon sx={{ fontSize: 15, ml: 0.5 }} />} label="Verified" sx={{ fontWeight: 700, bgcolor: 'success.lighter', color: 'success.dark', '& .MuiChip-icon': { color: 'inherit' } }} />
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>{general.supportEmail} · {general.contact}</Typography>
</Box>
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} />
<Button variant="outlined" size="small">Change logo</Button>
</Stack>
</MainCard>
</Card>
<Section icon={TuneOutlinedIcon} title="Organisation" subtitle="Profile and regional preferences" color="primary">
<Stack divider={<Divider />}>
<Row label="Organisation name" description="Shown on invoices and exports">
<TextField fullWidth size="small" value={general.orgName} onChange={setG('orgName')} />
</Row>
<Row label="Support email" description="Where customer replies are routed">
<TextField fullWidth size="small" value={general.supportEmail} onChange={setG('supportEmail')} />
</Row>
<Row label="Contact number" description="Primary operations line">
<TextField fullWidth size="small" value={general.contact} onChange={setG('contact')} />
</Row>
<Row label="Timezone" description="Used for schedules and reports">
<TextField select fullWidth size="small" value={general.timezone} onChange={setG('timezone')}>
{TIMEZONES.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
</Row>
<Row label="Language" description="Console display language">
<TextField select fullWidth size="small" value={general.language} onChange={setG('language')}>
{LANGUAGES.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Row>
</Stack>
</Section>
</Stack>
</TabPanel>
)}
{tab === 1 && (
<Stack spacing={2.5}>
<Section icon={NotificationsNoneIcon} title="Notification Preferences" subtitle="Choose what you get alerted about" color="primary">
<Stack divider={<Divider />}>
{NOTIFY_ROWS.map((row) => (
<Row key={row.k} label={row.t} description={row.d}>
<Box sx={rightAlign}><Switch checked={notify[row.k]} onChange={setN(row.k)} /></Box>
</Row>
))}
</Stack>
</Section>
<Section icon={CampaignOutlinedIcon} title="Delivery Channels" subtitle="How alerts reach your team" color="primary">
<Stack divider={<Divider />}>
<Row label="Email alerts" description="Send notifications to the support inbox">
<Box sx={rightAlign}><Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} /></Box>
</Row>
<Row label="SMS alerts" description="Send notifications to the registered mobile">
<Box sx={rightAlign}><Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} /></Box>
</Row>
</Stack>
</Section>
</Stack>
)}
{tab === 2 && (
<Stack spacing={2.5}>
<Section icon={LockOutlinedIcon} title="Change Password" subtitle="Use 8+ characters with a mix of letters, numbers & symbols" color="primary">
<Stack divider={<Divider />}>
<Row label="Current password" description="Enter your existing password" align="flex-start">
<PasswordField label="Current password" value={security.currentPassword} onChange={setSText('currentPassword')} autoComplete="current-password" />
</Row>
<Row label="New password" description="Choose a strong, unique password" align="flex-start">
<Box>
<PasswordField label="New password" value={security.newPassword} onChange={setSText('newPassword')} autoComplete="new-password" />
{security.newPassword && (
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mt: 1 }}>
<LinearProgress variant="determinate" value={(pwScore / 4) * 100} color={pwMeta.color} sx={{ flexGrow: 1, height: 6, borderRadius: 3 }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: `${pwMeta.color}.main`, minWidth: 56 }}>{pwMeta.label}</Typography>
</Stack>
)}
</Box>
</Row>
<Row label="Confirm new password" description="Re-enter the new password" align="flex-start">
<Box>
<PasswordField label="Confirm new password" value={security.confirmPassword} onChange={setSText('confirmPassword')} autoComplete="new-password" />
{mismatch && <Typography variant="caption" color="error.main" sx={{ mt: 0.75, display: 'block' }}>Passwords do not match</Typography>}
</Box>
</Row>
</Stack>
</Section>
<Section icon={ShieldOutlinedIcon} title="Two-Factor Authentication" subtitle="Add an extra layer of security to your account" color="primary">
<Row label="Authenticator app" description="Require a one-time code at sign-in for extra security">
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ justifyContent: { sm: 'flex-end' } }}>
<Chip
size="small"
icon={<ShieldOutlinedIcon sx={{ fontSize: 15, ml: 0.5 }} />}
label={security.twoFactor ? 'Enabled' : 'Disabled'}
sx={{ fontWeight: 700, bgcolor: security.twoFactor ? 'success.lighter' : 'grey.100', color: security.twoFactor ? 'success.dark' : 'grey.600', '& .MuiChip-icon': { color: 'inherit' } }}
/>
<Switch checked={security.twoFactor} onChange={(e) => { setSecurity((p) => ({ ...p, twoFactor: e.target.checked })); setDirty(true); }} />
</Stack>
</Row>
</Section>
<Section icon={WarningAmberRoundedIcon} title="Danger Zone" subtitle="Irreversible and high-impact actions" color="error" danger>
<Row label="Sign out of all sessions" description="End every active session on all devices">
<Box sx={rightAlign}>
<Button variant="outlined" color="error" startIcon={<LogoutOutlinedIcon />} onClick={() => { localStorage.removeItem('auth_token'); window.location.href = '/login'; }}>Sign out everywhere</Button>
</Box>
</Row>
</Section>
</Stack>
)}
</Grid>
</Grid>

View File

@@ -16,109 +16,139 @@ import {
} from '@mui/material';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import BoltIcon from '@mui/icons-material/Bolt';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import Logo from '@/components/Logo';
import bgImage from '../../assets/mid-mile-approach.jpg';
export default function Login() {
const navigate = useNavigate();
const [show, setShow] = useState(false);
const [auth, setAuth] = useState('admin@doormile.in');
const [auth, setAuth] = useState('');
const [pwd, setPwd] = useState('');
return (
<Grid container sx={{ minHeight: '100vh' }}>
{/* Brand panel */}
<Grid
item
md={6}
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${bgImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
inset: 0,
background: 'linear-gradient(180deg, rgba(15,23,42,0.6) 0%, rgba(192, 18, 39, 0.8) 100%)',
zIndex: 1
}
}}
>
<Card
sx={{
display: { xs: 'none', md: 'flex' },
flexDirection: 'column',
justifyContent: 'space-between',
p: 6,
color: '#fff',
background: 'linear-gradient(150deg, #C01227 0%, #9E0E20 55%, #7E0B17 100%)',
position: 'relative',
overflow: 'hidden'
zIndex: 2,
width: '100%',
maxWidth: 440,
p: { xs: 4, sm: 5 },
m: 2,
background: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(24px)',
WebkitBackdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.5)',
boxShadow: '0 24px 48px rgba(0,0,0,0.2)',
borderRadius: 4,
}}
>
<Box sx={{ position: 'absolute', width: 420, height: 420, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', top: -120, right: -120 }} />
<Box sx={{ position: 'absolute', width: 280, height: 280, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', bottom: -80, left: -60 }} />
<Logo onDark />
<Box sx={{ position: 'relative' }}>
<Typography variant="h2" sx={{ color: '#fff', fontWeight: 800, lineHeight: 1.2 }}>
Move every parcel,
<br /> on time, every time.
</Typography>
<Typography sx={{ color: 'rgba(255,255,255,0.85)', mt: 2, maxWidth: 420 }}>
The command center for your last-mile operation orders, riders, pricing and settlements in one corporate console.
</Typography>
<Stack spacing={1.5} sx={{ mt: 4 }}>
{[
{ icon: BoltIcon, t: 'AI-assisted route optimisation' },
{ icon: LocalShippingOutlinedIcon, t: 'Real-time rider & delivery tracking' },
{ icon: VerifiedOutlinedIcon, t: 'Automated client invoicing & payouts' }
].map((f) => (
<Stack key={f.t} direction="row" spacing={1.5} alignItems="center">
<Box sx={{ width: 34, height: 34, borderRadius: 2, bgcolor: 'rgba(255,255,255,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<f.icon fontSize="small" />
</Box>
<Typography sx={{ color: 'rgba(255,255,255,0.92)' }}>{f.t}</Typography>
</Stack>
))}
</Stack>
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
© 2026 Doormile Logistics Pvt. Ltd.
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}><Logo /></Box>
<Typography variant="h3" sx={{ fontWeight: 800, textAlign: 'center', letterSpacing: '-0.5px', color: '#1e293b' }}>Welcome back</Typography>
<Typography variant="body1" sx={{ mt: 1, mb: 4, textAlign: 'center', color: '#475569' }}>
Sign in to your Doormile operations account.
</Typography>
</Grid>
{/* Form panel */}
<Grid item xs={12} md={6} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 3, sm: 6 } }}>
<Card sx={{ width: '100%', maxWidth: 420, p: { xs: 3, sm: 4.5 } }}>
<Box sx={{ display: { xs: 'flex', md: 'none' }, mb: 2 }}><Logo /></Box>
<Typography variant="h3" sx={{ fontWeight: 700 }}>Welcome back</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, mb: 3 }}>
Sign in to your Doormile operations account.
</Typography>
<Stack spacing={2.5}>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Auth Name</Typography>
<TextField fullWidth placeholder="Enter your auth name" value={auth} onChange={(e) => setAuth(e.target.value)} />
</Box>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Password</Typography>
<TextField
fullWidth
type={show ? 'text' : 'password'}
placeholder="Enter your password"
value={pwd}
onChange={(e) => setPwd(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
{show ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<FormControlLabel control={<Checkbox defaultChecked size="small" />} label={<Typography variant="body2">Remember me</Typography>} />
<Link href="#" underline="hover" variant="body2" color="primary">Forgot password?</Link>
</Stack>
<Button fullWidth size="large" variant="contained" onClick={() => navigate('/dashboard')}>
Sign In
</Button>
<Stack spacing={2.5}>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75, color: '#334155', fontWeight: 600 }}>Auth Name</Typography>
<TextField
fullWidth
placeholder="Enter your auth name"
value={auth}
onChange={(e) => setAuth(e.target.value)}
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: 'rgba(255,255,255,0.6)',
borderRadius: 2,
transition: 'all 0.2s ease',
'&:hover': { bgcolor: 'rgba(255,255,255,0.9)' },
'&.Mui-focused': { bgcolor: '#fff', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' }
}
}}
/>
</Box>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75, color: '#334155', fontWeight: 600 }}>Password</Typography>
<TextField
fullWidth
type={show ? 'text' : 'password'}
placeholder="Enter your password"
value={pwd}
onChange={(e) => setPwd(e.target.value)}
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: 'rgba(255,255,255,0.6)',
borderRadius: 2,
transition: 'all 0.2s ease',
'&:hover': { bgcolor: 'rgba(255,255,255,0.9)' },
'&.Mui-focused': { bgcolor: '#fff', boxShadow: '0 4px 12px rgba(0,0,0,0.05)' }
}
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
{show ? <VisibilityOff fontSize="small" sx={{ color: '#94a3b8' }} /> : <Visibility fontSize="small" sx={{ color: '#94a3b8' }} />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<FormControlLabel control={<Checkbox defaultChecked size="small" sx={{ color: '#cbd5e1', '&.Mui-checked': { color: 'primary.main' } }} />} label={<Typography variant="body2" sx={{ color: '#475569', fontWeight: 500 }}>Remember me</Typography>} />
<Link href="#" underline="hover" variant="body2" color="primary" sx={{ fontWeight: 600 }}>Forgot password?</Link>
</Stack>
</Card>
</Grid>
</Grid>
<Button
fullWidth
size="large"
variant="contained"
onClick={() => { localStorage.setItem('auth_token', 'demo-session'); navigate('/dashboard'); }}
sx={{
mt: 2,
py: 1.5,
borderRadius: 2,
fontSize: '1.05rem',
fontWeight: 700,
textTransform: 'none',
boxShadow: '0 8px 16px rgba(192, 18, 39, 0.25)',
'&:hover': {
boxShadow: '0 12px 20px rgba(192, 18, 39, 0.35)',
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease'
}}
>
Sign In
</Button>
</Stack>
</Card>
<Box sx={{ position: 'absolute', bottom: 20, zIndex: 2 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.8rem' }}>
© {new Date().getFullYear()} Doormile Logistics Pvt. Ltd. All rights reserved.
</Typography>
</Box>
</Box>
);
}

View File

@@ -1,95 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import { locations, tenantsList } from '@/data/mock';
const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971'];
export default function CreateCustomer() {
const navigate = useNavigate();
const [code, setCode] = useState('+91');
const [topLocation, setTopLocation] = useState('');
const [client, setClient] = useState('');
const [form, setForm] = useState({
name: '', phone: '', email: '', doorNo: '', address: '',
location: '', city: '', state: '', postcode: '', landmark: ''
});
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
return (
<>
<PageHeader
title="Create Customer"
breadcrumbs={[{ label: 'Customers', to: '/customers' }, { label: 'Create Customer' }]}
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2.5 }}>
<TextField select size="small" label="Location" value={topLocation} onChange={(e) => setTopLocation(e.target.value)} sx={{ minWidth: 220 }}>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<TextField select size="small" label="Client" value={client} onChange={(e) => setClient(e.target.value)} sx={{ minWidth: 220 }}>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
</Stack>
<MainCard title="Customer Details">
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Name" value={form.name} onChange={set('name')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth size="small" label="Phone Number" value={form.phone} onChange={set('phone')}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TextField
select variant="standard" value={code} onChange={(e) => setCode(e.target.value)}
InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }}
>
{COUNTRY_CODES.map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
</TextField>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Email" value={form.email} onChange={set('email')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Door No" value={form.doorNo} onChange={set('doorNo')} />
</Grid>
<Grid item xs={12}>
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth size="small" label="Location" value={form.location} onChange={set('location')}>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Post Code" value={form.postcode} onChange={set('postcode')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
</Grid>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1.5} sx={{ mt: 3 }}>
<Button variant="outlined" onClick={() => navigate('/customers')}>Cancel</Button>
<Button variant="contained" onClick={() => navigate('/customers')}>Create</Button>
</Stack>
</MainCard>
</>
);
}

View File

@@ -1,245 +0,0 @@
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Stack, Button, TextField, MenuItem, InputAdornment, Box,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Typography,
Dialog, DialogTitle, DialogContent, DialogActions, Divider
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar';
import { customers, locations } from '@/data/mock';
const EMPTY_FORM = {
name: '', phone: '', address: '', location: '', city: '', state: '',
postcode: '', landmark: '', lat: '', lng: ''
};
export default function Customers() {
const navigate = useNavigate();
const [search, setSearch] = useState('');
const [location, setLocation] = useState('all');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [editOpen, setEditOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
const [viewOpen, setViewOpen] = useState(false);
const [viewer, setViewer] = useState(null);
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const filtered = useMemo(
() =>
customers.filter((c) => {
const matchLocation = location === 'all' || c.location === location;
const matchSearch =
!search ||
[c.name, c.phone, c.email, c.address, c.location, c.city]
.join(' ')
.toLowerCase()
.includes(search.toLowerCase());
return matchLocation && matchSearch;
}),
[search, location]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
const openEdit = (c) => {
setForm({
name: c.name, phone: c.phone, address: c.address, location: c.location,
city: c.city, state: c.state, postcode: c.postcode, landmark: c.landmark || '',
lat: c.lat || '', lng: c.lng || ''
});
setEditOpen(true);
};
const openView = (c) => {
setViewer(c);
setViewOpen(true);
};
return (
<>
<PageHeader
title="Customers"
breadcrumbs={[{ label: 'Customers' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<TextField
select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }}
sx={{ minWidth: 160 }} label="Location"
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<TextField
size="small" placeholder="Search customers…" value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/customers/create')}>
Add Customer
</Button>
</Stack>
}
/>
<MainCard noPadding>
{filtered.length === 0 ? (
<EmptyState title="No Customers Found" caption="Try adjusting your search or location filter." />
) : (
<>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Name</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Location</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((c, i) => (
<TableRow key={c.id} hover>
<TableCell>{page * rpp + i + 1}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={c.name} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{c.name}</Typography>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2">{c.phone}</Typography>
<Typography variant="caption" color="text.secondary">{c.email}</Typography>
</TableCell>
<TableCell sx={{ maxWidth: 220 }}>
<Typography variant="body2" noWrap>{c.address}</Typography>
<Typography variant="caption" color="text.secondary">{c.city}, {c.state} {c.postcode}</Typography>
</TableCell>
<TableCell>{c.location}</TableCell>
<TableCell align="center">
<Tooltip title="View"><IconButton size="small" onClick={() => openView(c)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Edit"><IconButton size="small" onClick={() => openEdit(c)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }}
rowsPerPageOptions={[5, 10, 25]}
/>
</>
)}
</MainCard>
<Dialog open={viewOpen} onClose={() => setViewOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>Customer Details</DialogTitle>
<Divider />
<DialogContent>
{viewer && (
<>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<UserAvatar name={viewer.name} size={56} />
<Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800' }}>{viewer.name}</Typography>
<Typography variant="body2" color="text.secondary">{viewer.location}</Typography>
</Box>
</Stack>
<Grid container spacing={2}>
{[
['Phone', viewer.phone],
['Email', viewer.email],
['Address', viewer.address],
['City', viewer.city],
['State', viewer.state],
['Postcode', viewer.postcode],
['Landmark', viewer.landmark || '—'],
['Coordinates', viewer.lat && viewer.lng ? `${viewer.lat}, ${viewer.lng}` : '—']
].map(([label, value]) => (
<Grid item xs={12} sm={6} key={label}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{value}</Typography>
</Grid>
))}
</Grid>
</>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button variant="outlined" onClick={() => setViewOpen(false)}>Close</Button>
<Button
variant="contained"
onClick={() => { setViewOpen(false); openEdit(viewer); }}
>
Edit
</Button>
</DialogActions>
</Dialog>
<Dialog open={editOpen} onClose={() => setEditOpen(false)} maxWidth="md" fullWidth>
<DialogTitle sx={{ fontWeight: 700 }}>Edit Customer</DialogTitle>
<Divider />
<DialogContent>
<Grid container spacing={2.5} sx={{ mt: 0 }}>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Customer Name" value={form.name} onChange={set('name')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth size="small" label="Contact Number" value={form.phone} onChange={set('phone')}
InputProps={{ startAdornment: <InputAdornment position="start">+91</InputAdornment> }}
/>
</Grid>
<Grid item xs={12}>
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth size="small" label="Location" value={form.location} onChange={set('location')}>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Postcode" value={form.postcode} onChange={set('postcode')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
</Grid>
<Grid item xs={12} md={6} />
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Latitude" value={form.lat} onChange={set('lat')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth size="small" label="Longitude" value={form.lng} onChange={set('lng')} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button variant="outlined" onClick={() => setEditOpen(false)}>Close</Button>
<Button variant="contained" onClick={() => setEditOpen(false)}>Update</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,180 +0,0 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Box, Card, Stack, Button, Typography, Chip, Divider, IconButton,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Grid,
Dialog, DialogTitle, DialogContent, DialogActions, TextField
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import PageHeader from '@/components/PageHeader';
import Logo from '@/components/Logo';
import { invoices, invoiceLineItems, tenants } from '@/data/mock';
import { inr } from '@/utils/format';
export default function InvoicePreview() {
const navigate = useNavigate();
const { id } = useParams();
const [payOpen, setPayOpen] = useState(false);
const invoice = invoices.find((i) => String(i.id) === String(id)) || invoices[0];
const tenant = tenants[0];
const subTotal = invoiceLineItems.reduce((a, l) => a + l.amount, 0);
const discount = Math.round(subTotal * 0.05);
const taxable = subTotal - discount;
const tax = Math.round(taxable * 0.18);
const grandTotal = taxable + tax;
return (
<>
<PageHeader
title="Invoice Details"
breadcrumbs={[{ label: 'Invoice', to: '/invoice' }, { label: 'Details' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems="center">
<IconButton onClick={() => navigate('/invoice')} sx={{ border: 1, borderColor: 'grey.300' }}>
<ArrowBackIcon fontSize="small" />
</IconButton>
<Chip label={invoice.invoiceId} sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />
<Button variant="outlined" startIcon={<PaymentsOutlinedIcon />} onClick={() => setPayOpen(true)}>
Update Payment
</Button>
<Button variant="contained" startIcon={<PrintOutlinedIcon />} onClick={() => window.print()}>
Print
</Button>
</Stack>
}
/>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Card sx={{ width: '100%', maxWidth: 860, p: { xs: 3, sm: 5 } }}>
{/* Top band */}
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" spacing={3}>
<Box>
<Logo />
<Typography variant="subtitle2" sx={{ mt: 2, fontWeight: 700, color: 'grey.800' }}>From</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Doormile Logistics Pvt. Ltd.</Typography>
<Typography variant="body2" color="text.secondary">No. 7, Brigade Road</Typography>
<Typography variant="body2" color="text.secondary">Bengaluru, Karnataka 560001</Typography>
<Typography variant="body2" color="text.secondary">GSTIN: 29ABCDE1234F1Z5</Typography>
<Typography variant="body2" color="text.secondary">billing@doormile.in</Typography>
</Box>
<Box sx={{ textAlign: { sm: 'right' } }}>
<Typography variant="h4" sx={{ fontWeight: 800, color: 'primary.main' }}>INVOICE</Typography>
<Typography variant="subtitle2" sx={{ mt: 2, fontWeight: 700, color: 'grey.800' }}>To</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{tenant.name}</Typography>
<Typography variant="body2" color="text.secondary">{tenant.contact}</Typography>
<Typography variant="body2" color="text.secondary">{tenant.address}</Typography>
<Typography variant="body2" color="text.secondary">{tenant.city} {tenant.postcode}</Typography>
<Typography variant="body2" color="text.secondary">{tenant.email}</Typography>
</Box>
</Stack>
<Divider sx={{ my: 3 }} />
{/* Invoice meta */}
<Grid container spacing={2}>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">Invoice No</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.invoiceId}</Typography>
</Grid>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">Date</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.invoiceDate}</Typography>
</Grid>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">Due Date</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.dueDate}</Typography>
</Grid>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">Period</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.period}</Typography>
</Grid>
</Grid>
{/* Line items */}
<TableContainer sx={{ mt: 3 }}>
<Table size="small">
<TableHead>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700 } }}>
<TableCell>S.No</TableCell>
<TableCell>Particulars</TableCell>
<TableCell>Unit</TableCell>
<TableCell align="center">Quantity</TableCell>
<TableCell align="right">Rate</TableCell>
<TableCell align="right">Other Charges</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{invoiceLineItems.map((l, idx) => (
<TableRow key={idx}>
<TableCell>{idx + 1}</TableCell>
<TableCell>{l.particulars}</TableCell>
<TableCell>{l.unit}</TableCell>
<TableCell align="center">{l.qty}</TableCell>
<TableCell align="right">{inr(l.rate)}</TableCell>
<TableCell align="right">{inr(l.other)}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(l.amount)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{/* Totals */}
<Stack alignItems="flex-end" sx={{ mt: 3 }}>
<Box sx={{ width: { xs: '100%', sm: 320 } }}>
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
<Typography variant="body2" color="text.secondary">Sub Total</Typography>
<Typography variant="body2">{inr(subTotal)}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
<Typography variant="body2" color="text.secondary">Discount (5%)</Typography>
<Typography variant="body2">- {inr(discount)}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
<Typography variant="body2" color="text.secondary">Tax (18% GST)</Typography>
<Typography variant="body2">{inr(tax)}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.5 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Grand Total</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'primary.main' }}>{inr(grandTotal)}</Typography>
</Stack>
</Box>
</Stack>
{/* Notes + accent */}
<Box sx={{ mt: 4 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.800' }}>Notes &amp; Terms</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Payment is due within 15 days of the invoice date. Please make payments via NEFT/RTGS to the
registered account. A 1.5% monthly interest applies to overdue balances. This is a
computer-generated invoice and does not require a signature.
</Typography>
</Box>
<Box sx={{ mt: 3, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
</Card>
</Box>
{/* Update Payment Dialog */}
<Dialog open={payOpen} onClose={() => setPayOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Update Payment</DialogTitle>
<DialogContent>
<Stack spacing={2.5} sx={{ mt: 1 }}>
<TextField label="Reference No" type="number" fullWidth placeholder="Enter transaction reference" />
<TextField label="Remarks" fullWidth multiline minRows={3} placeholder="Add a remark for this payment" />
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setPayOpen(false)} color="inherit">Cancel</Button>
<Button variant="contained" onClick={() => setPayOpen(false)}>Update</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,154 +0,0 @@
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, Stack, Button, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, Typography
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip';
import TabLabelCount from '@/components/TabLabelCount';
import { invoices } from '@/data/mock';
import { inr } from '@/utils/format';
const TABS = [
{ key: 'all', label: 'All' },
{ key: 'open', label: 'Open' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'paid', label: 'Paid' }
];
export default function Invoices() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const [dateOpen, setDateOpen] = useState(false);
const [from, setFrom] = useState(dayjs().startOf('month'));
const [to, setTo] = useState(dayjs());
const totals = useMemo(() => {
const sum = (s) => invoices.filter((i) => i.status === s).reduce((a, i) => a + i.amount, 0);
return {
billed: invoices.reduce((a, i) => a + i.amount, 0),
paid: sum('paid'),
open: sum('open'),
overdue: sum('overdue')
};
}, []);
const counts = {
all: invoices.length,
open: invoices.filter((i) => i.status === 'open').length,
overdue: invoices.filter((i) => i.status === 'overdue').length,
paid: invoices.filter((i) => i.status === 'paid').length
};
const tabKey = TABS[tab].key;
const filtered = useMemo(
() => invoices.filter((i) => (tabKey === 'all' ? true : i.status === tabKey)),
[tabKey]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Invoice"
breadcrumbs={[{ label: 'Invoice' }]}
action={
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} onClick={() => setDateOpen(true)} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
{from.format('MMM DD')} {to.format('MMM DD')}
</Button>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Billed" value={inr(totals.billed)} icon={ReceiptLongOutlinedIcon} caption="All invoices" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Paid" value={inr(totals.paid)} icon={CheckCircleOutlineIcon} color="success" caption={`${counts.paid} invoices`} /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending / Open" value={inr(totals.open)} icon={HourglassEmptyOutlinedIcon} color="warning" caption={`${counts.open} invoices`} /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Overdue" value={inr(totals.overdue)} icon={ErrorOutlineOutlinedIcon} color="error" caption={`${counts.overdue} invoices`} /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Client</TableCell>
<TableCell>Invoice Id</TableCell>
<TableCell>Invoice Date</TableCell>
<TableCell>Due Date</TableCell>
<TableCell align="center">Count</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((row, idx) => (
<TableRow key={row.id} hover>
<TableCell>{page * rpp + idx + 1}</TableCell>
<TableCell>{row.client}</TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/invoice/${row.id}`)}>{row.invoiceId}</TableCell>
<TableCell>{row.invoiceDate}</TableCell>
<TableCell>{row.dueDate}</TableCell>
<TableCell align="center">{row.count}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
<TableCell><StatusChip status={row.status} /></TableCell>
<TableCell align="center">
<Tooltip title="Preview">
<IconButton size="small" color="primary" onClick={() => navigate(`/invoice/${row.id}`)}>
<VisibilityOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]}
/>
</Card>
<Dialog open={dateOpen} onClose={() => setDateOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Filter by Date</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Select a date range to filter invoices.
</Typography>
<Stack spacing={2.5} sx={{ mt: 1 }}>
<DatePicker label="From Date" value={from} onChange={(v) => v && setFrom(v)} slotProps={{ textField: { fullWidth: true } }} />
<DatePicker label="To Date" value={to} onChange={(v) => v && setTo(v)} slotProps={{ textField: { fullWidth: true } }} />
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setDateOpen(false)} color="inherit">Cancel</Button>
<Button variant="contained" onClick={() => setDateOpen(false)}>Apply</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,135 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Stack, Button, IconButton, Typography, Box, TextField, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import AutorenewIcon from '@mui/icons-material/Autorenew';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import UserAvatar from '@/components/UserAvatar';
import { orders, riders } from '@/data/mock';
import { inr } from '@/utils/format';
const ZONES = ['Zone A', 'Zone B', 'Zone C', 'Zone D'];
const PROFIT = [42, 58, 36, 50, 28, 64];
// derive assignment rows from orders + riders mock
const ROWS = orders.slice(0, 6).map((o, i) => ({
...o,
zone: ZONES[i % ZONES.length],
rider: riders[i % riders.length].name,
profit: PROFIT[i % PROFIT.length]
}));
export default function AssignOrders() {
const navigate = useNavigate();
const [payment, setPayment] = useState('all');
const [rider, setRider] = useState('auto');
return (
<>
<PageHeader
title={
<Stack direction="row" spacing={1.5} alignItems="center">
<IconButton onClick={() => navigate('/orders')} size="small"><ArrowBackIcon /></IconButton>
<span>Assign Orders</span>
</Stack>
}
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Assign Orders' }]}
action={
<Button variant="outlined" startIcon={<AutorenewIcon />}>Re-Assign</Button>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Orders" value={8} icon={Inventory2OutlinedIcon} caption="To assign" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Riders" value={5} icon={TwoWheelerOutlinedIcon} color="info" caption="Available" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Zones" value={4} icon={MapOutlinedIcon} color="warning" caption="Covered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Kilometer" value={64} icon={RouteOutlinedIcon} color="success" caption="Total route" /></Grid>
</Grid>
<MainCard title="Assignment Plan" noPadding sx={{ mt: 1.5 }}>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Zone</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Order Location</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Delivery</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Type</TableCell>
<TableCell align="right">Profit</TableCell>
<TableCell align="right">Charges</TableCell>
<TableCell align="right">KMS</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ROWS.map((r) => (
<TableRow key={r.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.id}</TableCell>
<TableCell>
<Chip size="small" label={r.zone} sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />
</TableCell>
<TableCell>{r.tenant}</TableCell>
<TableCell>{r.location}</TableCell>
<TableCell>{r.pickup}</TableCell>
<TableCell>{r.drop}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{r.notes || '—'}</Typography></TableCell>
<TableCell>
<Stack direction="row" spacing={1} alignItems="center">
<UserAvatar name={r.rider} size={28} />
<Typography variant="body2">{r.rider}</Typography>
</Stack>
</TableCell>
<TableCell><StatusChip status={r.payment} /></TableCell>
<TableCell align="right" sx={{ color: 'success.main', fontWeight: 600 }}>{inr(r.profit)}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.charges)}</TableCell>
<TableCell align="right">{r.kms}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</MainCard>
<Box sx={{ mt: 2.5 }}>
<Grid container spacing={2.5} alignItems="center">
<Grid item xs={12} sm={6} md={3}>
<TextField select fullWidth size="small" label="Choose Payment" value={payment} onChange={(e) => setPayment(e.target.value)}>
<MenuItem value="all">All Payments</MenuItem>
<MenuItem value="prepaid">Prepaid</MenuItem>
<MenuItem value="cod">COD</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<TextField select fullWidth size="small" label="Choose Rider" value={rider} onChange={(e) => setRider(e.target.value)}>
<MenuItem value="auto">Auto Assign</MenuItem>
{riders.map((rd) => <MenuItem key={rd.id} value={rd.id}>{rd.name}</MenuItem>)}
</TextField>
</Grid>
</Grid>
</Box>
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="outlined" startIcon={<ArrowBackIcon />} onClick={() => navigate('/orders')}>Back</Button>
<Button variant="contained" startIcon={<PersonAddAltOutlinedIcon />} onClick={() => navigate('/orders')}>
Assign Orders
</Button>
</Stack>
</>
);
}

View File

@@ -1,254 +0,0 @@
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box,
RadioGroup, Radio, FormControlLabel, FormControl, FormLabel, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip,
Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, ListItemButton,
ListItemIcon, ListItemText, Checkbox, InputAdornment
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';
import UploadFileOutlinedIcon from '@mui/icons-material/UploadFileOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import UserAvatar from '@/components/UserAvatar';
import { locations, tenantsList, tenants, customers } from '@/data/mock';
import { inr } from '@/utils/format';
const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00'];
// derive table rows from the customers mock
const buildRows = () =>
customers.slice(0, 4).map((c, i) => ({
id: c.id,
customer: c.name,
address: c.address,
qty: [2, 1, 3, 1][i],
cash: [0, 1299, 0, 450][i],
kms: [4.2, 12.8, 6.5, 9.1][i],
charge: [86, 142, 98, 110][i]
}));
export default function CreateMultipleOrders() {
const navigate = useNavigate();
const [date, setDate] = useState(dayjs());
const [slot, setSlot] = useState('');
const [dropMode, setDropMode] = useState('csv');
const [rows, setRows] = useState(buildRows);
const [dialogOpen, setDialogOpen] = useState(false);
const [search, setSearch] = useState('');
const [picked, setPicked] = useState([]);
const totals = useMemo(
() =>
rows.reduce(
(acc, r) => ({
qty: acc.qty + r.qty,
cash: acc.cash + r.cash,
kms: +(acc.kms + r.kms).toFixed(1),
charge: acc.charge + r.charge
}),
{ qty: 0, cash: 0, kms: 0, charge: 0 }
),
[rows]
);
const removeRow = (id) => setRows((p) => p.filter((r) => r.id !== id));
const filteredCustomers = customers.filter(
(c) => !search || `${c.name} ${c.location}`.toLowerCase().includes(search.toLowerCase())
);
const toggle = (id) => setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
const addSelected = () => {
const existing = new Set(rows.map((r) => r.id));
const additions = customers
.filter((c) => picked.includes(c.id) && !existing.has(c.id))
.map((c) => ({ id: c.id, customer: c.name, address: c.address, qty: 1, cash: 0, kms: 5, charge: 90 }));
setRows((p) => [...p, ...additions]);
setPicked([]);
setDialogOpen(false);
};
return (
<>
<PageHeader
title="Create Multiple Orders"
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Create Multiple Orders' }]}
/>
<Stack spacing={2.5}>
<Card>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
{/* top selects */}
<Grid container spacing={2.5}>
<Grid item xs={12} md={4}>
<TextField select fullWidth label="App Location" defaultValue="">
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={4}>
<TextField select fullWidth label="Choose Client" defaultValue="">
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={4}>
<TextField select fullWidth label="Business Location" defaultValue="">
{tenants.map((t) => <MenuItem key={t.id} value={t.id}>{t.address}, {t.city}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={4}>
<TextField fullWidth label="Pickup Location" value="Koramangala Hub, Bengaluru" InputProps={{ readOnly: true }} />
</Grid>
<Grid item xs={12} md={4}>
<DatePicker label="Delivery Date" value={date} onChange={setDate} slotProps={{ textField: { fullWidth: true } }} />
</Grid>
<Grid item xs={12} md={4}>
<TextField select fullWidth label="Pickup Time Slot" value={slot} onChange={(e) => setSlot(e.target.value)}>
{TIME_SLOTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
</TextField>
</Grid>
</Grid>
<Box sx={{ mt: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 600 }}>Drop</Typography>
<Divider sx={{ mt: 1, mb: 2 }} />
<FormControl>
<FormLabel sx={{ mb: 1, fontSize: 14 }}>Add drop points using</FormLabel>
<RadioGroup row value={dropMode} onChange={(e) => setDropMode(e.target.value)}>
<FormControlLabel value="csv" control={<Radio />} label="Excel / CSV" />
<FormControlLabel value="selection" control={<Radio />} label="Selection" />
</RadioGroup>
</FormControl>
<Box sx={{ mt: 1.5 }}>
{dropMode === 'csv' ? (
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<Button variant="contained" component="label" startIcon={<UploadFileOutlinedIcon />}>
Upload CSV
<input type="file" hidden accept=".csv,.xlsx" />
</Button>
<Chip
label="orders_batch.csv"
onDelete={() => {}}
sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }}
/>
</Stack>
) : (
<Button variant="outlined" startIcon={<PersonAddAltOutlinedIcon />} onClick={() => setDialogOpen(true)}>
Select Customers
</Button>
)}
</Box>
</Box>
</CardContent>
</Card>
<MainCard title="Drop Points" noPadding>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Address</TableCell>
<TableCell align="center">Quantity</TableCell>
<TableCell align="right">Cash Collect</TableCell>
<TableCell align="right">Kms</TableCell>
<TableCell align="right">Charge</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((r, i) => (
<TableRow key={r.id} hover>
<TableCell>{i + 1}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={r.customer} size={32} />
<Typography variant="body2">{r.customer}</Typography>
</Stack>
</TableCell>
<TableCell><Typography variant="body2" color="text.secondary">{r.address}</Typography></TableCell>
<TableCell align="center">{r.qty}</TableCell>
<TableCell align="right">{r.cash ? inr(r.cash) : '—'}</TableCell>
<TableCell align="right">{r.kms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.charge)}</TableCell>
<TableCell align="center">
<Tooltip title="Remove">
<IconButton size="small" color="error" onClick={() => removeRow(r.id)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
<TableRow sx={{ '& td': { fontWeight: 700, borderTop: 2, borderColor: 'divider' } }}>
<TableCell colSpan={3}>Total</TableCell>
<TableCell align="center">{totals.qty}</TableCell>
<TableCell align="right">{inr(totals.cash)}</TableCell>
<TableCell align="right">{totals.kms}</TableCell>
<TableCell align="right">{inr(totals.charge)}</TableCell>
<TableCell />
</TableRow>
</TableBody>
</Table>
</TableContainer>
</MainCard>
<Card>
<CardContent>
<TextField fullWidth label="Notes" placeholder="Notes for this batch of orders" multiline minRows={3} />
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders')}>
Create
</Button>
</Stack>
</CardContent>
</Card>
</Stack>
{/* Customer selection dialog */}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} fullWidth maxWidth="sm">
<DialogTitle>Select Customers</DialogTitle>
<DialogContent dividers>
<TextField
fullWidth
size="small"
placeholder="Search customers…"
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ mb: 1.5 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<List disablePadding>
{filteredCustomers.map((c) => (
<ListItem key={c.id} disablePadding>
<ListItemButton onClick={() => toggle(c.id)}>
<ListItemIcon sx={{ minWidth: 40 }}>
<Checkbox edge="start" checked={picked.includes(c.id)} tabIndex={-1} disableRipple />
</ListItemIcon>
<UserAvatar name={c.name} size={32} sx={{ mr: 1.5 }} />
<ListItemText primary={c.name} secondary={`${c.address} · ${c.location}`} />
</ListItemButton>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={addSelected} disabled={!picked.length}>
Add Selected{picked.length ? ` (${picked.length})` : ''}
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,142 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider,
Autocomplete, FormControlLabel, Checkbox, Box
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';
import AddIcon from '@mui/icons-material/Add';
import PageHeader from '@/components/PageHeader';
import { locations, tenantsList, tenants } from '@/data/mock';
const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00'];
const SectionTitle = ({ children }) => (
<>
<Typography variant="h5" sx={{ fontWeight: 600 }}>{children}</Typography>
<Divider sx={{ mt: 1, mb: 2.5 }} />
</>
);
function AddressFields({ saveForLater, onSaveForLater }) {
return (
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Contact Name" placeholder="Enter contact name" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Contact Number" placeholder="+91 ..." />
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Address" placeholder="Enter full address" multiline minRows={2} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Door No / Street" placeholder="e.g. 24, 1st Cross" />
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Location" defaultValue="">
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Postcode" placeholder="e.g. 560102" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Landmark" placeholder="Nearby landmark" />
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox checked={saveForLater} onChange={(e) => onSaveForLater(e.target.checked)} />}
label="Save for later"
/>
</Grid>
</Grid>
);
}
export default function CreateOrder() {
const navigate = useNavigate();
const [deliveryDate, setDeliveryDate] = useState(dayjs());
const [slot, setSlot] = useState('');
const [savePickup, setSavePickup] = useState(false);
const [saveDrop, setSaveDrop] = useState(false);
return (
<>
<PageHeader
title="Create Order"
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Create Order' }]}
/>
<Card>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
{/* Top row */}
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} md={4}>
<Autocomplete
options={locations}
renderInput={(params) => <TextField {...params} label="Location" placeholder="Select location" />}
/>
</Grid>
<Grid item xs={12} md={4}>
<Autocomplete
options={tenantsList}
renderInput={(params) => <TextField {...params} label="Client" placeholder="Select client" />}
/>
</Grid>
<Grid item xs={12} md={4}>
<Autocomplete
options={tenants.map((t) => `${t.name}${t.address}`)}
renderInput={(params) => <TextField {...params} label="Business Location" placeholder="Select business location" />}
/>
</Grid>
</Grid>
<Box sx={{ mt: 3 }}>
<SectionTitle>Pickup Details</SectionTitle>
<AddressFields saveForLater={savePickup} onSaveForLater={setSavePickup} />
</Box>
<Box sx={{ mt: 3 }}>
<SectionTitle>Drop Details</SectionTitle>
<AddressFields saveForLater={saveDrop} onSaveForLater={setSaveDrop} />
</Box>
<Box sx={{ mt: 3 }}>
<SectionTitle>Schedule</SectionTitle>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<DatePicker
label="Delivery Date"
value={deliveryDate}
onChange={setDeliveryDate}
slotProps={{ textField: { fullWidth: true } }}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Pickup Time Slot" value={slot} onChange={(e) => setSlot(e.target.value)}>
{TIME_SLOTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Notes" placeholder="Special instructions for this order" multiline minRows={3} />
</Grid>
</Grid>
</Box>
<Divider sx={{ mt: 3 }} />
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="outlined" onClick={() => navigate('/orders')}>
Cancel
</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders')}>
Create Order
</Button>
</Stack>
</CardContent>
</Card>
</>
);
}

View File

@@ -1,170 +0,0 @@
import { useParams, useNavigate } from 'react-router-dom';
import {
Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, IconButton, Avatar,
Step, Stepper, StepLabel, StepConnector, stepConnectorClasses, styled, Chip
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import CallOutlinedIcon from '@mui/icons-material/CallOutlined';
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import CheckIcon from '@mui/icons-material/Check';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import MapPlaceholder from '@/components/MapPlaceholder';
import UserAvatar from '@/components/UserAvatar';
import { orders, orderTimeline, deliveries } from '@/data/mock';
import { inr } from '@/utils/format';
const RedConnector = styled(StepConnector)(({ theme }) => ({
[`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.grey[300], borderLeftWidth: 2, minHeight: 28 },
[`&.${stepConnectorClasses.active} .${stepConnectorClasses.line}, &.${stepConnectorClasses.completed} .${stepConnectorClasses.line}`]:
{ borderColor: theme.palette.primary.main }
}));
function Dot({ active }) {
return (
<Box sx={{ width: 26, height: 26, borderRadius: '50%', bgcolor: active ? 'primary.main' : 'grey.300', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{active ? <CheckIcon sx={{ fontSize: 16 }} /> : <Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#fff' }} />}
</Box>
);
}
export default function OrderDetails() {
const { id } = useParams();
const navigate = useNavigate();
const order = orders.find((o) => o.id === id) || orders[1];
const delivery = deliveries.find((d) => d.id === order.id) || deliveries[1];
return (
<>
<PageHeader
title={
<Stack direction="row" spacing={1.5} alignItems="center">
<IconButton onClick={() => navigate('/orders')} size="small"><ArrowBackIcon /></IconButton>
<span>Order Details</span>
</Stack>
}
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: order.id }]}
action={
<Stack direction="row" spacing={1.5}>
<Button variant="outlined" color="error">Cancel Order</Button>
<Button variant="contained" startIcon={<EditOutlinedIcon />}>Edit Order</Button>
</Stack>
}
/>
<Grid container spacing={2.5}>
{/* Left column */}
<Grid item xs={12} md={5} lg={4}>
<Stack spacing={2.5}>
<Card>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="caption" color="text.secondary">Order ID</Typography>
<Typography variant="h4" sx={{ fontWeight: 700 }}>{order.id}</Typography>
</Box>
<StatusChip status={order.status} size="medium" />
</Stack>
<Divider sx={{ my: 2 }} />
<Stack spacing={1.25}>
<Row label="Created" value={order.date} />
<Row label="Tenant" value={order.tenant} />
<Row label="Payment" value={<StatusChip status={order.payment} />} />
<Row label="Distance" value={`${order.kms} km`} />
<Row label="Amount" value={<Typography variant="h5" color="primary.main">{inr(order.charges)}</Typography>} />
</Stack>
</CardContent>
</Card>
<MainCard title="Customer">
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
<UserAvatar name={order.customer} size={48} />
<Box>
<Typography variant="subtitle1">{order.customer}</Typography>
<Typography variant="caption" color="text.secondary">Recipient</Typography>
</Box>
</Stack>
<Stack spacing={1.25}>
<IconRow icon={PhoneOutlinedIcon} text="+91 98765 43210" />
<IconRow icon={LocationOnOutlinedIcon} text={`${order.drop}, ${order.location}`} />
</Stack>
</MainCard>
<MainCard title="Delivery Timeline">
<Stepper orientation="vertical" connector={<RedConnector />} sx={{ ml: 0.5 }}>
{orderTimeline.map((s) => (
<Step key={s.label} active completed={s.done}>
<StepLabel StepIconComponent={() => <Dot active={s.done} />}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="subtitle2" sx={{ fontWeight: s.done ? 600 : 500, color: s.done ? 'text.primary' : 'text.secondary' }}>{s.label}</Typography>
<Typography variant="caption" color="text.secondary">{s.time}</Typography>
</Stack>
</StepLabel>
</Step>
))}
</Stepper>
</MainCard>
</Stack>
</Grid>
{/* Right column */}
<Grid item xs={12} md={7} lg={8}>
<Stack spacing={2.5}>
<MainCard title="Live Tracking" noPadding>
<Box sx={{ p: 2 }}>
<MapPlaceholder height={380} label="In Transit" />
<Stack direction="row" spacing={3} sx={{ mt: 2 }}>
<RouteEnd color="#00A854" title="Pickup" text={order.pickup} />
<RouteEnd color="#C01227" title="Drop" text={order.drop} />
</Stack>
</Box>
</MainCard>
<MainCard title="Assigned Rider">
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'center' }} justifyContent="space-between">
<Stack direction="row" spacing={2} alignItems="center">
<UserAvatar name={delivery.rider} size={56} />
<Box>
<Typography variant="subtitle1">{delivery.rider}</Typography>
<Typography variant="caption" color="text.secondary">Rider · +91 98450 11223</Typography>
<Box sx={{ mt: 0.5 }}><Chip size="small" label="On the way" sx={{ bgcolor: 'info.lighter', color: 'info.dark' }} /></Box>
</Box>
</Stack>
<Button variant="contained" startIcon={<CallOutlinedIcon />}>Call Rider</Button>
</Stack>
</MainCard>
</Stack>
</Grid>
</Grid>
</>
);
}
const Row = ({ label, value }) => (
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="body2" color="text.secondary">{label}</Typography>
{typeof value === 'string' ? <Typography variant="subtitle2">{value}</Typography> : value}
</Stack>
);
const IconRow = ({ icon: Icon, text }) => (
<Stack direction="row" spacing={1.25} alignItems="center">
<Icon sx={{ fontSize: 18, color: 'grey.500' }} />
<Typography variant="body2">{text}</Typography>
</Stack>
);
const RouteEnd = ({ color, title, text }) => (
<Stack direction="row" spacing={1} alignItems="flex-start">
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: color, mt: 0.5 }} />
<Box>
<Typography variant="caption" color="text.secondary">{title}</Typography>
<Typography variant="subtitle2">{text}</Typography>
</Box>
</Stack>
);

View File

@@ -1,195 +0,0 @@
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
} 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';
const TABS = [
{ key: 'created', label: 'Created' },
{ key: 'pending', label: 'Pending' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'cancelled', label: 'Cancelled' }
];
export default function OrdersList() {
const navigate = useNavigate();
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 [selected, setSelected] = useState([]);
const tabKey = TABS[tab].key;
const filtered = useMemo(
() =>
orders.filter((o) => {
const matchTab = tabKey === 'created' ? true : o.status === tabKey;
const matchTenant = tenant === 'all' || o.tenant === tenant;
const matchSearch =
!search ||
[o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchTenant && matchSearch;
}),
[tabKey, tenant, search]
);
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
};
return (
<>
<PageHeader
title="Orders"
breadcrumbs={[{ label: 'Orders' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}>
Create Multiple
</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>
Create Order
</Button>
</Stack>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Created Orders" value={counts.created} icon={Inventory2OutlinedIcon} caption="100%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending Orders" value={counts.pending} icon={HourglassEmptyOutlinedIcon} color="warning" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered Orders" value={counts.delivered} icon={CheckCircleOutlineIcon} color="success" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cancelled Orders" value={counts.cancelled} icon={CancelOutlinedIcon} color="error" caption="12.5%" /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}>
{/* filter toolbar */}
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search orders…" value={search} onChange={(e) => setSearch(e.target.value)}
sx={{ minWidth: 240 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant="outlined" size="medium" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem>
{[...new Set(orders.map((o) => o.tenant))].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{[...new Set(orders.map((o) => o.location))].map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
))}
</Tabs>
</Box>
{selected.length > 0 && (
<Stack direction="row" alignItems="center" spacing={2} sx={{ px: 2, py: 1, bgcolor: 'primary.lighter' }}>
<Typography variant="subtitle2" color="primary.dark">{selected.length} selected</Typography>
<Button size="small" color="error" startIcon={<DeleteOutlineIcon />}>Delete</Button>
</Stack>
)}
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={selected.length > 0 && selected.length < paged.length}
checked={paged.length > 0 && selected.length === paged.length}
onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])}
/>
</TableCell>
<TableCell>#</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Location</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell align="center">QTY</TableCell>
<TableCell align="right">COD</TableCell>
<TableCell align="right">KMS</TableCell>
<TableCell align="right">Charges</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((o) => (
<TableRow key={o.id} hover selected={selected.includes(o.id)}>
<TableCell padding="checkbox"><Checkbox checked={selected.includes(o.id)} onChange={() => toggle(o.id)} /></TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/orders/${o.id}`)}>{o.id}</TableCell>
<TableCell>{o.tenant}</TableCell>
<TableCell>{o.location}</TableCell>
<TableCell>{o.pickup}</TableCell>
<TableCell>{o.drop}</TableCell>
<TableCell align="center">{o.qty}</TableCell>
<TableCell align="right">{o.cod ? inr(o.cod) : '—'}</TableCell>
<TableCell align="right">{o.kms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{o.notes || '—'}</Typography></TableCell>
<TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="center">
<Tooltip title="View"><IconButton size="small" onClick={() => navigate(`/orders/${o.id}`)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Edit"><IconButton size="small"><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
<SpeedDial ariaLabel="Order actions" icon={<TuneIcon />} sx={{ position: 'fixed', bottom: 28, right: 28 }} FabProps={{ color: 'primary' }}>
<SpeedDialAction icon={<AutoAwesomeOutlinedIcon />} tooltipTitle="AI Optimisation" onClick={() => navigate('/orders/assign')} />
<SpeedDialAction icon={<PersonAddAltOutlinedIcon />} tooltipTitle="Manual Assign" onClick={() => navigate('/orders/assign')} />
<SpeedDialAction icon={<DeleteOutlineIcon />} tooltipTitle="Delete" />
</SpeedDial>
</>
);
}

View File

@@ -1,190 +0,0 @@
import { useState, useMemo } from 'react';
import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, IconButton, Tooltip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Typography,
Dialog, DialogTitle, DialogContent, DialogActions
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
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 { inr } from '@/utils/format';
const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled'];
export default function OrdersDetails() {
const [location, setLocation] = useState('all');
const [tenant, setTenant] = useState('all');
const [loc2, setLoc2] = useState('all');
const [status, setStatus] = useState('all');
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const [mapRow, setMapRow] = useState(null);
const [exportOpen, setExportOpen] = useState(false);
const filtered = useMemo(
() =>
ordersDetailReport.filter((o) => {
const matchStatus = status === 'all' || o.status === status;
const matchTenant = tenant === 'all' || o.client === tenant;
const matchSearch =
!search ||
[o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase());
return matchStatus && matchTenant && matchSearch;
}),
[status, tenant, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Orders Details"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Details' }]}
action={
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/>
<Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap>
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={status} onChange={(e) => { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status">
{STATUSES.map((s) => <MenuItem key={s} value={s}>{s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}</MenuItem>)}
</TextField>
<TextField
size="small" placeholder="Search orders…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant="contained" startIcon={<FileDownloadOutlinedIcon />} onClick={() => setExportOpen(true)}>
Export Report
</Button>
</Stack>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell />
<TableCell>Client</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Assigned</TableCell>
<TableCell align="center">Accepted</TableCell>
<TableCell align="center">Arrived</TableCell>
<TableCell align="center">Picked</TableCell>
<TableCell align="center">Active</TableCell>
<TableCell align="center">Delivered</TableCell>
<TableCell align="center">Cancelled</TableCell>
<TableCell>Notes</TableCell>
<TableCell align="right">KMS</TableCell>
<TableCell align="right">Charges</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((o, i) => (
<TableRow key={o.id} hover>
<TableCell>{page * rpp + i + 1}</TableCell>
<TableCell padding="checkbox">
<Tooltip title="View route">
<IconButton size="small" color="primary" onClick={() => setMapRow(o)}><MapOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
</TableCell>
<TableCell sx={{ fontWeight: 600 }}>{o.client}</TableCell>
<TableCell>{o.pickup}</TableCell>
<TableCell>{o.drop}</TableCell>
<TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="center">{o.assigned}</TableCell>
<TableCell align="center">{o.accepted}</TableCell>
<TableCell align="center">{o.arrived}</TableCell>
<TableCell align="center">{o.picked}</TableCell>
<TableCell align="center">{o.active}</TableCell>
<TableCell align="center">{o.delivered}</TableCell>
<TableCell align="center">{o.cancelled}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{o.notes || '—'}</Typography></TableCell>
<TableCell align="right">{o.kms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[10, 25, 50]}
/>
</Card>
{/* Map dialog */}
<Dialog open={!!mapRow} onClose={() => setMapRow(null)} fullScreen>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
Route {mapRow?.id}
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
{mapRow?.pickup} {mapRow?.drop}
</Typography>
</Box>
<IconButton onClick={() => setMapRow(null)}><CloseIcon /></IconButton>
</DialogTitle>
<DialogContent dividers>
<MapPlaceholder height={520} label="Route" />
</DialogContent>
</Dialog>
{/* Export dialog */}
<Dialog open={exportOpen} onClose={() => setExportOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Export Report</DialogTitle>
<DialogContent dividers>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
The export will include {filtered.length} record(s) matching the current filters:
</Typography>
<Grid container spacing={1.5}>
<Filter label="Location" value={location === 'all' ? 'All Locations' : location} />
<Filter label="Tenant" value={tenant === 'all' ? 'All Tenants' : tenant} />
<Filter label="Location (2)" value={loc2 === 'all' ? 'All Locations' : loc2} />
<Filter label="Status" value={status === 'all' ? 'All Status' : status} />
<Filter label="Date Range" value="Jun 01 Jun 05" />
<Filter label="Search" value={search || '—'} />
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => setExportOpen(false)}>Cancel</Button>
<Button variant="contained" startIcon={<FileDownloadOutlinedIcon />} onClick={() => setExportOpen(false)}>Download CSV</Button>
</DialogActions>
</Dialog>
</>
);
}
function Filter({ label, value }) {
return (
<Grid item xs={6}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="subtitle2" sx={{ textTransform: 'capitalize' }}>{value}</Typography>
</Grid>
);
}

View File

@@ -1,195 +0,0 @@
import { useState, Fragment } from 'react';
import {
Grid, Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography
} from '@mui/material';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
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 { inr } from '@/utils/format';
// Show 0 in red, anything else normally.
function NumCell({ value, align = 'center', bold = false }) {
const zero = Number(value) === 0;
return (
<TableCell align={align} sx={{ fontWeight: bold ? 700 : 400, color: zero ? 'error.main' : 'inherit' }}>
{value}
</TableCell>
);
}
export default function OrdersSummary() {
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(
(a, r) => ({
oPending: a.oPending + r.orders.pending,
oCancelled: a.oCancelled + r.orders.cancelled,
oCompleted: a.oCompleted + r.orders.completed,
dPending: a.dPending + r.deliveries.pending,
dCancelled: a.dCancelled + r.deliveries.cancelled,
dCompleted: a.dCompleted + r.deliveries.completed,
collection: a.collection + r.collection,
kms: a.kms + r.kms,
actualKms: a.actualKms + r.actualKms,
amount: a.amount + r.amount
}),
{ 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' };
return (
<>
<PageHeader
title="Orders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Summary' }]}
action={
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/>
<Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<Box sx={{ flexGrow: 1 }} />
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Stack>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={headSx} rowSpan={2} />
<TableCell sx={headSx} rowSpan={2}>#</TableCell>
<TableCell sx={headSx} rowSpan={2}>Tenant / Location</TableCell>
<TableCell sx={headSx} colSpan={3} align="center">Orders</TableCell>
<TableCell sx={headSx} colSpan={3} align="center">Deliveries</TableCell>
<TableCell sx={headSx} rowSpan={2} align="right">Collection Amt</TableCell>
<TableCell sx={headSx} rowSpan={2} align="center">Kms / Actual</TableCell>
<TableCell sx={headSx} rowSpan={2} align="right">Amount</TableCell>
<TableCell sx={headSx} rowSpan={2} align="center">Action</TableCell>
</TableRow>
<TableRow>
<TableCell sx={headSx} align="center">Pending</TableCell>
<TableCell sx={headSx} align="center">Cancelled</TableCell>
<TableCell sx={headSx} align="center">Completed</TableCell>
<TableCell sx={headSx} align="center">Pending</TableCell>
<TableCell sx={headSx} align="center">Cancelled</TableCell>
<TableCell sx={headSx} align="center">Completed</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ordersSummary.map((r, idx) => (
<Fragment key={r.id}>
<TableRow hover>
<TableCell padding="checkbox">
<IconButton size="small" onClick={() => toggle(r.id)}>
{open[r.id] ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</TableCell>
<TableCell>{idx + 1}</TableCell>
<TableCell>
<Typography variant="subtitle2">{r.tenant}</Typography>
<Typography variant="caption" color="text.secondary">{r.location}</Typography>
</TableCell>
<NumCell value={r.orders.pending} />
<NumCell value={r.orders.cancelled} />
<NumCell value={r.orders.completed} />
<NumCell value={r.deliveries.pending} />
<NumCell value={r.deliveries.cancelled} />
<NumCell value={r.deliveries.completed} />
<TableCell align="right">{inr(r.collection)}</TableCell>
<TableCell align="center">{r.kms} / {r.actualKms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.amount)}</TableCell>
<TableCell align="center">
<IconButton size="small" onClick={() => toggle(r.id)}><VisibilityOutlinedIcon fontSize="small" /></IconButton>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={13} sx={{ py: 0, border: 0 }}>
<Collapse in={!!open[r.id]} timeout="auto" unmountOnExit>
<Box sx={{ m: 1.5, p: 1.5, bgcolor: 'grey.50', borderRadius: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Riders</Typography>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={headSx}>#</TableCell>
<TableCell sx={headSx}>Rider</TableCell>
<TableCell sx={headSx} align="center">Orders</TableCell>
<TableCell sx={headSx} align="center">Deliveries</TableCell>
<TableCell sx={headSx} align="center">Pending</TableCell>
<TableCell sx={headSx} align="center">Cancelled</TableCell>
<TableCell sx={headSx} align="center">Completed</TableCell>
<TableCell sx={headSx} align="right">Collection Amt</TableCell>
<TableCell sx={headSx} align="center">Kms / Actual</TableCell>
<TableCell sx={headSx} align="right">Charges</TableCell>
</TableRow>
</TableHead>
<TableBody>
{r.riders.map((rd, i) => (
<TableRow key={rd.rider} hover>
<TableCell>{i + 1}</TableCell>
<TableCell>{rd.rider}</TableCell>
<NumCell value={rd.orders} />
<NumCell value={rd.deliveries} />
<NumCell value={rd.pending} />
<NumCell value={rd.cancelled} />
<NumCell value={rd.completed} />
<TableCell align="right">{inr(rd.collection)}</TableCell>
<TableCell align="center">{rd.kms} / {rd.actualKms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(rd.charges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
))}
{/* totals */}
<TableRow sx={{ '& td': { bgcolor: 'primary.lighter', borderTop: 2, borderColor: 'primary.light' } }}>
<TableCell />
<TableCell colSpan={2} sx={{ fontWeight: 700 }}>Totals</TableCell>
<NumCell value={totals.oPending} bold />
<NumCell value={totals.oCancelled} bold />
<NumCell value={totals.oCompleted} bold />
<NumCell value={totals.dPending} bold />
<NumCell value={totals.dCancelled} bold />
<NumCell value={totals.dCompleted} bold />
<TableCell align="right" sx={{ fontWeight: 700 }}>{inr(totals.collection)}</TableCell>
<TableCell align="center" sx={{ fontWeight: 700 }}>{totals.kms} / {totals.actualKms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 700 }}>{inr(totals.amount)}</TableCell>
<TableCell />
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Card>
</>
);
}

View File

@@ -1,105 +0,0 @@
import { useState, useMemo } from 'react';
import {
Box, Paper, Stack, Button, TextField, InputAdornment, IconButton, Checkbox, Typography, Divider, Tooltip
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import MenuIcon from '@mui/icons-material/Menu';
import RefreshIcon from '@mui/icons-material/Refresh';
import PageHeader from '@/components/PageHeader';
import MapPlaceholder from '@/components/MapPlaceholder';
import { ridersLive } from '@/data/mock';
// spread active riders across the map at distinct positions
const POSITIONS = [
{ x: '24%', y: '32%' },
{ x: '58%', y: '28%' },
{ x: '40%', y: '60%' },
{ x: '72%', y: '55%' },
{ x: '30%', y: '72%' },
{ x: '64%', y: '78%' }
];
export default function RidersLogs() {
const [search, setSearch] = useState('');
const [selected, setSelected] = useState(ridersLive.filter((r) => r.active).map((r) => r.userid));
const filtered = useMemo(
() =>
ridersLive.filter((r) =>
!search || [r.name, r.phone, r.userid].join(' ').toLowerCase().includes(search.toLowerCase())
),
[search]
);
const allChecked = filtered.length > 0 && filtered.every((r) => selected.includes(r.userid));
const someChecked = filtered.some((r) => selected.includes(r.userid)) && !allChecked;
const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
const toggleAll = (checked) =>
setSelected(checked ? [...new Set([...selected, ...filtered.map((r) => r.userid)])] : selected.filter((id) => !filtered.some((r) => r.userid === id)));
const mapRiders = ridersLive
.filter((r) => r.active && selected.includes(r.userid))
.map((r, i) => ({ ...POSITIONS[i % POSITIONS.length], active: true }));
return (
<>
<PageHeader
title="Riders Locations"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Locations' }]}
/>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 2.5, alignItems: 'stretch' }}>
{/* Left side panel */}
<Paper variant="outlined" sx={{ width: { xs: '100%', md: 300 }, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ p: 2 }}>
<TextField
fullWidth size="small" placeholder="Search Rider…" value={search} onChange={(e) => setSearch(e.target.value)}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
</Box>
<Divider />
<Stack direction="row" alignItems="center" sx={{ px: 1, py: 0.5 }}>
<Checkbox checked={allChecked} indeterminate={someChecked} onChange={(e) => toggleAll(e.target.checked)} />
<Typography variant="subtitle2">All</Typography>
<Box sx={{ flexGrow: 1 }} />
<Typography variant="caption" color="text.secondary" sx={{ pr: 1 }}>{selected.length} selected</Typography>
</Stack>
<Divider />
<Box sx={{ flexGrow: 1, overflowY: 'auto', maxHeight: { xs: 360, md: 620 } }}>
{filtered.map((r) => (
<Stack key={r.userid} direction="row" alignItems="flex-start" spacing={0.5} sx={{ px: 1, py: 1, borderBottom: 1, borderColor: 'divider' }}>
<Checkbox size="small" checked={selected.includes(r.userid)} onChange={() => toggle(r.userid)} sx={{ mt: -0.5 }} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" alignItems="center" spacing={0.75}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: r.active ? 'success.main' : 'grey.400', flexShrink: 0 }} />
<Typography variant="subtitle2" noWrap>{r.name}</Typography>
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{r.phone}</Typography>
<Stack direction="row" justifyContent="space-between">
<Typography variant="caption" color="text.secondary">{r.userid}</Typography>
<Typography variant="caption" color={r.active ? 'success.main' : 'text.disabled'}>{r.lastLog}</Typography>
</Stack>
</Box>
</Stack>
))}
{filtered.length === 0 && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', p: 2, textAlign: 'center' }}>No riders found</Typography>
)}
</Box>
</Paper>
{/* Map area */}
<Paper variant="outlined" sx={{ flexGrow: 1, p: 2, display: 'flex', flexDirection: 'column' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2 }}>
<Tooltip title="Menu"><IconButton size="small"><MenuIcon /></IconButton></Tooltip>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1 }}>Riders Locations</Typography>
<Button variant="contained" startIcon={<RefreshIcon />}>Refresh</Button>
</Stack>
<MapPlaceholder height={620} label="Riders Locations" showRoute={false} riders={mapRiders} />
</Paper>
</Box>
</>
);
}

View File

@@ -1,167 +0,0 @@
import { useState, Fragment } from 'react';
import {
Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton, Chip, Tooltip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography,
Dialog, DialogTitle, DialogContent
} from '@mui/material';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import RoomOutlinedIcon from '@mui/icons-material/RoomOutlined';
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 { inr } from '@/utils/format';
function NumCell({ value, align = 'center' }) {
const zero = Number(value) === 0;
return <TableCell align={align} sx={{ color: zero ? 'error.main' : 'inherit' }}>{value}</TableCell>;
}
function KmsChips({ kms, actual }) {
return (
<Stack direction="row" spacing={0.5} justifyContent="center">
<Chip size="small" label={`${kms} km`} sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />
<Chip size="small" label={`${actual} km`} variant="outlined" />
</Stack>
);
}
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' };
return (
<>
<PageHeader
title="Riders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Summary' }]}
action={
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/>
<Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<Box sx={{ flexGrow: 1 }} />
</Stack>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={headSx}>#</TableCell>
<TableCell sx={headSx}>Rider</TableCell>
<TableCell sx={headSx} align="center">Orders</TableCell>
<TableCell sx={headSx} align="center">Pending</TableCell>
<TableCell sx={headSx} align="center">Cancelled</TableCell>
<TableCell sx={headSx} align="center">Delivered</TableCell>
<TableCell sx={headSx} align="center">KMS</TableCell>
<TableCell sx={headSx} align="right">Amount</TableCell>
<TableCell sx={headSx} align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{ridersSummary.map((r, idx) => (
<Fragment key={r.id}>
<TableRow hover>
<TableCell>{idx + 1}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.5} alignItems="center">
<UserAvatar name={r.rider} size={32} />
<Typography variant="subtitle2">{r.rider}</Typography>
</Stack>
</TableCell>
<NumCell value={r.orders} />
<NumCell value={r.pending} />
<NumCell value={r.cancelled} />
<NumCell value={r.delivered} />
<TableCell align="center"><KmsChips kms={r.kms} actual={r.actualKms} /></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.amount)}</TableCell>
<TableCell align="center">
<Tooltip title={open[r.id] ? 'Collapse' : 'Expand'}>
<IconButton size="small" onClick={() => toggle(r.id)}>
{open[r.id] ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</Tooltip>
<Tooltip title="Location">
<IconButton size="small" color="primary" onClick={() => setMapRider(r)}><RoomOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={9} sx={{ py: 0, border: 0 }}>
<Collapse in={!!open[r.id]} timeout="auto" unmountOnExit>
<Box sx={{ m: 1.5, p: 1.5, bgcolor: 'grey.50', borderRadius: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Client Summary</Typography>
<Table size="small">
<TableHead>
<TableRow>
<TableCell sx={headSx}>#</TableCell>
<TableCell sx={headSx}>Client</TableCell>
<TableCell sx={headSx} align="center">All</TableCell>
<TableCell sx={headSx} align="center">Pending</TableCell>
<TableCell sx={headSx} align="center">Completed</TableCell>
<TableCell sx={headSx} align="center">Cancelled</TableCell>
<TableCell sx={headSx} align="center">Kms</TableCell>
<TableCell sx={headSx} align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{r.clients.map((c, i) => (
<TableRow key={c.client} hover>
<TableCell>{i + 1}</TableCell>
<TableCell>{c.client}</TableCell>
<NumCell value={c.all} />
<NumCell value={c.pending} />
<NumCell value={c.completed} />
<NumCell value={c.cancelled} />
<TableCell align="center"><KmsChips kms={c.kms} actual={c.actualKms} /></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(c.amount)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={1} sx={{ p: 2, borderTop: 1, borderColor: 'divider' }}>
<Typography variant="subtitle2" color="text.secondary">Total Amount</Typography>
<Typography variant="h5" color="primary.main" sx={{ fontWeight: 700 }}>{inr(totalAmount)}</Typography>
</Stack>
</Card>
<Dialog open={!!mapRider} onClose={() => setMapRider(null)} maxWidth="md" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{mapRider?.rider} Location
<IconButton onClick={() => setMapRider(null)}><CloseIcon /></IconButton>
</DialogTitle>
<DialogContent dividers>
<MapPlaceholder height={420} label="Rider Location" riders={[{ x: '50%', y: '45%', active: true }]} />
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,71 +0,0 @@
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, CardContent, Stack, Button, TextField, Divider, InputAdornment
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import PageHeader from '@/components/PageHeader';
export default function CreateRider() {
const navigate = useNavigate();
return (
<>
<PageHeader
title="Create Rider"
breadcrumbs={[{ label: 'Riders', to: '/riders' }, { label: 'Create Rider' }]}
/>
<Card>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Admin Name" placeholder="Enter admin name" />
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth label="Phone Number" placeholder="98450 11223"
InputProps={{ startAdornment: <InputAdornment position="start">+91</InputAdornment> }}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Email Address" placeholder="rider@doormile.in" />
</Grid>
<Grid item xs={12} md={6} />
<Grid item xs={12}>
<TextField fullWidth label="Address" placeholder="Enter full address" multiline minRows={2} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Suburb" placeholder="Enter suburb" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="City" placeholder="Enter city" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="State" placeholder="Enter state" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Post Code" placeholder="e.g. 560102" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Door No" placeholder="e.g. 24" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Landmark" placeholder="Nearby landmark" />
</Grid>
</Grid>
<Divider sx={{ mt: 3 }} />
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="outlined" onClick={() => navigate('/riders')}>
Cancel
</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders')}>
Create
</Button>
</Stack>
</CardContent>
</Card>
</>
);
}

View File

@@ -1,180 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box, IconButton, InputAdornment
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import PageHeader from '@/components/PageHeader';
import { riders, tenantsList } from '@/data/mock';
const SHIFTS = ['Morning', 'Evening', 'Night'];
const ACCOUNT_TYPES = ['Savings', 'Current'];
const VEHICLES = ['Honda Activa', 'TVS Jupiter', 'Hero Splendor', 'Bajaj Pulsar', 'Honda Dio'];
const SectionTitle = ({ children }) => (
<>
<Typography variant="h5" sx={{ fontWeight: 600 }}>{children}</Typography>
<Divider sx={{ mt: 1, mb: 2.5 }} />
</>
);
export default function EditRider() {
const navigate = useNavigate();
const rider = riders[0];
const [firstName] = rider.name.split(' ');
const lastName = rider.name.split(' ').slice(1).join(' ');
const [insuranceExpiry, setInsuranceExpiry] = useState(dayjs().add(8, 'month'));
return (
<>
<PageHeader
title={
<Stack direction="row" spacing={1} alignItems="center">
<IconButton size="small" onClick={() => navigate('/riders')}><ArrowBackIcon /></IconButton>
<span>Edit Rider</span>
</Stack>
}
breadcrumbs={[{ label: 'Riders', to: '/riders' }, { label: 'Edit Rider' }]}
/>
<Card sx={{ mb: 10 }}>
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
<SectionTitle>Contact Information</SectionTitle>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField fullWidth label="First Name" defaultValue={firstName} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Last Name" defaultValue={lastName} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Phone Number" defaultValue={rider.phone} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Email" placeholder="rider@doormile.in" />
</Grid>
<Grid item xs={12}>
<TextField fullWidth label="Address" defaultValue={rider.address} multiline minRows={2} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Location / Suburb" defaultValue={rider.address.split(',')[0]} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="City" defaultValue={(rider.address.split(',')[1] || '').trim()} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="State" placeholder="Enter state" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Identification No" placeholder="Aadhaar / ID number" />
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Choose Partner" defaultValue="">
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
</Grid>
</Grid>
<Box sx={{ mt: 3 }}>
<SectionTitle>Charges</SectionTitle>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Shift Type" defaultValue={rider.shift}>
{SHIFTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Base Fare" defaultValue={rider.fare} InputProps={{ startAdornment: <InputAdornment position="start"></InputAdornment> }} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Additional Kms" placeholder="Per extra km" InputProps={{ startAdornment: <InputAdornment position="start"></InputAdornment> }} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Other Charges" placeholder="0" InputProps={{ startAdornment: <InputAdornment position="start"></InputAdornment> }} />
</Grid>
</Grid>
</Box>
<Box sx={{ mt: 3 }}>
<SectionTitle>Bank Details</SectionTitle>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Account No" placeholder="Enter account number" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Account Name" defaultValue={rider.name} />
</Grid>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Account Type" defaultValue="Savings">
{ACCOUNT_TYPES.map((a) => <MenuItem key={a} value={a}>{a}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Bank Name" placeholder="Enter bank name" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="IFSC Code" placeholder="e.g. HDFC0001234" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Branch" placeholder="Enter branch" />
</Grid>
</Grid>
</Box>
<Box sx={{ mt: 3 }}>
<SectionTitle>Vehicle Details</SectionTitle>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<TextField select fullWidth label="Vehicle Name" defaultValue={rider.vehicle}>
{VEHICLES.map((v) => <MenuItem key={v} value={v}>{v}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Vehicle No" defaultValue={rider.vehicleNo} />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Model Year" placeholder="e.g. 2022" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Vehicle Color" placeholder="e.g. Black" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="License No" placeholder="Driving license number" />
</Grid>
<Grid item xs={12} md={6}>
<TextField fullWidth label="Insurance No" placeholder="Insurance policy number" />
</Grid>
<Grid item xs={12} md={6}>
<DatePicker
label="Insurance Expiry Date"
value={insuranceExpiry}
onChange={setInsuranceExpiry}
slotProps={{ textField: { fullWidth: true } }}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
<Box
sx={{
position: 'sticky', bottom: 0, py: 2, px: { xs: 2, md: 3 }, mt: -8,
bgcolor: 'background.paper', borderTop: 1, borderColor: 'divider', zIndex: 2
}}
>
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
<Button variant="outlined" startIcon={<ArrowBackIcon />} onClick={() => navigate('/riders')}>
Back
</Button>
<Button variant="contained" onClick={() => navigate('/riders')}>
Update
</Button>
</Stack>
</Box>
</>
);
}

View File

@@ -1,221 +0,0 @@
import { useState, useMemo, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip,
TablePagination, Typography, Chip, Collapse
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PowerSettingsNewOutlinedIcon from '@mui/icons-material/PowerSettingsNewOutlined';
import PageHeader from '@/components/PageHeader';
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 { inr } from '@/utils/format';
const TABS = [
{ key: 'all', label: 'ALL' },
{ key: 'active', label: 'Active' }
];
export default function Riders() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [location, setLocation] = useState('all');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [expanded, setExpanded] = useState(null);
const tabKey = TABS[tab].key;
const filtered = useMemo(
() =>
riders.filter((r) => {
const matchTab = tabKey === 'all' ? true : r.status !== 'offline';
const matchLocation = location === 'all' || r.address.includes(location);
const matchSearch =
!search ||
[r.id, r.userId, r.name, r.phone, r.address, r.vehicle, r.vehicleNo]
.join(' ')
.toLowerCase()
.includes(search.toLowerCase());
return matchTab && matchLocation && matchSearch;
}),
[tabKey, location, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
const counts = {
total: riders.length,
active: riders.filter((r) => r.status === 'online').length,
onDelivery: riders.filter((r) => r.status === 'on-delivery').length,
offline: riders.filter((r) => r.status === 'offline').length,
all: riders.length,
activeTab: riders.filter((r) => r.status !== 'offline').length
};
return (
<>
<PageHeader
title="Riders"
breadcrumbs={[{ label: 'Riders' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders/create')}>
Add Rider
</Button>
</Stack>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Riders" value={counts.total} icon={GroupsOutlinedIcon} caption="All registered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active" value={counts.active} icon={CheckCircleOutlineIcon} color="success" caption="Online now" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="On Delivery" value={counts.onDelivery} icon={TwoWheelerOutlinedIcon} color="info" caption="In transit" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Offline" value={counts.offline} icon={PowerSettingsNewOutlinedIcon} color="error" caption="Not available" /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search riders…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 240 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab
key={t.key}
label={<TabLabelCount label={t.label} count={t.key === 'all' ? counts.all : counts.activeTab} active={tab === i} />}
/>
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.NO</TableCell>
<TableCell>User ID</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Address</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell>Shift</TableCell>
<TableCell>Time</TableCell>
<TableCell align="right">Fare</TableCell>
<TableCell align="right">Fuel</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((r, i) => (
<Fragment key={r.id}>
<TableRow hover>
<TableCell>{page * rpp + i + 1}</TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.userId}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={r.name} size={32} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{r.name}</Typography>
<Typography variant="caption" color="text.secondary">{r.phone}</Typography>
</Box>
</Stack>
</TableCell>
<TableCell>{r.address}</TableCell>
<TableCell>
<Typography variant="body2">{r.vehicle}</Typography>
<Typography variant="caption" color="text.secondary">{r.vehicleNo}</Typography>
</TableCell>
<TableCell>{r.shift}</TableCell>
<TableCell>
<Stack direction="row" spacing={0.75} alignItems="center">
<Chip size="small" label={r.start} sx={{ bgcolor: 'success.lighter', color: 'success.main', fontWeight: 600 }} />
<Chip size="small" label={r.end} sx={{ bgcolor: 'error.lighter', color: 'error.main', fontWeight: 600 }} />
</Stack>
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.fare)}</TableCell>
<TableCell align="right">{inr(r.fuel)}</TableCell>
<TableCell><StatusChip status={r.status} /></TableCell>
<TableCell align="center">
<Tooltip title="Edit">
<IconButton size="small" onClick={() => navigate(`/riders/${r.id}/edit`)}><EditOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
<Tooltip title={expanded === r.id ? 'Hide logs' : 'View logs'}>
<IconButton size="small" onClick={() => setExpanded(expanded === r.id ? null : r.id)}>
{expanded === r.id ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={11} sx={{ p: 0, border: 0 }}>
<Collapse in={expanded === r.id} timeout="auto" unmountOnExit>
<Box sx={{ p: 2.5, bgcolor: 'grey.50' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1.5 }}>Rider Logs</Typography>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Location</TableCell>
<TableCell>Battery</TableCell>
<TableCell>Charging</TableCell>
<TableCell>Speed</TableCell>
<TableCell>Accuracy</TableCell>
<TableCell>Time</TableCell>
<TableCell>Order</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{riderLogs.map((log, li) => (
<TableRow key={li}>
<TableCell>{log.location}</TableCell>
<TableCell>{log.battery}</TableCell>
<TableCell>{log.charging}</TableCell>
<TableCell>{log.speed}</TableCell>
<TableCell>{log.accuracy}</TableCell>
<TableCell>{log.time}</TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{log.order}</TableCell>
<TableCell><StatusChip status={log.status} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
</>
);
}

View File

@@ -0,0 +1,304 @@
import { useState, useMemo, useEffect } from 'react';
import {
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Chip, Link,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
TablePagination, Typography, CircularProgress, Alert, Tooltip,
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import MailOutlineIcon from '@mui/icons-material/MailOutline';
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
import ManageAccountsOutlinedIcon from '@mui/icons-material/ManageAccountsOutlined';
import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined';
import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount';
import { fetchPoints, deletePoint, COLLECTIONS } from '@/utils/qdrant';
import UserFormDialog from './UserFormDialog';
// Map a raw Qdrant point from doormile_auth to a flat team-user row.
function toUser(point) {
const p = point.payload || {};
return {
id: point.id,
name: p.name || '—',
email: p.email || '',
phone: p.phone || '',
role: p.role || 'unknown',
pin: p.pin || ''
};
}
const titleCase = (s) => String(s || '').replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
// Per-role accent colour + icon, used for the avatar badge and role chip.
const ROLE_META = {
admin: { color: 'primary', icon: AdminPanelSettingsOutlinedIcon },
manager: { color: 'success', icon: ManageAccountsOutlinedIcon },
rep: { color: 'info', icon: BadgeOutlinedIcon },
support: { color: 'warning', icon: SupportAgentOutlinedIcon }
};
const roleMeta = (role) => ROLE_META[String(role || '').toLowerCase()] || { color: 'secondary', icon: PersonOutlineOutlinedIcon };
// Avatar with a small role-coloured badge in the corner.
function UserIdentity({ name, email, role }) {
const { color, icon: RoleIcon } = roleMeta(role);
const handle = email ? email.split('@')[0] : '';
return (
<Stack direction="row" spacing={1.5} alignItems="center">
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<UserAvatar name={name} size={40} sx={{ border: '2px solid', borderColor: `${color}.lighter` }} />
<Box
sx={{
position: 'absolute', right: -3, bottom: -3, width: 18, height: 18, borderRadius: '50%',
bgcolor: `${color}.main`, color: '#fff', display: 'inline-flex', alignItems: 'center',
justifyContent: 'center', border: '2px solid #fff'
}}
>
<RoleIcon sx={{ fontSize: 11 }} />
</Box>
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800', lineHeight: 1.25 }}>{name}</Typography>
{handle && <Typography variant="caption" color="text.secondary">@{handle}</Typography>}
</Box>
</Stack>
);
}
function RoleCell({ role }) {
const { color, icon: RoleIcon } = roleMeta(role);
return (
<Chip
size="small"
icon={<RoleIcon sx={{ fontSize: 15, ml: 0.5 }} />}
label={titleCase(role)}
sx={{
fontWeight: 600,
bgcolor: `${color === 'secondary' ? 'grey.100' : `${color}.lighter`}`,
color: `${color === 'secondary' ? 'grey.700' : `${color}.dark`}`,
'& .MuiChip-icon': { color: 'inherit' }
}}
/>
);
}
export default function TeamUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const [dialog, setDialog] = useState({ open: false, mode: 'add', initial: null });
const [toDelete, setToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
const load = () => {
setLoading(true);
setError(null);
fetchPoints(COLLECTIONS.teamUsers)
.then((points) => setUsers(points.map(toUser)))
.catch((e) => setError(e.message || 'Failed to load team users'))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
const tabs = useMemo(() => {
const seen = [];
users.forEach((u) => { if (!seen.includes(u.role)) seen.push(u.role); });
return [{ key: 'all', label: 'All' }, ...seen.map((r) => ({ key: r, label: titleCase(r) }))];
}, [users]);
const tabKey = tabs[Math.min(tab, tabs.length - 1)]?.key || 'all';
const counts = useMemo(() => {
const c = { all: users.length };
users.forEach((u) => { c[u.role] = (c[u.role] || 0) + 1; });
return c;
}, [users]);
const filtered = useMemo(
() =>
users.filter((u) => {
const matchTab = tabKey === 'all' || u.role === tabKey;
const matchSearch =
!search || [u.name, u.email, u.phone, u.role].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchSearch;
}),
[users, tabKey, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
const handleSaved = () => { setDialog({ open: false, mode: 'add', initial: null }); load(); };
const confirmDelete = async () => {
setDeleting(true);
try {
await deletePoint(COLLECTIONS.teamUsers, toDelete.id);
setToDelete(null);
load();
} catch (e) {
setError(e.message || 'Failed to delete user');
} finally {
setDeleting(false);
}
};
return (
<>
<PageHeader
title="App Users"
breadcrumbs={[{ label: 'App Users' }]}
action={
<Stack direction="row" spacing={1.5}>
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading}>Refresh</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add User</Button>
</Stack>
}
/>
<Card sx={{ overflow: 'hidden' }}>
<Box
sx={{
px: 2.5, py: 2, borderBottom: 1, borderColor: 'divider',
display: 'flex', alignItems: 'center', gap: 1.5,
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
}}
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: 'primary.lighter', color: 'primary.main', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<GroupsOutlinedIcon fontSize="small" />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.2 }}>App Users Directory</Typography>
<Typography variant="caption" color="text.secondary">Manage console members, roles and access</Typography>
</Box>
</Box>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search by name, email, phone…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 300 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
{!loading && (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" color="text.secondary">
{filtered.length} {filtered.length === 1 ? 'user' : 'users'}
</Typography>
<Chip size="small" label="live · doormile_auth" sx={{ height: 22, fontSize: '0.7rem', bgcolor: 'success.lighter', color: 'success.dark', fontWeight: 600 }} />
</Stack>
)}
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={Math.min(tab, tabs.length - 1)} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{tabs.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key] || 0} active={tab === i} />} />
))}
</Tabs>
</Box>
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
<TableContainer>
<Table sx={{ minWidth: 800 }}>
<TableHead>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell sx={{ width: 64 }}>S.No</TableCell>
<TableCell>User</TableCell>
<TableCell>Email</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Role</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} sx={{ border: 'none' }}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
</TableCell>
</TableRow>
) : paged.length === 0 ? (
<TableRow>
<TableCell colSpan={6} sx={{ border: 'none' }}>
<EmptyState title="No team users found" caption="Try a different role or search term, or add a user." />
</TableCell>
</TableRow>
) : (
paged.map((row, i) => (
<TableRow key={row.id} hover>
<TableCell><Typography variant="body2" color="text.secondary">{page * rpp + i + 1}</Typography></TableCell>
<TableCell><UserIdentity name={row.name} email={row.email} role={row.role} /></TableCell>
<TableCell>
{row.email ? (
<Link href={`mailto:${row.email}`} underline="hover" color="text.primary" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75, fontSize: '0.8125rem' }}>
<MailOutlineIcon sx={{ fontSize: 15, color: 'grey.400' }} />{row.email}
</Link>
) : <Typography variant="body2" color="text.disabled"></Typography>}
</TableCell>
<TableCell>
{row.phone ? (
<Link href={`tel:${row.phone}`} underline="hover" color="text.primary" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75, fontSize: '0.8125rem' }}>
<PhoneOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />{row.phone}
</Link>
) : <Typography variant="body2" color="text.disabled"></Typography>}
</TableCell>
<TableCell><RoleCell role={row.role} /></TableCell>
<TableCell align="right">
<Tooltip title="Edit"><IconButton size="small" onClick={() => setDialog({ open: true, mode: 'edit', initial: row })}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete"><IconButton size="small" color="error" onClick={() => setToDelete(row)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
<UserFormDialog
open={dialog.open}
mode={dialog.mode}
initial={dialog.initial}
onClose={() => setDialog({ open: false, mode: 'add', initial: null })}
onSaved={handleSaved}
/>
<Dialog open={!!toDelete} onClose={deleting ? undefined : () => setToDelete(null)}>
<DialogTitle>Delete user?</DialogTitle>
<DialogContent>
<DialogContentText>
This will permanently remove <strong>{toDelete?.name}</strong> from the doormile_auth collection. This cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button onClick={() => setToDelete(null)} disabled={deleting}>Cancel</Button>
<Button color="error" variant="contained" onClick={confirmDelete} disabled={deleting} startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : null}>Delete</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,86 @@
import { useState, useEffect } from 'react';
import {
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
MenuItem, Alert, CircularProgress, IconButton
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
const ROLES = ['admin', 'rep', 'manager'];
const EMPTY = { name: '', email: '', phone: '', role: 'rep', pin: '' };
const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts);
export default function UserFormDialog({ open, mode, initial, onClose, onSaved }) {
const isEdit = mode === 'edit';
const [form, setForm] = useState(EMPTY);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (open) {
setForm({ ...EMPTY, ...(initial || {}) });
setError(null);
}
}, [open, initial]);
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const handleSave = async () => {
if (!form.name.trim()) { setError('Name is required.'); return; }
if (!form.email.trim()) { setError('Email is required.'); return; }
setSaving(true);
setError(null);
const payload = {
name: form.name.trim(),
email: form.email.trim(),
phone: form.phone,
role: form.role,
...(form.pin ? { pin: String(form.pin) } : {})
};
try {
if (isEdit) {
await setPayload(COLLECTIONS.teamUsers, initial.id, payload);
} else {
await createPoint(COLLECTIONS.teamUsers, payload);
}
onSaved();
} catch (e) {
setError(e.message || 'Failed to save user');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="sm" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{isEdit ? 'Edit Team User' : 'Add Team User'}
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
</DialogTitle>
<DialogContent dividers>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Grid container spacing={2} sx={{ mt: 0 }}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Name *" value={form.name} onChange={set('name')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Email *" value={form.email} onChange={set('email')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Phone" value={form.phone} onChange={set('phone')} /></Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Role" value={form.role} onChange={set('role')}>
{withValue(ROLES, form.role).map((o) => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="PIN" value={form.pin} onChange={set('pin')} /></Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button onClick={onClose} disabled={saving}>Cancel</Button>
<Button variant="contained" onClick={handleSave} disabled={saving} startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}>
{isEdit ? 'Save Changes' : 'Create User'}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,158 @@
import { useState, useEffect } from 'react';
import {
Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, TextField,
MenuItem, Box, Typography, Divider, Alert, CircularProgress, IconButton
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { createPoint, setPayload, COLLECTIONS } from '@/utils/qdrant';
const BUSINESS_TYPES = ['retail', 'wholesale', 'manufacturer', 'distributor', 'services', 'ecommerce', 'other'];
const STATUSES = ['newClient', 'contacted', 'onboarded', 'lost'];
const FREQUENCIES = ['Daily', 'Weekly', 'Fortnightly', 'Monthly', 'Occasional'];
const CONSENTS = ['basicOnly', 'full', 'none'];
const EMPTY = {
name: '', phone: '', city: '', businessState: '', businessType: 'retail', status: 'newClient',
frequency: 'Daily', parcelVolume: 0, activeContracts: 0, provider: '', efficiency: '',
logisticsSegment: '', transitFrom: '', transitTo: '', neighbourhood: '',
surveyAddress: '', surveyLat: '', surveyLng: '', dataConsent: 'basicOnly', notes: ''
};
// Ensure a select always has its current value among the options.
const withValue = (opts, v) => (v && !opts.includes(v) ? [v, ...opts] : opts);
export default function ClientFormDialog({ open, mode, initial, onClose, onSaved }) {
const isEdit = mode === 'edit';
const [form, setForm] = useState(EMPTY);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (open) {
setForm({ ...EMPTY, ...(initial || {}) });
setError(null);
}
}, [open, initial]);
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const handleSave = async () => {
if (!form.name.trim()) { setError('Client name is required.'); return; }
setSaving(true);
setError(null);
const lat = form.surveyLat === '' ? undefined : Number(form.surveyLat);
const lng = form.surveyLng === '' ? undefined : Number(form.surveyLng);
const payload = {
name: form.name.trim(),
phone: form.phone,
city: form.city,
businessState: form.businessState,
businessType: form.businessType,
status: form.status,
frequency: form.frequency,
parcelVolume: Number(form.parcelVolume) || 0,
activeContracts: Number(form.activeContracts) || 0,
provider: form.provider,
efficiency: form.efficiency,
logisticsSegment: form.logisticsSegment,
transitFrom: form.transitFrom,
transitTo: form.transitTo,
neighbourhood: form.neighbourhood,
surveyAddress: form.surveyAddress,
surveyZone: form.neighbourhood,
dataConsent: form.dataConsent,
notes: form.notes,
lastUpdated: new Date().toISOString().slice(0, 10),
...(lat != null ? { surveyLat: lat } : {}),
...(lng != null ? { surveyLng: lng } : {}),
...(lat != null && lng != null ? { surveyGeo: { lat, lon: lng } } : {})
};
try {
if (isEdit) {
await setPayload(COLLECTIONS.clients, initial.id, payload);
} else {
await createPoint(COLLECTIONS.clients, {
...payload,
clientId: `client_${Date.now()}`,
surveySubmitted: false
});
}
onSaved();
} catch (e) {
setError(e.message || 'Failed to save client');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onClose={saving ? undefined : onClose} maxWidth="md" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{isEdit ? 'Edit Client' : 'Add Client'}
<IconButton onClick={onClose} size="small" disabled={saving}><CloseIcon /></IconButton>
</DialogTitle>
<DialogContent dividers>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Typography variant="overline" color="text.secondary">Business</Typography>
<Grid container spacing={2} sx={{ mt: 0, mb: 1 }}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Client Name *" value={form.name} onChange={set('name')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Phone" value={form.phone} onChange={set('phone')} /></Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Business Type" value={form.businessType} onChange={set('businessType')}>
{withValue(BUSINESS_TYPES, form.businessType).map((o) => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Status" value={form.status} onChange={set('status')}>
{withValue(STATUSES, form.status).map((o) => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Order Frequency" value={form.frequency} onChange={set('frequency')}>
{withValue(FREQUENCIES, form.frequency).map((o) => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12} sm={3}><TextField fullWidth size="small" type="number" label="Parcel Volume" value={form.parcelVolume} onChange={set('parcelVolume')} /></Grid>
<Grid item xs={12} sm={3}><TextField fullWidth size="small" type="number" label="Active Contracts" value={form.activeContracts} onChange={set('activeContracts')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Current Provider" value={form.provider} onChange={set('provider')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Efficiency" value={form.efficiency} onChange={set('efficiency')} /></Grid>
<Grid item xs={12}><TextField fullWidth size="small" label="Logistics Segment" value={form.logisticsSegment} onChange={set('logisticsSegment')} placeholder="First Mile, Last Mile" /></Grid>
</Grid>
<Divider sx={{ my: 1.5 }} />
<Typography variant="overline" color="text.secondary">Location & Transit</Typography>
<Grid container spacing={2} sx={{ mt: 0, mb: 1 }}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="State" value={form.businessState} onChange={set('businessState')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Neighbourhood / Zone" value={form.neighbourhood} onChange={set('neighbourhood')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Transit From" value={form.transitFrom} onChange={set('transitFrom')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Transit To" value={form.transitTo} onChange={set('transitTo')} /></Grid>
<Grid item xs={12} sm={3}><TextField fullWidth size="small" type="number" label="Latitude" value={form.surveyLat} onChange={set('surveyLat')} /></Grid>
<Grid item xs={12} sm={3}><TextField fullWidth size="small" type="number" label="Longitude" value={form.surveyLng} onChange={set('surveyLng')} /></Grid>
<Grid item xs={12}><TextField fullWidth size="small" label="Survey Address" value={form.surveyAddress} onChange={set('surveyAddress')} multiline minRows={2} /></Grid>
</Grid>
<Divider sx={{ my: 1.5 }} />
<Typography variant="overline" color="text.secondary">Other</Typography>
<Grid container spacing={2} sx={{ mt: 0 }}>
<Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Data Consent" value={form.dataConsent} onChange={set('dataConsent')}>
{withValue(CONSENTS, form.dataConsent).map((o) => <MenuItem key={o} value={o}>{o}</MenuItem>)}
</TextField>
</Grid>
<Grid item xs={12}><TextField fullWidth size="small" label="Notes" value={form.notes} onChange={set('notes')} multiline minRows={2} /></Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button onClick={onClose} disabled={saving}>Cancel</Button>
<Button variant="contained" onClick={handleSave} disabled={saving} startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}>
{isEdit ? 'Save Changes' : 'Create Client'}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -1,81 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971'];
export default function CreateClient() {
const navigate = useNavigate();
const [code, setCode] = useState('+91');
const [form, setForm] = useState({
adminName: '', phone: '', email: '', address: '',
suburb: '', city: '', state: '', postcode: '', doorNo: '', landmark: ''
});
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
return (
<>
<PageHeader
title="Create Client"
breadcrumbs={[{ label: 'Tenants', to: '/tenants' }, { label: 'Create Client' }]}
/>
<MainCard title="Client Details">
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Admin Name" value={form.adminName} onChange={set('adminName')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth size="small" label="Phone Number" value={form.phone} onChange={set('phone')}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TextField
select variant="standard" value={code} onChange={(e) => setCode(e.target.value)}
InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }}
>
{COUNTRY_CODES.map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
</TextField>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Email Address" value={form.email} onChange={set('email')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Door No" value={form.doorNo} onChange={set('doorNo')} />
</Grid>
<Grid item xs={12}>
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Suburb" value={form.suburb} onChange={set('suburb')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Post Code" value={form.postcode} onChange={set('postcode')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
</Grid>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1.5} sx={{ mt: 3 }}>
<Button variant="outlined" onClick={() => navigate('/tenants')}>Cancel</Button>
<Button variant="contained" onClick={() => navigate('/tenants')}>Create</Button>
</Stack>
</MainCard>
</>
);
}

View File

@@ -1,160 +1,396 @@
import { useState, useMemo, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import { useState, useMemo, useEffect, Fragment } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab,
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab, Grid,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Typography, Collapse, Grid
TablePagination, Typography, Collapse, CircularProgress, Alert, Tooltip, Chip, Divider, Link,
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import HandshakeOutlinedIcon from '@mui/icons-material/HandshakeOutlined';
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import NotesOutlinedIcon from '@mui/icons-material/NotesOutlined';
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount';
import { tenants, tenantPricing } from '@/data/mock';
import { inr } from '@/utils/format';
import { fetchPoints, deletePoint, COLLECTIONS } from '@/utils/qdrant';
import ClientFormDialog from './ClientFormDialog';
const TABS = [
{ key: 'active', label: 'Active' },
{ key: 'pending', label: 'Pending' },
{ key: 'inactive', label: 'Inactive' }
];
const generateLogicalId = (id) => {
const str = String(id).replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
return 'CLI-' + str.substring(0, 6).padStart(6, '0');
};
// Map a raw Qdrant point from doormile_clients to a flat client row.
function toClient(point) {
const p = point.payload || {};
// Qdrant point.id is usually a UUID.
// We generate a clean CLI-XXXXXX format based on it.
const logicalId = generateLogicalId(point.id);
return {
id: point.id,
logicalId,
// Force override the ugly payload client_timestamp string with the clean ID
clientId: logicalId,
name: p.name || '—',
phone: p.phone || '',
city: p.city || '',
businessState: p.businessState || '',
businessType: p.businessType || '',
status: p.status || 'unknown',
parcelVolume: p.parcelVolume ?? 0,
activeContracts: p.activeContracts ?? 0,
frequency: p.frequency || '',
provider: p.provider || '',
efficiency: p.efficiency || '',
logisticsSegment: p.logisticsSegment || '',
transitFrom: p.transitFrom || '',
transitTo: p.transitTo || '',
neighbourhood: p.neighbourhood || p.surveyZone || '',
surveyAddress: p.surveyAddress || '',
surveyLat: p.surveyLat ?? p.surveyGeo?.lat ?? '',
surveyLng: p.surveyLng ?? p.surveyGeo?.lon ?? '',
dataConsent: p.dataConsent || '',
lastUpdated: p.lastUpdated || '',
notes: p.notes || ''
};
}
// Humanize raw payload tokens like `basicOnly`, `newClient`, `first_mile` → "Basic Only".
const humanize = (s) =>
String(s || '')
.replace(/[_-]+/g, ' ')
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
.replace(/\s+/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase())
.trim();
const titleCase = humanize;
// Map categorical enum values to a semantic palette color.
const consentTone = (v) => ({ full: 'success', basiconly: 'info', none: 'default' }[String(v || '').toLowerCase()] || 'default');
const efficiencyTone = (v) => {
const k = String(v || '').toLowerCase();
if (/high|good|excellent/.test(k)) return 'success';
if (/med|average|moderate/.test(k)) return 'warning';
if (/low|poor|bad/.test(k)) return 'error';
return 'info';
};
const FIELD_LABEL_SX = { textTransform: 'uppercase', letterSpacing: 0.4, fontSize: '0.68rem', fontWeight: 700, color: 'grey.700' };
// A soft, theme-tinted pill for categorical values. Falls back to a dash placeholder.
function Pill({ label, color = 'default' }) {
if (label === undefined || label === null || label === '') {
return <Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.500' }}></Typography>;
}
return (
<Chip
size="small"
label={label}
sx={{
fontWeight: 600,
...(color === 'default'
? { bgcolor: 'grey.100', color: 'grey.700' }
: { bgcolor: `${color}.lighter`, color: `${color}.dark` })
}}
/>
);
}
// Label + arbitrary node value (text, pill, or grouped chips).
function Field({ label, children }) {
return (
<Box>
<Typography variant="caption" color="text.secondary" sx={FIELD_LABEL_SX}>{label}</Typography>
<Box sx={{ mt: 0.5 }}>{children}</Box>
</Box>
);
}
function ReadField({ label, value }) {
return (
<Box>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.800' }}>{value || '—'}</Typography>
</Box>
<Field label={label}>
<Typography variant="body2" sx={{ fontWeight: 500, color: value ? 'grey.800' : 'grey.500', wordBreak: 'break-word' }}>
{value || '—'}
</Typography>
</Field>
);
}
function PricingTab() {
// A bordered sub-card with a tinted header strip — frames each group of fields.
function SectionCard({ icon: Icon, title, accent = 'primary', children }) {
return (
<Box>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
<Button variant="contained" startIcon={<AddIcon />}>Add Pricing</Button>
<Box sx={{ height: '100%', borderRadius: 2, border: 1, borderColor: 'divider', bgcolor: 'background.paper', overflow: 'hidden' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ px: 2, py: 1.25, borderBottom: 1, borderColor: 'divider', bgcolor: `${accent}.lighter`, opacity: 0.999 }}>
<Icon sx={{ fontSize: 18, color: `${accent}.main` }} />
<Typography variant="overline" sx={{ fontWeight: 700, color: 'grey.800', letterSpacing: 0.6, lineHeight: 1 }}>{title}</Typography>
</Stack>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1, overflow: 'hidden' }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Slab</TableCell>
<TableCell align="right">Base Price</TableCell>
<TableCell align="right">Min Kms</TableCell>
<TableCell align="right">Price/Km</TableCell>
<TableCell align="right">Other Charges</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tenantPricing.map((p, i) => (
<TableRow key={i}>
<TableCell>{p.date}</TableCell>
<TableCell>{p.slab}</TableCell>
<TableCell align="right">{inr(p.basePrice)}</TableCell>
<TableCell align="right">{p.minKms}</TableCell>
<TableCell align="right">{inr(p.pricePerKm)}</TableCell>
<TableCell align="right">{inr(p.otherCharges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
<Stack spacing={1.75} sx={{ p: 2 }}>{children}</Stack>
</Box>
);
}
function EditTab({ tenant }) {
const [form, setForm] = useState({
name: tenant.name, contact: tenant.contact, phone: tenant.phone, email: tenant.email,
address: tenant.address, city: tenant.city, postcode: tenant.postcode, lat: tenant.lat, lng: tenant.lng
});
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
// Mini KPI tile used inside the Business card for the headline numbers.
function StatTile({ label, value, icon: Icon, color = 'primary' }) {
return (
<Box>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Tenant Name" value={form.name} onChange={set('name')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Person" value={form.contact} onChange={set('contact')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Number" value={form.phone} onChange={set('phone')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Email" value={form.email} onChange={set('email')} /></Grid>
<Grid item xs={12}><TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="PostCode" value={form.postcode} onChange={set('postcode')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Latitude" value={form.lat} onChange={set('lat')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Longitude" value={form.lng} onChange={set('lng')} /></Grid>
</Grid>
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="contained">Update</Button>
<Box sx={{ flex: 1, p: 1.5, borderRadius: 1.5, border: 1, borderColor: 'divider', bgcolor: 'grey.50' }}>
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 32, height: 32, borderRadius: 1, bgcolor: `${color}.lighter`, color: `${color}.main`, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon sx={{ fontSize: 18 }} />
</Box>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.1 }}>{value}</Typography>
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Box>
</Stack>
</Box>
);
}
function TenantRow({ row, index }) {
// Compact metric used inline in the table so key numbers show without expanding.
function Metric({ label, value, color = 'grey.800' }) {
return (
<Box sx={{ textAlign: 'center', minWidth: 56 }}>
<Typography variant="body2" sx={{ fontWeight: 700, color, lineHeight: 1.2 }}>{value}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>{label}</Typography>
</Box>
);
}
function ClientRow({ row, index, onEdit, onDelete }) {
const [open, setOpen] = useState(false);
const [inner, setInner] = useState(0);
return (
<Fragment>
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
<TableRow
hover
sx={{
cursor: 'pointer',
'& > *': { borderBottom: open ? 'unset' : undefined },
...(open && { bgcolor: 'primary.lighter', '&:hover': { bgcolor: 'primary.lighter' } })
}}
onClick={() => setOpen((o) => !o)}
>
<TableCell padding="checkbox">
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{index + 1}</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap' }}>
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 700, color: 'primary.main', bgcolor: 'primary.lighter', px: 1, py: 0.5, borderRadius: 1, border: '1px solid', borderColor: 'primary.light', whiteSpace: 'nowrap' }}>
{row.logicalId}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={row.name} size={34} />
<Box>
<UserAvatar name={row.name} size={38} />
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
<Typography variant="caption" color="text.secondary">{row.volume} orders / mo</Typography>
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 0.4 }}>
{row.businessType && (
<Chip
size="small"
label={titleCase(row.businessType)}
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600, bgcolor: 'grey.100', color: 'grey.700' }}
/>
)}
</Stack>
</Box>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2">{row.contact}</Typography>
<Typography variant="caption" color="text.secondary">{row.phone}</Typography>
<Stack direction="row" spacing={0.75} alignItems="center">
<PhoneOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
<Box>
<Typography variant="body2">{row.phone || '—'}</Typography>
{row.frequency && <Typography variant="caption" color="text.secondary">{row.frequency}</Typography>}
</Box>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2">{row.address}</Typography>
<Typography variant="caption" color="text.secondary">{row.city} · {row.postcode}</Typography>
<Stack direction="row" spacing={0.75} alignItems="center">
<PlaceOutlinedIcon sx={{ fontSize: 15, color: 'grey.400' }} />
<Box>
<Typography variant="body2">{row.city || '—'}{row.businessState ? `, ${row.businessState}` : ''}</Typography>
{row.neighbourhood && <Typography variant="caption" color="text.secondary">{row.neighbourhood}</Typography>}
</Box>
</Stack>
</TableCell>
<TableCell>
<Stack direction="row" spacing={1.5} alignItems="center" justifyContent="center" divider={<Divider orientation="vertical" flexItem />}>
<Metric label="Parcels" value={Number(row.parcelVolume).toLocaleString()} />
<Metric label="Contracts" value={row.activeContracts} color="primary.main" />
</Stack>
</TableCell>
<TableCell><StatusChip status={row.status} /></TableCell>
<TableCell align="right">
<Tooltip title="Edit"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(row); }}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete"><IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(row); }}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={6} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
<TableCell colSpan={8} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ m: 2, borderRadius: 1, border: 1, borderColor: 'divider', overflow: 'hidden' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', bgcolor: 'grey.50' }}>
<Tabs value={inner} onChange={(_, v) => setInner(v)} sx={{ px: 2 }}>
<Tab label="Details" />
<Tab label="Pricing" />
<Tab label="Edit" />
</Tabs>
</Box>
<Box sx={{ p: 2.5 }}>
{inner === 0 && (
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6} md={4}><ReadField label="Name" value={row.name} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Contact Person" value={row.contact} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Phone" value={row.phone} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="E-Mail" value={row.email} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Address" value={row.address} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="City" value={row.city} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="PostCode" value={row.postcode} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Latitude" value={row.lat} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Longitude" value={row.lng} /></Grid>
<Box sx={{ m: 2, borderRadius: 2.5, border: 1, borderColor: 'divider', overflow: 'hidden', boxShadow: '0 4px 16px rgba(0,0,0,0.06)' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1.5}
sx={{
px: 2.5, py: 1.75, borderBottom: 1, borderColor: 'divider',
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}88 0%, ${theme.palette.background.paper} 75%)`
}}
>
<Stack direction="row" spacing={1.5} alignItems="center">
<UserAvatar name={row.name} size={40} />
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2 }}>{row.name}</Typography>
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 0.25 }}>
<BadgeOutlinedIcon sx={{ fontSize: 14, color: 'grey.400' }} />
<Typography variant="caption" color="text.secondary">{row.clientId}</Typography>
</Stack>
</Box>
<StatusChip status={row.status} sx={{ ml: 0.5 }} />
</Stack>
<Button size="small" variant="contained" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(row)}>Edit Client</Button>
</Stack>
<Box sx={{ p: 2.5, bgcolor: 'grey.50' }}>
<Grid container spacing={2.5} alignItems="stretch">
<Grid item xs={12} md={4}>
<SectionCard icon={StorefrontOutlinedIcon} title="Business" accent="primary">
<Stack direction="row" spacing={1.5}>
<StatTile label="Parcel Volume" value={Number(row.parcelVolume).toLocaleString()} icon={Inventory2OutlinedIcon} color="primary" />
<StatTile label="Active Contracts" value={row.activeContracts} icon={HandshakeOutlinedIcon} color="primary" />
</Stack>
<Field label="Client ID">
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 600, color: 'grey.800' }}>{row.clientId}</Typography>
</Field>
<Field label="Business Type"><Pill label={row.businessType && titleCase(row.businessType)} color="primary" /></Field>
<Field label="Order Frequency"><Pill label={row.frequency && titleCase(row.frequency)} color="info" /></Field>
</SectionCard>
</Grid>
)}
{inner === 1 && <PricingTab />}
{inner === 2 && <EditTab tenant={row} />}
<Grid item xs={12} md={4}>
<SectionCard icon={LocalShippingOutlinedIcon} title="Logistics" accent="primary">
<Field label="Logistics Segment">
{row.logisticsSegment ? (
<Stack direction="row" spacing={0.75} flexWrap="wrap" useFlexGap>
{String(row.logisticsSegment).split(/[,/]/).map((seg) => seg.trim()).filter(Boolean).map((seg) => (
<Pill key={seg} label={titleCase(seg)} color="primary" />
))}
</Stack>
) : <Pill label={null} />}
</Field>
<ReadField label="Current Provider" value={row.provider} />
<Field label="Efficiency"><Pill label={row.efficiency && titleCase(row.efficiency)} color={efficiencyTone(row.efficiency)} /></Field>
<Field label="Transit Route">
{(() => {
const stops = [row.transitFrom, ...String(row.transitTo || '').split(',')]
.map((s) => s.trim()).filter(Boolean);
if (!stops.length) return <Pill label={null} />;
return (
<Stack direction="row" spacing={0.75} alignItems="center" flexWrap="wrap" useFlexGap>
{stops.map((stop, idx) => (
<Fragment key={`${stop}-${idx}`}>
{idx > 0 && <ArrowRightAltIcon sx={{ fontSize: 20, color: 'primary.main' }} />}
<Pill label={titleCase(stop)} color={idx === 0 ? 'default' : 'info'} />
</Fragment>
))}
</Stack>
);
})()}
</Field>
</SectionCard>
</Grid>
<Grid item xs={12} md={4}>
<SectionCard icon={PlaceOutlinedIcon} title="Location & Survey" accent="primary">
<Field label="City / State">
<Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.800' }}>{[row.city, row.businessState].filter(Boolean).join(', ') || '—'}</Typography>
</Field>
<ReadField label="Neighbourhood" value={row.neighbourhood} />
<Field label="Survey Address">
<Stack direction="row" spacing={1} sx={{ p: 1.25, borderRadius: 1.5, bgcolor: 'grey.50', border: 1, borderColor: 'divider' }}>
<PlaceOutlinedIcon sx={{ fontSize: 16, color: 'grey.400', mt: '2px' }} />
<Typography variant="body2" sx={{ color: 'grey.800', lineHeight: 1.5 }}>{row.surveyAddress || '—'}</Typography>
</Stack>
</Field>
<Field label="Coordinates">
{row.surveyLat && row.surveyLng ? (
<Stack direction="row" spacing={1.25} alignItems="center" flexWrap="wrap" useFlexGap>
<Tooltip title={`${row.surveyLat}, ${row.surveyLng}`} arrow placement="top">
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ cursor: 'default' }}>
<PlaceOutlinedIcon sx={{ fontSize: 16, color: 'grey.400' }} />
<Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.800', borderBottom: '1px dotted', borderColor: 'grey.300' }}>
{[row.neighbourhood, row.city].filter(Boolean).join(', ') || 'Pinned location'}
</Typography>
</Stack>
</Tooltip>
<Button
component="a"
href={`https://www.google.com/maps?q=${row.surveyLat},${row.surveyLng}`}
target="_blank" rel="noopener"
size="small" variant="outlined" startIcon={<MapOutlinedIcon sx={{ fontSize: 16 }} />}
sx={{
py: 0.25, px: 1.25, minWidth: 0, fontSize: '0.75rem', fontWeight: 600, borderRadius: 5,
color: 'primary.main', borderColor: 'primary.100', bgcolor: 'primary.lighter',
'&:hover': { borderColor: 'primary.main', bgcolor: 'primary.lighter' }
}}
>
View on map
</Button>
</Stack>
) : <Typography variant="body2" sx={{ color: 'grey.500' }}></Typography>}
</Field>
</SectionCard>
</Grid>
<Grid item xs={12}>
<SectionCard icon={InfoOutlinedIcon} title="Contact & Compliance" accent="primary">
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6} md={4}><ReadField label="Phone" value={row.phone} /></Grid>
<Grid item xs={12} sm={6} md={4}><Field label="Data Consent"><Pill label={row.dataConsent && titleCase(row.dataConsent)} color={consentTone(row.dataConsent)} /></Field></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Last Updated" value={row.lastUpdated} /></Grid>
{row.notes && (
<Grid item xs={12}>
<Box sx={{ p: 2, borderRadius: 1.5, bgcolor: 'warning.lighter', border: 1, borderColor: 'warning.light' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
<NotesOutlinedIcon sx={{ fontSize: 16, color: 'warning.dark' }} />
<Typography variant="caption" sx={{ color: 'warning.dark', textTransform: 'uppercase', letterSpacing: 0.4, fontSize: '0.68rem', fontWeight: 700 }}>Notes</Typography>
</Stack>
<Typography variant="body2" sx={{ color: 'grey.800' }}>{row.notes}</Typography>
</Box>
</Grid>
)}
</Grid>
</SectionCard>
</Grid>
</Grid>
</Box>
</Box>
</Collapse>
@@ -165,85 +401,189 @@ function TenantRow({ row, index }) {
}
export default function Tenants() {
const navigate = useNavigate();
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchParams] = useSearchParams();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [search, setSearch] = useState(searchParams.get('q') || '');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const tabKey = TABS[tab].key;
// Keep the search box in sync when navigated here with a ?q= query (e.g. from the top search bar).
useEffect(() => {
const q = searchParams.get('q');
if (q != null) { setSearch(q); setPage(0); }
}, [searchParams]);
const [dialog, setDialog] = useState({ open: false, mode: 'add', initial: null });
const [toDelete, setToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
const load = () => {
setLoading(true);
setError(null);
fetchPoints(COLLECTIONS.clients)
.then((points) => setClients(points.map(toClient)))
.catch((e) => setError(e.message || 'Failed to load clients'))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
const stats = useMemo(() => ({
total: clients.length,
newCount: clients.filter((c) => c.status === 'newClient').length,
parcels: clients.reduce((s, c) => s + (Number(c.parcelVolume) || 0), 0),
contracts: clients.reduce((s, c) => s + (Number(c.activeContracts) || 0), 0)
}), [clients]);
const tabs = useMemo(() => {
const seen = [];
clients.forEach((c) => { if (!seen.includes(c.status)) seen.push(c.status); });
return [{ key: 'all', label: 'All' }, ...seen.map((s) => ({ key: s, label: titleCase(s) }))];
}, [clients]);
const tabKey = tabs[Math.min(tab, tabs.length - 1)]?.key || 'all';
const counts = useMemo(() => {
const c = {};
TABS.forEach((t) => { c[t.key] = tenants.filter((d) => d.status === t.key).length; });
const c = { all: clients.length };
clients.forEach((cl) => { c[cl.status] = (c[cl.status] || 0) + 1; });
return c;
}, []);
}, [clients]);
const filtered = useMemo(
() =>
tenants.filter((t) => {
const matchTab = t.status === tabKey;
clients.filter((t) => {
const matchTab = tabKey === 'all' || t.status === tabKey;
const matchSearch =
!search ||
[t.name, t.contact, t.email, t.phone, t.city].join(' ').toLowerCase().includes(search.toLowerCase());
[t.name, t.phone, t.city, t.businessType, t.clientId, t.neighbourhood]
.join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchSearch;
}),
[tabKey, search]
[clients, tabKey, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
const handleSaved = () => { setDialog({ open: false, mode: 'add', initial: null }); load(); };
const confirmDelete = async () => {
setDeleting(true);
try {
await deletePoint(COLLECTIONS.clients, toDelete.id);
setToDelete(null);
load();
} catch (e) {
setError(e.message || 'Failed to delete client');
} finally {
setDeleting(false);
}
};
return (
<>
<PageHeader
title="Tenants"
breadcrumbs={[{ label: 'Tenants' }]}
title="Clients"
breadcrumbs={[{ label: 'Clients' }]}
action={
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/tenants/create')}>
Create Client
</Button>
<Stack direction="row" spacing={1.5}>
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={load} disabled={loading}>Refresh</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialog({ open: true, mode: 'add', initial: null })}>Add Client</Button>
</Stack>
}
/>
<Card>
<Grid container spacing={2.5} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Clients" value={stats.total} icon={ApartmentOutlinedIcon} caption="All registered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="New Clients" value={stats.newCount} icon={FiberNewOutlinedIcon} color="primary" caption="Awaiting onboarding" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Total Parcel Volume" value={stats.parcels.toLocaleString()} icon={Inventory2OutlinedIcon} color="primary" caption="Across all clients" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard accent title="Active Contracts" value={stats.contracts} icon={HandshakeOutlinedIcon} color="primary" caption="Currently running" /></Grid>
</Grid>
<Card sx={{ overflow: 'hidden' }}>
<Box
sx={{
px: 2.5, py: 2, borderBottom: 1, borderColor: 'divider',
display: 'flex', alignItems: 'center', gap: 1.5,
background: (theme) => `linear-gradient(90deg, ${theme.palette.primary.lighter}66 0%, ${theme.palette.background.paper} 70%)`
}}
>
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: 'primary.lighter', color: 'primary.main', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
<ApartmentOutlinedIcon fontSize="small" />
</Box>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.2 }}>Client Directory</Typography>
<Typography variant="caption" color="text.secondary">Browse, search and manage every client account</Typography>
</Box>
</Box>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search clients…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 260 }}
size="small" placeholder="Search by name, phone, city, ID…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 300 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
{!loading && (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" color="text.secondary">
{filtered.length} {filtered.length === 1 ? 'client' : 'clients'}
</Typography>
<Chip size="small" label="live · doormile_clients" sx={{ height: 22, fontSize: '0.7rem', bgcolor: 'success.lighter', color: 'success.dark', fontWeight: 600 }} />
</Stack>
)}
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
<Tabs value={Math.min(tab, tabs.length - 1)} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{tabs.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key] || 0} active={tab === i} />} />
))}
</Tabs>
</Box>
{error && <Alert severity="error" sx={{ m: 2 }} action={<Button color="inherit" size="small" onClick={load}>Retry</Button>}>{error}</Alert>}
<TableContainer>
<Table>
<Table sx={{ minWidth: 900 }}>
<TableHead>
<TableRow>
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', textTransform: 'uppercase', fontSize: '0.72rem', letterSpacing: 0.4 } }}>
<TableCell padding="checkbox" />
<TableCell>S.No</TableCell>
<TableCell>ID</TableCell>
<TableCell>Client</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Actions</TableCell>
<TableCell>Location</TableCell>
<TableCell align="center">Volume</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.length === 0 ? (
{loading ? (
<TableRow>
<TableCell colSpan={6} sx={{ border: 'none' }}>
<EmptyState title="No tenants found" caption="Try a different tab or search term." />
<TableCell colSpan={8} sx={{ border: 'none' }}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}><CircularProgress /></Box>
</TableCell>
</TableRow>
) : paged.length === 0 ? (
<TableRow>
<TableCell colSpan={8} sx={{ border: 'none' }}>
<EmptyState title="No clients found" caption="Try a different tab or search term, or add a client." />
</TableCell>
</TableRow>
) : (
paged.map((row, i) => <TenantRow key={row.id} row={row} index={page * rpp + i} />)
paged.map((row, i) => (
<ClientRow
key={row.id}
row={row}
index={page * rpp + i}
onEdit={(r) => setDialog({ open: true, mode: 'edit', initial: r })}
onDelete={(r) => setToDelete(r)}
/>
))
)}
</TableBody>
</Table>
@@ -253,6 +593,27 @@ export default function Tenants() {
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
<ClientFormDialog
open={dialog.open}
mode={dialog.mode}
initial={dialog.initial}
onClose={() => setDialog({ open: false, mode: 'add', initial: null })}
onSaved={handleSaved}
/>
<Dialog open={!!toDelete} onClose={deleting ? undefined : () => setToDelete(null)}>
<DialogTitle>Delete client?</DialogTitle>
<DialogContent>
<DialogContentText>
This will permanently remove <strong>{toDelete?.name}</strong> from the doormile_clients collection. This cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 3, py: 2 }}>
<Button onClick={() => setToDelete(null)} disabled={deleting}>Cancel</Button>
<Button color="error" variant="contained" onClick={confirmDelete} disabled={deleting} startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : null}>Delete</Button>
</DialogActions>
</Dialog>
</>
);
}

108
src/utils/qdrant.js Normal file
View File

@@ -0,0 +1,108 @@
// ==============================|| QDRANT DATA LAYER ||============================== //
// Real connection to the Doormile Qdrant cluster (read + write).
//
// Requests go through the Vite dev proxy at `/qdrant` (see vite.config.js), which
// injects the api-key server-side so it never ships in the browser bundle and CORS
// is avoided. For a production build, point VITE_QDRANT_BASE at your own proxy.
const BASE = import.meta.env.VITE_QDRANT_BASE || '/qdrant';
export const COLLECTIONS = {
clients: 'doormile_clients',
teamUsers: 'doormile_auth'
};
async function request(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options
});
if (!res.ok) {
let detail = res.statusText;
try {
const body = await res.json();
detail = body?.status?.error || body?.status || detail;
} catch { /* ignore non-json error bodies */ }
throw new Error(`Qdrant ${res.status}: ${detail}`);
}
return res.json();
}
/**
* Scroll every point of a collection (follows next_page_offset until exhausted).
* Returns an array of { id, payload } objects with the raw Qdrant payload.
*/
export async function fetchPoints(collection, { pageSize = 250, withVector = false } = {}) {
const all = [];
let offset = null;
for (let guard = 0; guard < 1000; guard += 1) {
const data = await request(`/collections/${collection}/points/scroll`, {
method: 'POST',
body: JSON.stringify({
limit: pageSize,
with_payload: true,
with_vector: withVector,
...(offset != null ? { offset } : {})
})
});
const points = data?.result?.points || [];
all.push(...points);
offset = data?.result?.next_page_offset ?? null;
if (offset == null || points.length === 0) break;
}
return all;
}
// Cache vector sizes per collection so we don't re-fetch the config on every write.
const _vectorSizeCache = {};
export async function getVectorSize(collection) {
if (_vectorSizeCache[collection] != null) return _vectorSizeCache[collection];
const data = await request(`/collections/${collection}`);
const vectors = data?.result?.config?.params?.vectors;
// Single unnamed vector → { size, distance }. Default to 1 if absent.
const size = typeof vectors?.size === 'number' ? vectors.size : 1;
_vectorSizeCache[collection] = size;
return size;
}
/**
* Update the payload of an existing point (merges the given keys; vectors untouched).
*/
export async function setPayload(collection, id, payload) {
return request(`/collections/${collection}/points/payload?wait=true`, {
method: 'POST',
body: JSON.stringify({ payload, points: [id] })
});
}
/**
* Create a brand-new point. The collection requires a vector of a fixed size, but
* this CRM doesn't do semantic search, so we store a zero-vector of the right length.
* Returns the generated point id.
*/
export async function createPoint(collection, payload) {
const size = await getVectorSize(collection);
const id = (crypto?.randomUUID && crypto.randomUUID()) || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const vector = new Array(size).fill(0);
await request(`/collections/${collection}/points?wait=true`, {
method: 'PUT',
body: JSON.stringify({ points: [{ id, vector, payload }] })
});
return id;
}
/**
* Delete a point by id.
*/
export async function deletePoint(collection, id) {
return request(`/collections/${collection}/points/delete?wait=true`, {
method: 'POST',
body: JSON.stringify({ points: [id] })
});
}

View File

@@ -12,6 +12,20 @@ export default defineConfig({
},
server: {
port: 3000,
open: true
open: true,
// Proxy Qdrant so the api-key stays server-side and the browser avoids CORS.
// Frontend calls /qdrant/... → forwarded to the Qdrant cluster with the key injected.
proxy: {
'/qdrant': {
target: 'http://66.116.207.225:6333',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/qdrant/, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('api-key', 'Package@321#');
});
}
}
}
}
});