first commit
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
50
README.md
Normal 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, 6–10px 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
21
index.html
Normal 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
2667
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal 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
BIN
public/Doormile-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
public/preloader.png
Normal file
BIN
public/preloader.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
76
src/App.jsx
Normal file
76
src/App.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/components/EmptyState.jsx
Normal file
33
src/components/EmptyState.jsx
Normal 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
51
src/components/Logo.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
src/components/MainCard.jsx
Normal file
17
src/components/MainCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/MapPlaceholder.jsx
Normal file
59
src/components/MapPlaceholder.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
src/components/PageHeader.jsx
Normal file
42
src/components/PageHeader.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
src/components/StatCard.jsx
Normal file
55
src/components/StatCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
src/components/StatusChip.jsx
Normal file
54
src/components/StatusChip.jsx
Normal 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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
30
src/components/TabLabelCount.jsx
Normal file
30
src/components/TabLabelCount.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
src/components/UserAvatar.jsx
Normal file
12
src/components/UserAvatar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
src/components/charts/AreaChart.jsx
Normal file
66
src/components/charts/AreaChart.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/charts/DonutChart.jsx
Normal file
59
src/components/charts/DonutChart.jsx
Normal 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
181
src/data/mock.js
Normal 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' }
|
||||
];
|
||||
261
src/layout/MainLayout/Header.jsx
Normal file
261
src/layout/MainLayout/Header.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
200
src/layout/MainLayout/Sidebar.jsx
Normal file
200
src/layout/MainLayout/Sidebar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
src/layout/MainLayout/index.jsx
Normal file
45
src/layout/MainLayout/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
src/layout/MinimalLayout.jsx
Normal file
11
src/layout/MinimalLayout.jsx
Normal 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
22
src/main.jsx
Normal 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
55
src/menu/navItems.jsx
Normal 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
124
src/pages/Dashboard.jsx
Normal 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
279
src/pages/Deliveries.jsx
Normal 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
93
src/pages/Pricing.jsx
Normal 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
69
src/pages/Profile.jsx
Normal 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
162
src/pages/Requests.jsx
Normal 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
206
src/pages/Settings.jsx
Normal 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
124
src/pages/auth/Login.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
95
src/pages/customers/CreateCustomer.jsx
Normal file
95
src/pages/customers/CreateCustomer.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
245
src/pages/customers/Customers.jsx
Normal file
245
src/pages/customers/Customers.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
180
src/pages/invoice/InvoicePreview.jsx
Normal file
180
src/pages/invoice/InvoicePreview.jsx
Normal 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 & 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
154
src/pages/invoice/Invoices.jsx
Normal file
154
src/pages/invoice/Invoices.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
src/pages/maintenance/ComingSoon.jsx
Normal file
80
src/pages/maintenance/ComingSoon.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
src/pages/maintenance/Error404.jsx
Normal file
34
src/pages/maintenance/Error404.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
src/pages/maintenance/Error500.jsx
Normal file
34
src/pages/maintenance/Error500.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
src/pages/maintenance/UnderConstruction.jsx
Normal file
45
src/pages/maintenance/UnderConstruction.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
135
src/pages/orders/AssignOrders.jsx
Normal file
135
src/pages/orders/AssignOrders.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
254
src/pages/orders/CreateMultipleOrders.jsx
Normal file
254
src/pages/orders/CreateMultipleOrders.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
142
src/pages/orders/CreateOrder.jsx
Normal file
142
src/pages/orders/CreateOrder.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
170
src/pages/orders/OrderDetails.jsx
Normal file
170
src/pages/orders/OrderDetails.jsx
Normal 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>
|
||||
);
|
||||
195
src/pages/orders/OrdersList.jsx
Normal file
195
src/pages/orders/OrdersList.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
190
src/pages/reports/OrdersDetails.jsx
Normal file
190
src/pages/reports/OrdersDetails.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
195
src/pages/reports/OrdersSummary.jsx
Normal file
195
src/pages/reports/OrdersSummary.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
105
src/pages/reports/RidersLogs.jsx
Normal file
105
src/pages/reports/RidersLogs.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
167
src/pages/reports/RidersSummary.jsx
Normal file
167
src/pages/reports/RidersSummary.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
71
src/pages/riders/CreateRider.jsx
Normal file
71
src/pages/riders/CreateRider.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
180
src/pages/riders/EditRider.jsx
Normal file
180
src/pages/riders/EditRider.jsx
Normal 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
221
src/pages/riders/Riders.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
src/pages/tenants/CreateClient.jsx
Normal file
81
src/pages/tenants/CreateClient.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
258
src/pages/tenants/Tenants.jsx
Normal file
258
src/pages/tenants/Tenants.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
121
src/theme/componentsOverride.js
Normal file
121
src/theme/componentsOverride.js
Normal 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
20
src/theme/index.js
Normal 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
102
src/theme/palette.js
Normal 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
14
src/theme/shadows.js
Normal 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
25
src/theme/typography.js
Normal 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
20
src/utils/format.js
Normal 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
17
vite.config.js
Normal 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
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user