first commit

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

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

@@ -0,0 +1,124 @@
import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import AreaChart from '@/components/charts/AreaChart';
import DonutChart from '@/components/charts/DonutChart';
import UserAvatar from '@/components/UserAvatar';
import { ordersTrend, statusBreakdown, orders, riders } from '@/data/mock';
import { inr } from '@/utils/format';
export default function Dashboard() {
return (
<>
<PageHeader
title="Dashboard"
breadcrumbs={[{ label: 'Dashboard' }]}
action={
<Stack direction="row" spacing={1.5}>
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }}>
<MenuItem value="all">All Locations</MenuItem>
<MenuItem value="blr">Bengaluru</MenuItem>
<MenuItem value="mum">Mumbai</MenuItem>
</TextField>
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />}>Export</Button>
</Stack>
}
/>
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Orders" value="1,402" icon={Inventory2OutlinedIcon} trend={8.4} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered" value="1,330" icon={LocalShippingOutlinedIcon} color="success" trend={6.1} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Riders" value="48" icon={TwoWheelerOutlinedIcon} color="info" trend={-2.3} caption="vs last month" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Revenue" value={inr(384200)} icon={CurrencyRupeeIcon} color="warning" trend={11.7} caption="vs last month" /></Grid>
<Grid item xs={12} lg={8}>
<MainCard
title="Orders Overview"
action={<Stack direction="row" spacing={2}><Legend color="#C01227" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
>
<AreaChart
labels={ordersTrend.map((d) => d.m)}
series={[
{ name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) },
{ name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) }
]}
/>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="Order Status">
<Box sx={{ py: 2 }}>
<DonutChart data={statusBreakdown} centerValue="1,402" centerLabel="Orders" />
</Box>
</MainCard>
</Grid>
<Grid item xs={12} lg={7}>
<MainCard title="Recent Orders" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Order ID</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Route</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.slice(0, 6).map((o) => (
<TableRow key={o.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell>
<TableCell>{o.customer}</TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary">{o.pickup} {o.drop}</Typography>
</TableCell>
<TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</MainCard>
</Grid>
<Grid item xs={12} lg={5}>
<MainCard title="Top Riders Today">
<Stack divider={<Divider />} spacing={0}>
{riders.slice(0, 5).map((r, i) => (
<Stack key={r.id} direction="row" spacing={2} alignItems="center" sx={{ py: 1.25 }}>
<Typography variant="subtitle2" color="text.secondary" sx={{ width: 18 }}>{i + 1}</Typography>
<UserAvatar name={r.name} size={36} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2">{r.name}</Typography>
<Typography variant="caption" color="text.secondary">{r.vehicle} · {r.rating}</Typography>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="subtitle2">{r.deliveries}</Typography>
<Typography variant="caption" color="text.secondary">deliveries</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
</Grid>
</>
);
}
function Legend({ color, label }) {
return (
<Stack direction="row" spacing={0.75} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: color }} />
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Stack>
);
}

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,206 @@
import { useState } from 'react';
import {
Grid,
Tabs,
Tab,
Box,
Stack,
TextField,
MenuItem,
Switch,
FormControlLabel,
Button,
Typography,
Divider,
Snackbar,
Alert
} from '@mui/material';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard';
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)'];
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
function TabPanel({ value, index, children }) {
if (value !== index) return null;
return <Box sx={{ pt: 1 }}>{children}</Box>;
}
export default function Settings() {
const [tab, setTab] = useState(0);
const [toast, setToast] = useState(false);
// General
const [general, setGeneral] = useState({
orgName: 'Doormile Logistics Pvt. Ltd.',
supportEmail: 'support@doormile.in',
contact: '+91 98450 11223',
timezone: TIMEZONES[0],
language: LANGUAGES[0]
});
// Notifications
const [notify, setNotify] = useState({
newOrders: true,
riderStatus: true,
invoicePaid: true,
weeklyDigest: false,
emailAlerts: true,
smsAlerts: false
});
// Security
const [security, setSecurity] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactor: false
});
const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked }));
const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked }));
const save = () => setToast(true);
return (
<>
<PageHeader
title="Settings"
breadcrumbs={[{ label: 'Settings' }]}
action={
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>
Save Changes
</Button>
}
/>
<Grid container spacing={2.5}>
<Grid item xs={12} md={3}>
<MainCard noPadding>
<Tabs
orientation="vertical"
value={tab}
onChange={(_, v) => setTab(v)}
sx={{
'& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 }
}}
>
<Tab icon={<TuneOutlinedIcon fontSize="small" />} iconPosition="start" label="General" />
<Tab icon={<NotificationsNoneIcon fontSize="small" />} iconPosition="start" label="Notifications" />
<Tab icon={<LockOutlinedIcon fontSize="small" />} iconPosition="start" label="Security" />
</Tabs>
</MainCard>
</Grid>
<Grid item xs={12} md={9}>
{/* General */}
<TabPanel value={tab} index={0}>
<MainCard title="Organisation">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Organisation Name" value={general.orgName} onChange={setG('orgName')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Support Email" value={general.supportEmail} onChange={setG('supportEmail')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Contact Number" value={general.contact} onChange={setG('contact')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Timezone" value={general.timezone} onChange={setG('timezone')}>
{TIMEZONES.map((t) => (
<MenuItem key={t} value={t}>{t}</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Language" value={general.language} onChange={setG('language')}>
{LANGUAGES.map((l) => (
<MenuItem key={l} value={l}>{l}</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</MainCard>
</TabPanel>
{/* Notifications */}
<TabPanel value={tab} index={1}>
<MainCard title="Notification Preferences">
<Stack divider={<Divider flexItem />} spacing={0}>
{[
{ k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' },
{ k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' },
{ k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' },
{ k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' }
].map((row) => (
<Stack key={row.k} direction="row" alignItems="center" justifyContent="space-between" sx={{ py: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{row.t}</Typography>
<Typography variant="caption" color="text.secondary">{row.d}</Typography>
</Box>
<Switch checked={notify[row.k]} onChange={setN(row.k)} />
</Stack>
))}
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Channels</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<FormControlLabel control={<Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} />} label="Email alerts" />
<FormControlLabel control={<Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} />} label="SMS alerts" />
</Stack>
</MainCard>
</TabPanel>
{/* Security */}
<TabPanel value={tab} index={2}>
<Stack spacing={2.5}>
<MainCard title="Change Password">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Current Password" value={security.currentPassword} onChange={setS('currentPassword')} />
</Grid>
<Grid item xs={12} />
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="New Password" value={security.newPassword} onChange={setS('newPassword')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Confirm New Password" value={security.confirmPassword} onChange={setS('confirmPassword')} />
</Grid>
</Grid>
</MainCard>
<MainCard title="Two-Factor Authentication">
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>Authenticator app</Typography>
<Typography variant="caption" color="text.secondary">
Require a one-time code at sign-in for extra security.
</Typography>
</Box>
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} />
</Stack>
</MainCard>
</Stack>
</TabPanel>
</Grid>
</Grid>
<Snackbar
open={toast}
autoHideDuration={2500}
onClose={() => setToast(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}>
Settings saved successfully.
</Alert>
</Snackbar>
</>
);
}

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

@@ -0,0 +1,124 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Grid,
Card,
Stack,
Typography,
TextField,
InputAdornment,
IconButton,
Button,
Checkbox,
FormControlLabel,
Link
} from '@mui/material';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import BoltIcon from '@mui/icons-material/Bolt';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import Logo from '@/components/Logo';
export default function Login() {
const navigate = useNavigate();
const [show, setShow] = useState(false);
const [auth, setAuth] = useState('admin@doormile.in');
const [pwd, setPwd] = useState('');
return (
<Grid container sx={{ minHeight: '100vh' }}>
{/* Brand panel */}
<Grid
item
md={6}
sx={{
display: { xs: 'none', md: 'flex' },
flexDirection: 'column',
justifyContent: 'space-between',
p: 6,
color: '#fff',
background: 'linear-gradient(150deg, #C01227 0%, #9E0E20 55%, #7E0B17 100%)',
position: 'relative',
overflow: 'hidden'
}}
>
<Box sx={{ position: 'absolute', width: 420, height: 420, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', top: -120, right: -120 }} />
<Box sx={{ position: 'absolute', width: 280, height: 280, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', bottom: -80, left: -60 }} />
<Logo onDark />
<Box sx={{ position: 'relative' }}>
<Typography variant="h2" sx={{ color: '#fff', fontWeight: 800, lineHeight: 1.2 }}>
Move every parcel,
<br /> on time, every time.
</Typography>
<Typography sx={{ color: 'rgba(255,255,255,0.85)', mt: 2, maxWidth: 420 }}>
The command center for your last-mile operation orders, riders, pricing and settlements in one corporate console.
</Typography>
<Stack spacing={1.5} sx={{ mt: 4 }}>
{[
{ icon: BoltIcon, t: 'AI-assisted route optimisation' },
{ icon: LocalShippingOutlinedIcon, t: 'Real-time rider & delivery tracking' },
{ icon: VerifiedOutlinedIcon, t: 'Automated client invoicing & payouts' }
].map((f) => (
<Stack key={f.t} direction="row" spacing={1.5} alignItems="center">
<Box sx={{ width: 34, height: 34, borderRadius: 2, bgcolor: 'rgba(255,255,255,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<f.icon fontSize="small" />
</Box>
<Typography sx={{ color: 'rgba(255,255,255,0.92)' }}>{f.t}</Typography>
</Stack>
))}
</Stack>
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
© 2026 Doormile Logistics Pvt. Ltd.
</Typography>
</Grid>
{/* Form panel */}
<Grid item xs={12} md={6} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 3, sm: 6 } }}>
<Card sx={{ width: '100%', maxWidth: 420, p: { xs: 3, sm: 4.5 } }}>
<Box sx={{ display: { xs: 'flex', md: 'none' }, mb: 2 }}><Logo /></Box>
<Typography variant="h3" sx={{ fontWeight: 700 }}>Welcome back</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, mb: 3 }}>
Sign in to your Doormile operations account.
</Typography>
<Stack spacing={2.5}>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Auth Name</Typography>
<TextField fullWidth placeholder="Enter your auth name" value={auth} onChange={(e) => setAuth(e.target.value)} />
</Box>
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Password</Typography>
<TextField
fullWidth
type={show ? 'text' : 'password'}
placeholder="Enter your password"
value={pwd}
onChange={(e) => setPwd(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
{show ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
</IconButton>
</InputAdornment>
)
}}
/>
</Box>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<FormControlLabel control={<Checkbox defaultChecked size="small" />} label={<Typography variant="body2">Remember me</Typography>} />
<Link href="#" underline="hover" variant="body2" color="primary">Forgot password?</Link>
</Stack>
<Button fullWidth size="large" variant="contained" onClick={() => navigate('/dashboard')}>
Sign In
</Button>
</Stack>
</Card>
</Grid>
</Grid>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,258 @@
import { useState, useMemo, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
Tooltip, TablePagination, Typography, Collapse, Grid
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip';
import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount';
import { tenants, tenantPricing } from '@/data/mock';
import { inr } from '@/utils/format';
const TABS = [
{ key: 'active', label: 'Active' },
{ key: 'pending', label: 'Pending' },
{ key: 'inactive', label: 'Inactive' }
];
function ReadField({ label, value }) {
return (
<Box>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.800' }}>{value || '—'}</Typography>
</Box>
);
}
function PricingTab() {
return (
<Box>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
<Button variant="contained" startIcon={<AddIcon />}>Add Pricing</Button>
</Stack>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1, overflow: 'hidden' }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Slab</TableCell>
<TableCell align="right">Base Price</TableCell>
<TableCell align="right">Min Kms</TableCell>
<TableCell align="right">Price/Km</TableCell>
<TableCell align="right">Other Charges</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tenantPricing.map((p, i) => (
<TableRow key={i}>
<TableCell>{p.date}</TableCell>
<TableCell>{p.slab}</TableCell>
<TableCell align="right">{inr(p.basePrice)}</TableCell>
<TableCell align="right">{p.minKms}</TableCell>
<TableCell align="right">{inr(p.pricePerKm)}</TableCell>
<TableCell align="right">{inr(p.otherCharges)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Box>
);
}
function EditTab({ tenant }) {
const [form, setForm] = useState({
name: tenant.name, contact: tenant.contact, phone: tenant.phone, email: tenant.email,
address: tenant.address, city: tenant.city, postcode: tenant.postcode, lat: tenant.lat, lng: tenant.lng
});
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
return (
<Box>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Tenant Name" value={form.name} onChange={set('name')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Person" value={form.contact} onChange={set('contact')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Number" value={form.phone} onChange={set('phone')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Email" value={form.email} onChange={set('email')} /></Grid>
<Grid item xs={12}><TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="PostCode" value={form.postcode} onChange={set('postcode')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Latitude" value={form.lat} onChange={set('lat')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Longitude" value={form.lng} onChange={set('lng')} /></Grid>
</Grid>
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 2.5 }}>
<Button variant="contained">Update</Button>
</Stack>
</Box>
);
}
function TenantRow({ row, index }) {
const [open, setOpen] = useState(false);
const [inner, setInner] = useState(0);
return (
<Fragment>
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
<TableCell padding="checkbox">
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{index + 1}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={row.name} size={34} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
<Typography variant="caption" color="text.secondary">{row.volume} orders / mo</Typography>
</Box>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2">{row.contact}</Typography>
<Typography variant="caption" color="text.secondary">{row.phone}</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">{row.address}</Typography>
<Typography variant="caption" color="text.secondary">{row.city} · {row.postcode}</Typography>
</TableCell>
<TableCell><StatusChip status={row.status} /></TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={6} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ m: 2, borderRadius: 1, border: 1, borderColor: 'divider', overflow: 'hidden' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', bgcolor: 'grey.50' }}>
<Tabs value={inner} onChange={(_, v) => setInner(v)} sx={{ px: 2 }}>
<Tab label="Details" />
<Tab label="Pricing" />
<Tab label="Edit" />
</Tabs>
</Box>
<Box sx={{ p: 2.5 }}>
{inner === 0 && (
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6} md={4}><ReadField label="Name" value={row.name} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Contact Person" value={row.contact} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Phone" value={row.phone} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="E-Mail" value={row.email} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Address" value={row.address} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="City" value={row.city} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="PostCode" value={row.postcode} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Latitude" value={row.lat} /></Grid>
<Grid item xs={12} sm={6} md={4}><ReadField label="Longitude" value={row.lng} /></Grid>
</Grid>
)}
{inner === 1 && <PricingTab />}
{inner === 2 && <EditTab tenant={row} />}
</Box>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
);
}
export default function Tenants() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(10);
const tabKey = TABS[tab].key;
const counts = useMemo(() => {
const c = {};
TABS.forEach((t) => { c[t.key] = tenants.filter((d) => d.status === t.key).length; });
return c;
}, []);
const filtered = useMemo(
() =>
tenants.filter((t) => {
const matchTab = t.status === tabKey;
const matchSearch =
!search ||
[t.name, t.contact, t.email, t.phone, t.city].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchSearch;
}),
[tabKey, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
return (
<>
<PageHeader
title="Tenants"
breadcrumbs={[{ label: 'Tenants' }]}
action={
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/tenants/create')}>
Create Client
</Button>
}
/>
<Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search clients…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 260 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox" />
<TableCell>S.No</TableCell>
<TableCell>Client</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.length === 0 ? (
<TableRow>
<TableCell colSpan={6} sx={{ border: 'none' }}>
<EmptyState title="No tenants found" caption="Try a different tab or search term." />
</TableCell>
</TableRow>
) : (
paged.map((row, i) => <TenantRow key={row.id} row={row} index={page * rpp + i} />)
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
</>
);
}