first commit

This commit is contained in:
2026-06-05 17:28:05 +05:30
commit a162fa89e5
62 changed files with 8729 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Build output
dist/
dist-ssr/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment
.env
.env.local
.env.*.local
# Editor / OS
.DS_Store
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln

50
README.md Normal file
View File

@@ -0,0 +1,50 @@
# Doormile Console — Admin
A modern, corporate logistics console for **Doormile**, a last-mile / courier delivery operation. Built with **React 18 + Vite + Material-UI v5**, themed around the Doormile brand red `#C01227`.
## Quick start
```bash
npm install
npm run dev # http://localhost:3000
npm run build # production build → dist/
npm run preview # preview the production build
```
## What's inside
A clean, data-dense but breathable corporate shell — fixed **red top header** (search, notifications, messages, profile) + **collapsible red sidebar** (260px ↔ 78px icon rail) + light `#FAFAFB` content area — wrapping **27 screens**:
| Area | Screens |
| --- | --- |
| Overview | Dashboard |
| Orders | Orders list · Order Details (tracking + timeline) · Create Order · Create Multiple Orders · Assign Orders |
| Deliveries | Deliveries (expandable products, status tabs) |
| Network | Tenants · Create Client · Customers · Create Customer · Pricing · Riders · Create Rider · Edit Rider |
| Reports | Order Summary · Order Details · Riders Summary · Riders Logs (live map) |
| Finance | Invoices · Invoice Preview (printable A4) · Requests |
| Account | User Profile |
| Auth & states | Login · 404 · 500 · Under Construction · Coming Soon |
## Design system
- **Brand red** `#C01227` (`lighter #F8E0E3 → darker #7E0B17`), white-on-red header/sidebar.
- **Status palette** — success `#00A854`, warning `#FFBF00`, info `#00A2AE`, error `#F04134`.
- **Public Sans** type scale, 610px radii, soft `0 1px 4px` card shadows, and a signature **red glow** on primary CTAs.
- All tuning lives in `src/theme/` (`palette.js`, `typography.js`, `shadows.js`, `componentsOverride.js`).
## Project structure
```
src/
theme/ Doormile red theme + MUI component overrides
layout/ MainLayout (red header + collapsible sidebar) · MinimalLayout
menu/ sidebar nav config
components/ PageHeader, StatCard, StatusChip, MainCard, EmptyState,
UserAvatar, MapPlaceholder, Logo, charts/ (Area, Donut)
data/mock.js static demo data powering every screen
pages/ all 27 screens (lazy-loaded)
App.jsx router · main.jsx app entry
```
Charts and maps are dependency-free SVG placeholders (`components/charts`, `MapPlaceholder`) — swap in Recharts / Leaflet / Google Maps when wiring real data. All data is mocked in `src/data/mock.js`; there is no backend.

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/preloader.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#C01227" />
<meta name="description" content="Doormile — corporate logistics & last-mile delivery admin console" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
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>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2667
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "doormile-console-admin",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Doormile — corporate logistics & last-mile delivery admin console",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.20",
"@mui/x-date-pickers": "^6.20.2",
"dayjs": "^1.11.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.3.1"
}
}

BIN
public/Doormile-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/preloader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

76
src/App.jsx Normal file
View File

@@ -0,0 +1,76 @@
import { Suspense, lazy } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Box, CircularProgress } from '@mui/material';
import MainLayout from '@/layout/MainLayout';
import MinimalLayout from '@/layout/MinimalLayout';
const load = (factory) => {
const C = lazy(factory);
return (
<Suspense
fallback={
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<CircularProgress color="primary" />
</Box>
}
>
<C />
</Suspense>
);
};
export default function App() {
return (
<Routes>
{/* Shell pages */}
<Route element={<MainLayout />}>
<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="/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>
{/* Full-bleed pages */}
<Route element={<MinimalLayout />}>
<Route path="/login" element={load(() => import('@/pages/auth/Login'))} />
<Route path="/under-construction" element={load(() => import('@/pages/maintenance/UnderConstruction'))} />
<Route path="/coming-soon" element={load(() => import('@/pages/maintenance/ComingSoon'))} />
<Route path="/500" element={load(() => import('@/pages/maintenance/Error500'))} />
<Route path="/404" element={load(() => import('@/pages/maintenance/Error404'))} />
</Route>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={load(() => import('@/pages/maintenance/Error404'))} />
</Routes>
);
}

View File

@@ -0,0 +1,33 @@
import { Box, Typography } from '@mui/material';
import InboxOutlinedIcon from '@mui/icons-material/InboxOutlined';
// ==============================|| EMPTY STATE ||============================== //
export default function EmptyState({ icon: Icon = InboxOutlinedIcon, title = 'No records found', caption, sx }) {
return (
<Box sx={{ textAlign: 'center', py: 6, px: 2, ...sx }}>
<Box
sx={{
width: 72,
height: 72,
borderRadius: '50%',
bgcolor: 'grey.100',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
mb: 2
}}
>
<Icon sx={{ fontSize: 34, color: 'grey.400' }} />
</Box>
<Typography variant="h5" color="text.secondary">
{title}
</Typography>
{caption && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{caption}
</Typography>
)}
</Box>
);
}

51
src/components/Logo.jsx Normal file
View File

@@ -0,0 +1,51 @@
import { Box, Typography } from '@mui/material';
// ==============================|| DOORMILE WORDMARK LOGO ||============================== //
// Uses the brand wordmark asset (white PNG). `onDark` shows it as-is on dark/red
// surfaces; on light surfaces it is recoloured to near-black. `compact` (e.g. the
// collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit.
const LOGO_SRC = '/Doormile-logo.png';
export default function Logo({ onDark = false, compact = false, height = 26, sx }) {
if (compact) {
const mark = onDark ? '#FFFFFF' : '#C01227';
const markText = onDark ? '#C01227' : '#FFFFFF';
return (
<Box sx={{ display: 'flex', alignItems: 'center', ...sx }}>
<Box
sx={{
width: 34,
height: 34,
borderRadius: 2,
bgcolor: mark,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
boxShadow: onDark ? 'none' : '0 4px 10px rgba(192, 18, 39,0.30)'
}}
>
<Typography sx={{ color: markText, fontWeight: 800, fontSize: '1.25rem', lineHeight: 1 }}>D</Typography>
</Box>
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center', ...sx }}>
<Box
component="img"
src={LOGO_SRC}
alt="Doormile"
sx={{
height,
width: 'auto',
display: 'block',
// The asset is white; on light surfaces recolour it to near-black so it stays visible.
filter: onDark ? 'none' : 'brightness(0) saturate(100%)'
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,42 @@
import { Box, Typography, Breadcrumbs, Link, Stack } from '@mui/material';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Link as RouterLink } from 'react-router-dom';
// ==============================|| PAGE HEADER (title + breadcrumb + actions) ||============================== //
export default function PageHeader({ title, breadcrumbs = [], action }) {
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1.5}
sx={{ mb: 3 }}
>
<Box>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>
{title}
</Typography>
{breadcrumbs.length > 0 && (
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />} sx={{ mt: 0.5 }}>
<Link component={RouterLink} to="/dashboard" underline="hover" color="text.secondary" variant="caption">
Home
</Link>
{breadcrumbs.map((b, i) =>
b.to && i < breadcrumbs.length - 1 ? (
<Link key={i} component={RouterLink} to={b.to} underline="hover" color="text.secondary" variant="caption">
{b.label}
</Link>
) : (
<Typography key={i} variant="caption" color="primary.main" sx={{ fontWeight: 600 }}>
{b.label}
</Typography>
)
)}
</Breadcrumbs>
)}
</Box>
{action && <Box>{action}</Box>}
</Stack>
);
}

View File

@@ -0,0 +1,55 @@
import { Card, CardContent, Box, Typography, Avatar, Stack } from '@mui/material';
import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded';
// ==============================|| STAT / KPI CARD ||============================== //
export default function StatCard({ title, value, icon: Icon, color = 'primary', trend, caption }) {
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>
{Icon && (
<Avatar
variant="rounded"
sx={{ bgcolor: `${color}.lighter`, color: `${color}.main`, width: 44, height: 44 }}
>
<Icon fontSize="small" />
</Avatar>
)}
</Stack>
{(trendUp !== null || caption) && (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1.5 }}>
{trendUp !== null && (
<>
{trendUp ? (
<ArrowUpwardRoundedIcon sx={{ fontSize: 16, color: 'success.main' }} />
) : (
<ArrowDownwardRoundedIcon sx={{ fontSize: 16, color: 'error.main' }} />
)}
<Typography variant="caption" sx={{ fontWeight: 600, color: trendUp ? 'success.main' : 'error.main' }}>
{Math.abs(trend)}%
</Typography>
</>
)}
{caption && (
<Typography variant="caption" color="text.secondary">
{caption}
</Typography>
)}
</Stack>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,54 @@
import { Chip } from '@mui/material';
// ==============================|| STATUS CHIP ||============================== //
// Soft-filled status chips used across orders, deliveries, riders, invoices.
const MAP = {
// orders / deliveries
pending: { color: 'warning', label: 'Pending' },
created: { color: 'info', label: 'Created' },
assigned: { color: 'info', label: 'Assigned' },
accepted: { color: 'info', label: 'Accepted' },
arrived: { color: 'info', label: 'Arrived' },
picked: { color: 'primary', label: 'Picked' },
'in-transit': { color: 'info', label: 'In Transit' },
active: { color: 'primary', label: 'Active' },
delivered: { color: 'success', label: 'Delivered' },
completed: { color: 'success', label: 'Completed' },
skipped: { color: 'warning', label: 'Skipped' },
failed: { color: 'error', label: 'Failed' },
cancelled: { color: 'error', label: 'Cancelled' },
// riders / tenants
online: { color: 'success', label: 'Online' },
offline: { color: 'default', label: 'Offline' },
'on-delivery': { color: 'info', label: 'On Delivery' },
inactive: { color: 'default', label: 'Inactive' },
// invoices
paid: { color: 'success', label: 'Paid' },
open: { color: 'info', label: 'Open' },
overdue: { color: 'error', label: 'Overdue' },
prepaid: { color: 'success', label: 'Prepaid' },
cod: { color: 'warning', label: 'COD' }
};
const TONE = {
success: { bg: '#E3F6EC', fg: '#00773B' },
warning: { bg: '#FFF7E0', fg: '#8A6500' },
info: { bg: '#E0F7F8', fg: '#00727B' },
error: { bg: '#FEEAE9', fg: '#A82216' },
primary: { bg: '#F8E0E3', fg: '#9E0E20' },
default: { bg: '#F0F0F0', fg: '#595959' }
};
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 tone = TONE[cfg.color] || TONE.default;
return (
<Chip
size={size}
label={label || cfg.label}
sx={{ bgcolor: tone.bg, color: tone.fg, border: 'none', ...sx }}
/>
);
}

View File

@@ -0,0 +1,30 @@
import { Stack, Box } from '@mui/material';
// ==============================|| TAB LABEL WITH INLINE COUNT PILL ||============================== //
// Renders a tab label with the count laid out inline (not an overlapping Badge),
// so adjacent tabs never clip the number. The pill highlights when its tab is active.
export default function TabLabelCount({ label, count, active = false }) {
return (
<Stack direction="row" alignItems="center" spacing={1}>
<span>{label}</span>
<Box
component="span"
sx={{
minWidth: 20,
height: 20,
px: 0.75,
borderRadius: 10,
fontSize: '0.72rem',
fontWeight: 700,
lineHeight: '20px',
textAlign: 'center',
bgcolor: active ? 'primary.main' : 'grey.200',
color: active ? '#fff' : 'text.secondary'
}}
>
{count}
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,12 @@
import { Avatar } from '@mui/material';
import { stringToColor, initials } from '@/utils/format';
// ==============================|| INITIALS AVATAR ||============================== //
export default function UserAvatar({ name = '', size = 32, sx }) {
return (
<Avatar sx={{ width: size, height: size, bgcolor: stringToColor(name), fontSize: size * 0.42, ...sx }}>
{initials(name)}
</Avatar>
);
}

View File

@@ -0,0 +1,66 @@
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

@@ -0,0 +1,59 @@
import { Box, Stack, Typography } from '@mui/material';
// Dependency-free donut chart. data: [{ label, value, color }]
export default function DonutChart({ data = [], size = 180, thickness = 26, centerLabel, centerValue }) {
const total = data.reduce((s, d) => s + d.value, 0) || 1;
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
let offset = 0;
return (
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
alignItems="center"
justifyContent="center"
sx={{ flexWrap: 'wrap' }}
>
<Box sx={{ position: 'relative', width: size, maxWidth: '100%', aspectRatio: '1 / 1' }}>
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`}>
<g transform={`rotate(-90 ${size / 2} ${size / 2})`}>
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="#F0F0F0" strokeWidth={thickness} />
{data.map((d, i) => {
const len = (d.value / total) * c;
const seg = (
<circle
key={i}
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke={d.color}
strokeWidth={thickness}
strokeDasharray={`${len} ${c - len}`}
strokeDashoffset={-offset}
strokeLinecap="butt"
/>
);
offset += len;
return seg;
})}
</g>
</svg>
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="h3" sx={{ fontWeight: 700 }}>{centerValue ?? total}</Typography>
<Typography variant="caption" color="text.secondary">{centerLabel ?? 'Total'}</Typography>
</Box>
</Box>
<Stack spacing={1.25}>
{data.map((d) => (
<Stack key={d.label} direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: d.color }} />
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 80 }}>{d.label}</Typography>
<Typography variant="subtitle2">{d.value.toLocaleString('en-IN')}</Typography>
</Stack>
))}
</Stack>
</Stack>
);
}

181
src/data/mock.js Normal file
View File

@@ -0,0 +1,181 @@
// ==============================|| 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

@@ -0,0 +1,261 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
Toolbar,
IconButton,
Box,
InputBase,
Badge,
Avatar,
Typography,
Menu,
MenuItem,
Divider,
ListItemIcon,
ListItemText,
Tooltip,
Button,
Stack,
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';
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;
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 submitSearch = (e) => {
e.preventDefault();
const q = search.trim();
if (q) navigate(`/orders?q=${encodeURIComponent(q)}`);
};
return (
<AppBar
position="fixed"
elevation={0}
sx={{ bgcolor: RED, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
>
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}>
<MenuIcon />
</IconButton>
{/* Brand wordmark — left side */}
<Box
onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
>
<Logo onDark height={22} />
</Box>
<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>
<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>
<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>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}>
Aman Deshmukh
</Typography>
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}>
Operations Admin
</Typography>
</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}
open={Boolean(account)}
onClose={() => setAccount(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, minWidth: 200 } }}
>
<MenuItem onClick={() => { setAccount(null); navigate('/profile'); }}>
<ListItemIcon><PersonOutlineIcon fontSize="small" /></ListItemIcon>
View Profile
</MenuItem>
<MenuItem onClick={() => { setAccount(null); navigate('/settings'); }}>
<ListItemIcon><SettingsOutlinedIcon fontSize="small" /></ListItemIcon>
Settings
</MenuItem>
<Divider />
<MenuItem onClick={() => { setAccount(null); navigate('/login'); }} sx={{ color: 'error.main' }}>
<ListItemIcon><LogoutIcon fontSize="small" color="error" /></ListItemIcon>
Logout
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
);
}

View File

@@ -0,0 +1,200 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Drawer,
Box,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
Collapse,
Tooltip,
Toolbar
} from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import navItems from '@/menu/navItems';
import Logo from '@/components/Logo';
export const DRAWER_WIDTH = 264;
export const MINI_WIDTH = 78;
const RED = '#C01227';
function NavLeaf({ item, open, active, depth = 0, onClick }) {
const Icon = item.icon;
const button = (
<ListItemButton
selected={active}
onClick={onClick}
sx={{
minHeight: 44,
my: 0.25,
mx: open ? 1 : 0.75,
px: open ? 1.5 : 0,
justifyContent: open ? 'flex-start' : 'center',
borderRadius: 2,
color: 'rgba(255,255,255,0.78)',
'& .MuiListItemIcon-root': { color: 'inherit' },
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' },
'&.Mui-selected': {
bgcolor: 'rgba(255,255,255,0.18)',
color: '#fff',
'&:hover': { bgcolor: 'rgba(255,255,255,0.22)' }
}
}}
>
<ListItemIcon sx={{ minWidth: open ? 34 : 'auto', justifyContent: 'center' }}>
{depth > 0 && !Icon ? <FiberManualRecordIcon sx={{ fontSize: 8 }} /> : Icon ? <Icon fontSize="small" /> : null}
</ListItemIcon>
{open && (
<ListItemText
primary={item.title}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: active ? 700 : 500 }}
/>
)}
</ListItemButton>
);
return open ? button : <Tooltip title={item.title} placement="right">{button}</Tooltip>;
}
export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
const location = useLocation();
const navigate = useNavigate();
const isActive = (url) => url && location.pathname.startsWith(url);
const expanded = open || isMobile;
const initialOpen = navItems
.flatMap((g) => g.items)
.filter((i) => i.children && i.children.some((c) => isActive(c.url)))
.map((i) => i.id);
const [collapse, setCollapse] = useState(initialOpen);
const go = (url) => {
navigate(url);
if (isMobile) onMobileClose();
};
const content = (
<Box sx={{ bgcolor: RED, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}>
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}>
<Logo onDark compact={!expanded} />
</Toolbar>
<Box sx={{ overflowY: 'auto', overflowX: 'hidden', flexGrow: 1, pb: 2 }}>
{navItems.map((grp) => (
<Box key={grp.group} sx={{ mt: 1 }}>
{expanded && (
<Typography
variant="overline"
sx={{ px: 2.5, color: 'rgba(255,255,255,0.55)', fontSize: '0.6875rem', letterSpacing: '0.08em' }}
>
{grp.group}
</Typography>
)}
<List disablePadding sx={{ mt: 0.5 }}>
{grp.items.map((item) => {
if (item.children) {
const opened = collapse.includes(item.id);
const childActive = item.children.some((c) => isActive(c.url));
const Icon = item.icon;
const head = (
<ListItemButton
onClick={() =>
expanded
? setCollapse((p) => (p.includes(item.id) ? p.filter((x) => x !== item.id) : [...p, item.id]))
: go(item.children[0].url)
}
sx={{
minHeight: 44,
my: 0.25,
mx: expanded ? 1 : 0.75,
px: expanded ? 1.5 : 0,
justifyContent: expanded ? 'flex-start' : 'center',
borderRadius: 2,
color: childActive ? '#fff' : 'rgba(255,255,255,0.78)',
bgcolor: childActive && !opened ? 'rgba(255,255,255,0.12)' : 'transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' }
}}
>
<ListItemIcon sx={{ minWidth: expanded ? 34 : 'auto', justifyContent: 'center', color: 'inherit' }}>
<Icon fontSize="small" />
</ListItemIcon>
{expanded && (
<>
<ListItemText primary={item.title} primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 500 }} />
{opened ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
</>
)}
</ListItemButton>
);
return (
<Box key={item.id}>
{expanded ? head : <Tooltip title={item.title} placement="right">{head}</Tooltip>}
{expanded && (
<Collapse in={opened} timeout="auto" unmountOnExit>
<Box sx={{ pl: 1.5 }}>
{item.children.map((c) => (
<NavLeaf key={c.id} item={c} open depth={1} active={isActive(c.url)} onClick={() => go(c.url)} />
))}
</Box>
</Collapse>
)}
</Box>
);
}
return (
<NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
);
})}
</List>
</Box>
))}
</Box>
{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
</Typography>
</Box>
)}
</Box>
);
if (isMobile) {
return (
<Drawer
variant="temporary"
open={mobileOpen}
onClose={onMobileClose}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: DRAWER_WIDTH, border: 'none' } }}
>
{content}
</Drawer>
);
}
return (
<Drawer
variant="permanent"
sx={{
width: open ? DRAWER_WIDTH : MINI_WIDTH,
flexShrink: 0,
whiteSpace: 'nowrap',
'& .MuiDrawer-paper': {
width: open ? DRAWER_WIDTH : MINI_WIDTH,
border: 'none',
overflowX: 'hidden',
transition: (t) => t.transitions.create('width', { duration: t.transitions.duration.standard })
}
}}
open={open}
>
{content}
</Drawer>
);
}

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Box, Toolbar, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Header from './Header';
import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar';
export default function MainLayout() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('lg'));
const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
const toggle = () => {
if (isMobile) setMobileOpen((p) => !p);
else setOpen((p) => !p);
};
return (
<Box sx={{ display: 'flex', bgcolor: 'background.default', minHeight: '100vh' }}>
<Header onToggle={toggle} />
<Sidebar
open={open}
isMobile={isMobile}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
/>
<Box
component="main"
sx={{
flexGrow: 1,
width: { lg: `calc(100% - ${open ? DRAWER_WIDTH : MINI_WIDTH}px)` },
minHeight: '100vh',
transition: theme.transitions.create('width', { duration: theme.transitions.duration.standard })
}}
>
<Toolbar sx={{ minHeight: 64 }} />
<Box sx={{ p: { xs: 2, sm: 3 } }}>
<Outlet />
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import { Box } from '@mui/material';
// Used by auth + maintenance pages — full-bleed, no shell.
export default function MinimalLayout() {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
<Outlet />
</Box>
);
}

22
src/main.jsx Normal file
View File

@@ -0,0 +1,22 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider, CssBaseline } from '@mui/material';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import theme from '@/theme';
import App from '@/App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs}>
<BrowserRouter>
<App />
</BrowserRouter>
</LocalizationProvider>
</ThemeProvider>
</React.StrictMode>
);

55
src/menu/navItems.jsx Normal file
View File

@@ -0,0 +1,55 @@
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';
// ==============================|| DOORMILE - SIDEBAR NAV CONFIG ||============================== //
const navItems = [
{
group: 'Operations',
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 }
]
},
{
group: 'Network',
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 }
]
}
];
export default navItems;

124
src/pages/Dashboard.jsx Normal file
View File

@@ -0,0 +1,124 @@
import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
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';
export default function Dashboard() {
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>
}
/>
<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>
<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>
</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>
<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>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="subtitle2">{r.deliveries}</Typography>
<Typography variant="caption" color="text.secondary">deliveries</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
</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>
);
}

279
src/pages/Deliveries.jsx Normal file
View File

@@ -0,0 +1,279 @@
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>
</>
);
}

93
src/pages/Pricing.jsx Normal file
View File

@@ -0,0 +1,93 @@
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>
</>
);
}

69
src/pages/Profile.jsx Normal file
View File

@@ -0,0 +1,69 @@
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>
</>
);
}

162
src/pages/Requests.jsx Normal file
View File

@@ -0,0 +1,162 @@
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>
</>
);
}

206
src/pages/Settings.jsx Normal file
View File

@@ -0,0 +1,206 @@
import { useState } from 'react';
import {
Grid,
Tabs,
Tab,
Box,
Stack,
TextField,
MenuItem,
Switch,
FormControlLabel,
Button,
Typography,
Divider,
Snackbar,
Alert
} 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 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>;
}
export default function Settings() {
const [tab, setTab] = useState(0);
const [toast, setToast] = useState(false);
// General
const [general, setGeneral] = useState({
orgName: 'Doormile Logistics Pvt. Ltd.',
supportEmail: 'support@doormile.in',
contact: '+91 98450 11223',
timezone: TIMEZONES[0],
language: LANGUAGES[0]
});
// Notifications
const [notify, setNotify] = useState({
newOrders: true,
riderStatus: true,
invoicePaid: true,
weeklyDigest: false,
emailAlerts: true,
smsAlerts: false
});
// Security
const [security, setSecurity] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactor: false
});
const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked }));
const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked }));
const save = () => setToast(true);
return (
<>
<PageHeader
title="Settings"
breadcrumbs={[{ label: 'Settings' }]}
action={
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>
Save Changes
</Button>
}
/>
<Grid container spacing={2.5}>
<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>
</Grid>
<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}>
<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>
</Box>
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} />
</Stack>
</MainCard>
</Stack>
</TabPanel>
</Grid>
</Grid>
<Snackbar
open={toast}
autoHideDuration={2500}
onClose={() => setToast(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}>
Settings saved successfully.
</Alert>
</Snackbar>
</>
);
}

124
src/pages/auth/Login.jsx Normal file
View File

@@ -0,0 +1,124 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Grid,
Card,
Stack,
Typography,
TextField,
InputAdornment,
IconButton,
Button,
Checkbox,
FormControlLabel,
Link
} 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';
export default function Login() {
const navigate = useNavigate();
const [show, setShow] = useState(false);
const [auth, setAuth] = useState('admin@doormile.in');
const [pwd, setPwd] = useState('');
return (
<Grid container sx={{ minHeight: '100vh' }}>
{/* Brand panel */}
<Grid
item
md={6}
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'
}}
>
<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.
</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>
</Card>
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,95 @@
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

@@ -0,0 +1,245 @@
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

@@ -0,0 +1,180 @@
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

@@ -0,0 +1,154 @@
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

@@ -0,0 +1,80 @@
import { useState } from 'react';
import { Box, Stack, Typography, TextField, Button } from '@mui/material';
import Logo from '@/components/Logo';
const COUNTDOWN = [
{ label: 'Days', value: 12 },
{ label: 'Hours', value: 8 },
{ label: 'Minutes', value: 45 },
{ label: 'Seconds', value: 30 }
];
export default function ComingSoon() {
const [email, setEmail] = useState('');
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
textAlign: 'center'
}}
>
<Stack spacing={3} alignItems="center" sx={{ maxWidth: 620, width: '100%' }}>
<Logo />
<Typography variant="h2" sx={{ fontWeight: 800, color: 'grey.800' }}>Coming Soon</Typography>
<Typography variant="body1" color="text.secondary">Something new is on its way</Typography>
{/* Countdown */}
<Stack direction="row" spacing={{ xs: 1, sm: 2 }} alignItems="center" justifyContent="center">
{COUNTDOWN.map((c, i) => (
<Stack key={c.label} direction="row" spacing={{ xs: 1, sm: 2 }} alignItems="center">
<Box
sx={{
width: { xs: 64, sm: 84 },
py: 2,
borderRadius: 2,
bgcolor: 'primary.lighter',
border: 1,
borderColor: 'primary.light'
}}
>
<Typography variant="h2" sx={{ fontWeight: 800, color: 'primary.main', lineHeight: 1 }}>
{String(c.value).padStart(2, '0')}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 1 }}>
{c.label}
</Typography>
</Box>
{i < COUNTDOWN.length - 1 && (
<Typography variant="h2" sx={{ fontWeight: 800, color: 'primary.main' }}>:</Typography>
)}
</Stack>
))}
</Stack>
{/* Subscribe */}
<Box sx={{ width: '100%', maxWidth: 480 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
Be the first to be notified when Doormile launches.
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<TextField
fullWidth
size="small"
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button variant="contained" sx={{ whiteSpace: 'nowrap', px: 3 }}>Notify Me</Button>
</Stack>
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,34 @@
import { useNavigate } from 'react-router-dom';
import { Box, Stack, Typography, Button } from '@mui/material';
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
export default function Error404() {
const navigate = useNavigate();
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
textAlign: 'center'
}}
>
<Stack spacing={2} alignItems="center">
<Typography sx={{ fontWeight: 900, fontSize: { xs: '6rem', md: '9rem' }, lineHeight: 1, color: 'primary.main' }}>
404
</Typography>
<Box sx={{ width: 64, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Page Not Found</Typography>
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
The page you are looking for was moved, removed, renamed, or might never exist!
</Typography>
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
Back To Home
</Button>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,34 @@
import { useNavigate } from 'react-router-dom';
import { Box, Stack, Typography, Button } from '@mui/material';
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
export default function Error500() {
const navigate = useNavigate();
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
textAlign: 'center'
}}
>
<Stack spacing={2} alignItems="center">
<Typography sx={{ fontWeight: 900, fontSize: { xs: '6rem', md: '9rem' }, lineHeight: 1, color: 'primary.main' }}>
500
</Typography>
<Box sx={{ width: 64, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Internal Server Error</Typography>
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
Server error 500. We are fixing the problem. Please try again at a later stage.
</Typography>
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
Back To Home
</Button>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,45 @@
import { useNavigate } from 'react-router-dom';
import { Box, Stack, Typography, Button } from '@mui/material';
import ConstructionOutlinedIcon from '@mui/icons-material/ConstructionOutlined';
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
export default function UnderConstruction() {
const navigate = useNavigate();
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
textAlign: 'center'
}}
>
<Stack spacing={2.5} alignItems="center">
<Box
sx={{
width: 110,
height: 110,
borderRadius: '50%',
bgcolor: 'primary.lighter',
color: 'primary.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ConstructionOutlinedIcon sx={{ fontSize: 56 }} />
</Box>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Under Construction</Typography>
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
Hey! Please check out this site later. We are doing some maintenance on it right now.
</Typography>
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
Back To Home
</Button>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,135 @@
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

@@ -0,0 +1,254 @@
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

@@ -0,0 +1,142 @@
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

@@ -0,0 +1,170 @@
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

@@ -0,0 +1,195 @@
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

@@ -0,0 +1,190 @@
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

@@ -0,0 +1,195 @@
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

@@ -0,0 +1,105 @@
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

@@ -0,0 +1,167 @@
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

@@ -0,0 +1,71 @@
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

@@ -0,0 +1,180 @@
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>
</>
);
}

221
src/pages/riders/Riders.jsx Normal file
View File

@@ -0,0 +1,221 @@
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,81 @@
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

@@ -0,0 +1,258 @@
import { useState, useMemo, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Typography, Collapse, Grid
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
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 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';
const TABS = [
{ key: 'active', label: 'Active' },
{ key: 'pending', label: 'Pending' },
{ key: 'inactive', label: 'Inactive' }
];
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>
);
}
function PricingTab() {
return (
<Box>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
<Button variant="contained" startIcon={<AddIcon />}>Add Pricing</Button>
</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>
</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 }));
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>
</Stack>
</Box>
);
}
function TenantRow({ row, index }) {
const [open, setOpen] = useState(false);
const [inner, setInner] = useState(0);
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>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={row.name} size={34} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
<Typography variant="caption" color="text.secondary">{row.volume} orders / mo</Typography>
</Box>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2">{row.contact}</Typography>
<Typography variant="caption" color="text.secondary">{row.phone}</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">{row.address}</Typography>
<Typography variant="caption" color="text.secondary">{row.city} · {row.postcode}</Typography>
</TableCell>
<TableCell><StatusChip status={row.status} /></TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={6} 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>
</Grid>
)}
{inner === 1 && <PricingTab />}
{inner === 2 && <EditTab tenant={row} />}
</Box>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
);
}
export default function Tenants() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const tabKey = TABS[tab].key;
const counts = useMemo(() => {
const c = {};
TABS.forEach((t) => { c[t.key] = tenants.filter((d) => d.status === t.key).length; });
return c;
}, []);
const filtered = useMemo(
() =>
tenants.filter((t) => {
const matchTab = t.status === tabKey;
const matchSearch =
!search ||
[t.name, t.contact, t.email, t.phone, t.city].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchSearch;
}),
[tabKey, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Tenants"
breadcrumbs={[{ label: 'Tenants' }]}
action={
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/tenants/create')}>
Create Client
</Button>
}
/>
<Card>
<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 }}
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={counts[t.key]} active={tab === i} />} />
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox" />
<TableCell>S.No</TableCell>
<TableCell>Client</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.length === 0 ? (
<TableRow>
<TableCell colSpan={6} sx={{ border: 'none' }}>
<EmptyState title="No tenants found" caption="Try a different tab or search term." />
</TableCell>
</TableRow>
) : (
paged.map((row, i) => <TenantRow 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

@@ -0,0 +1,121 @@
import customShadows from './shadows';
// ==============================|| DOORMILE THEME - COMPONENT OVERRIDES ||============================== //
// Clean, corporate Material Design tuning for the whole console.
export default function componentsOverride(theme) {
const { palette } = theme;
return {
MuiCssBaseline: {
styleOverrides: {
body: { backgroundColor: palette.background.default },
'*::-webkit-scrollbar': { width: 8, height: 8 },
'*::-webkit-scrollbar-thumb': { background: palette.grey[300], borderRadius: 8 },
'*::-webkit-scrollbar-thumb:hover': { background: palette.grey[400] }
}
},
MuiButton: {
defaultProps: { disableElevation: true },
styleOverrides: {
root: { borderRadius: 6, fontWeight: 600, padding: '7px 18px' },
containedPrimary: {
boxShadow: customShadows.primaryGlow,
'&:hover': { boxShadow: customShadows.primaryGlowHover, backgroundColor: palette.primary.dark }
},
outlined: { borderColor: palette.grey[300] },
sizeLarge: { padding: '10px 22px', fontSize: '0.9375rem' }
}
},
MuiIconButton: {
styleOverrides: { root: { borderRadius: 8 } }
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 10,
border: `1px solid ${palette.grey[200]}`,
boxShadow: customShadows.card,
backgroundImage: 'none'
}
}
},
MuiCardHeader: {
defaultProps: { titleTypographyProps: { variant: 'h5' }, subheaderTypographyProps: { variant: 'caption' } },
styleOverrides: { root: { padding: 20 } }
},
MuiCardContent: {
styleOverrides: { root: { padding: 20, '&:last-child': { paddingBottom: 20 } } }
},
MuiPaper: {
defaultProps: { elevation: 0 },
styleOverrides: { rounded: { borderRadius: 10 } }
},
MuiChip: {
styleOverrides: {
root: { borderRadius: 6, fontWeight: 600, fontSize: '0.75rem' },
sizeSmall: { height: 22 },
label: { paddingLeft: 8, paddingRight: 8 }
}
},
MuiTableCell: {
styleOverrides: {
root: { borderColor: palette.grey[200], padding: '12px 16px', fontSize: '0.8125rem' },
head: {
fontWeight: 600,
color: palette.grey[600],
backgroundColor: palette.grey[50],
textTransform: 'none',
whiteSpace: 'nowrap'
}
}
},
MuiTableRow: {
styleOverrides: {
root: { '&:hover': { backgroundColor: palette.primary.lighter + '66' } }
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
borderRadius: 8,
backgroundColor: palette.background.paper,
'& .MuiOutlinedInput-notchedOutline': { borderColor: palette.grey[300] },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: palette.grey[400] }
},
input: { padding: '11px 14px' }
}
},
MuiInputLabel: {
styleOverrides: { root: { color: palette.grey[600], fontSize: '0.875rem' } }
},
MuiTab: {
styleOverrides: {
root: { textTransform: 'none', fontWeight: 600, minHeight: 46, fontSize: '0.875rem' }
}
},
MuiTabs: {
styleOverrides: { indicator: { height: 3, borderRadius: 3 } }
},
MuiTooltip: {
styleOverrides: {
tooltip: { backgroundColor: palette.grey[800], borderRadius: 6, fontSize: '0.75rem', padding: '6px 10px' }
}
},
MuiDialog: {
styleOverrides: { paper: { borderRadius: 12 } }
},
MuiAvatar: {
styleOverrides: { root: { fontWeight: 600, fontSize: '0.875rem' } }
},
MuiListItemButton: {
styleOverrides: { root: { borderRadius: 8 } }
},
MuiLinearProgress: {
styleOverrides: { root: { borderRadius: 8, height: 6, backgroundColor: palette.grey[200] } }
},
MuiMenu: {
styleOverrides: { paper: { borderRadius: 10, boxShadow: customShadows.dropdown, marginTop: 4 } }
}
};
}

20
src/theme/index.js Normal file
View File

@@ -0,0 +1,20 @@
import { createTheme } from '@mui/material/styles';
import palette from './palette';
import typography from './typography';
import customShadows from './shadows';
import componentsOverride from './componentsOverride';
// ==============================|| DOORMILE THEME - ENTRY ||============================== //
let theme = createTheme({
palette,
typography,
shape: { borderRadius: 6 },
customShadows,
mixins: { toolbar: { minHeight: 64 } }
});
theme.components = componentsOverride(theme);
export default theme;

102
src/theme/palette.js Normal file
View File

@@ -0,0 +1,102 @@
// ==============================|| DOORMILE THEME - PALETTE ||============================== //
// Corporate red brand palette. Brand red #C01227.
export const grey = {
0: '#FFFFFF',
50: '#FAFAFA',
100: '#F5F5F5',
200: '#F0F0F0',
300: '#D9D9D9',
400: '#BFBFBF',
500: '#8C8C8C',
600: '#595959',
700: '#434343',
800: '#262626',
900: '#141414',
A50: '#FAFAFB',
A100: '#E6EBF1'
};
const palette = {
mode: 'light',
common: { black: '#000000', white: '#FFFFFF' },
primary: {
lighter: '#F8E0E3',
100: '#EFBBC1',
200: '#E08A92',
light: '#D6515C',
400: '#CC2E3C',
main: '#C01227',
dark: '#9E0E20',
700: '#870C1B',
darker: '#7E0B17',
900: '#520710',
contrastText: '#FFFFFF'
},
secondary: {
lighter: grey[100],
100: grey[100],
200: grey[200],
light: grey[300],
400: grey[400],
main: grey[500],
600: grey[600],
dark: grey[700],
800: grey[800],
darker: grey[900],
A100: grey[0],
A200: grey[400],
A300: grey[700],
contrastText: grey[0]
},
error: {
lighter: '#FEEAE9',
light: '#F88078',
main: '#F04134',
dark: '#A82216',
darker: '#7A150C',
contrastText: '#FFFFFF'
},
warning: {
lighter: '#FFF7E0',
light: '#FFD666',
main: '#FFBF00',
dark: '#B38600',
darker: '#805F00',
contrastText: '#262626'
},
info: {
lighter: '#E0F7F8',
light: '#66CBD2',
main: '#00A2AE',
dark: '#00727B',
darker: '#005159',
contrastText: '#FFFFFF'
},
success: {
lighter: '#E3F6EC',
light: '#5CC98C',
main: '#00A854',
dark: '#00773B',
darker: '#00552A',
contrastText: '#FFFFFF'
},
grey,
text: {
primary: grey[800],
secondary: grey[600],
disabled: grey[400]
},
action: {
disabled: grey[300],
hover: 'rgba(192, 18, 39, 0.04)',
selected: 'rgba(192, 18, 39, 0.08)'
},
divider: grey[200],
background: {
paper: '#FFFFFF',
default: grey.A50
}
};
export default palette;

14
src/theme/shadows.js Normal file
View File

@@ -0,0 +1,14 @@
// ==============================|| DOORMILE THEME - CUSTOM SHADOWS ||============================== //
// Soft, subtle corporate elevation + a branded red glow for primary CTAs.
const customShadows = {
card: '0px 1px 4px rgba(0, 0, 0, 0.08)',
cardHover: '0px 4px 16px rgba(0, 0, 0, 0.10)',
widget: '0px 2px 14px rgba(38, 38, 38, 0.06)',
dropdown: '0px 8px 24px rgba(38, 38, 38, 0.12)',
primaryGlow: '0px 6px 16px rgba(192, 18, 39, 0.28)',
primaryGlowHover: '0px 8px 20px rgba(192, 18, 39, 0.36)',
header: '0px 1px 0px rgba(0, 0, 0, 0.06)'
};
export default customShadows;

25
src/theme/typography.js Normal file
View File

@@ -0,0 +1,25 @@
// ==============================|| DOORMILE THEME - TYPOGRAPHY ||============================== //
const typography = {
fontFamily: '"Public Sans", "Inter", "Helvetica", "Arial", sans-serif',
htmlFontSize: 16,
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 600,
h1: { fontWeight: 700, fontSize: '2.375rem', lineHeight: 1.21 },
h2: { fontWeight: 700, fontSize: '1.875rem', lineHeight: 1.27 },
h3: { fontWeight: 600, fontSize: '1.5rem', lineHeight: 1.33 },
h4: { fontWeight: 600, fontSize: '1.25rem', lineHeight: 1.4 },
h5: { fontWeight: 600, fontSize: '1rem', lineHeight: 1.5 },
h6: { fontWeight: 500, fontSize: '0.875rem', lineHeight: 1.57 },
caption: { fontWeight: 400, fontSize: '0.75rem', lineHeight: 1.66 },
body1: { fontSize: '0.875rem', lineHeight: 1.57 },
body2: { fontSize: '0.75rem', lineHeight: 1.66 },
subtitle1: { fontSize: '0.875rem', fontWeight: 600, lineHeight: 1.57 },
subtitle2: { fontSize: '0.75rem', fontWeight: 500, lineHeight: 1.66 },
overline: { fontSize: '0.6875rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase' },
button: { textTransform: 'capitalize', fontWeight: 600 }
};
export default typography;

20
src/utils/format.js Normal file
View File

@@ -0,0 +1,20 @@
// ==============================|| FORMAT HELPERS ||============================== //
export const inr = (n) =>
'₹' + Number(n || 0).toLocaleString('en-IN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
export const stringToColor = (string = '') => {
let hash = 0;
for (let i = 0; i < string.length; i += 1) hash = string.charCodeAt(i) + ((hash << 5) - hash);
const palette = ['#C01227', '#00A854', '#00A2AE', '#FFBF00', '#662582', '#1565C0', '#EF6C00', '#5C6BC0'];
return palette[Math.abs(hash) % palette.length];
};
export const initials = (name = '') =>
name
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0])
.join('')
.toUpperCase();

17
vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 3000,
open: true
}
});