first commit
This commit is contained in:
124
src/pages/Dashboard.jsx
Normal file
124
src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material';
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
|
||||
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import AreaChart from '@/components/charts/AreaChart';
|
||||
import DonutChart from '@/components/charts/DonutChart';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { ordersTrend, statusBreakdown, orders, riders } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
breadcrumbs={[{ label: 'Dashboard' }]}
|
||||
action={
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }}>
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
<MenuItem value="blr">Bengaluru</MenuItem>
|
||||
<MenuItem value="mum">Mumbai</MenuItem>
|
||||
</TextField>
|
||||
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />}>Export</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Orders" value="1,402" icon={Inventory2OutlinedIcon} trend={8.4} caption="vs last month" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered" value="1,330" icon={LocalShippingOutlinedIcon} color="success" trend={6.1} caption="vs last month" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Riders" value="48" icon={TwoWheelerOutlinedIcon} color="info" trend={-2.3} caption="vs last month" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Revenue" value={inr(384200)} icon={CurrencyRupeeIcon} color="warning" trend={11.7} caption="vs last month" /></Grid>
|
||||
|
||||
<Grid item xs={12} lg={8}>
|
||||
<MainCard
|
||||
title="Orders Overview"
|
||||
action={<Stack direction="row" spacing={2}><Legend color="#C01227" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
|
||||
>
|
||||
<AreaChart
|
||||
labels={ordersTrend.map((d) => d.m)}
|
||||
series={[
|
||||
{ name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) },
|
||||
{ name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) }
|
||||
]}
|
||||
/>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="Order Status">
|
||||
<Box sx={{ py: 2 }}>
|
||||
<DonutChart data={statusBreakdown} centerValue="1,402" centerLabel="Orders" />
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={7}>
|
||||
<MainCard title="Recent Orders" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Order ID</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Route</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{orders.slice(0, 6).map((o) => (
|
||||
<TableRow key={o.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell>
|
||||
<TableCell>{o.customer}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption" color="text.secondary">{o.pickup} → {o.drop}</Typography>
|
||||
</TableCell>
|
||||
<TableCell><StatusChip status={o.status} /></TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={5}>
|
||||
<MainCard title="Top Riders Today">
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{riders.slice(0, 5).map((r, i) => (
|
||||
<Stack key={r.id} direction="row" spacing={2} alignItems="center" sx={{ py: 1.25 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" sx={{ width: 18 }}>{i + 1}</Typography>
|
||||
<UserAvatar name={r.name} size={36} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle2">{r.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.vehicle} · ⭐ {r.rating}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Typography variant="subtitle2">{r.deliveries}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">deliveries</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend({ color, label }) {
|
||||
return (
|
||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: color }} />
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
279
src/pages/Deliveries.jsx
Normal file
279
src/pages/Deliveries.jsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { useState, useMemo, Fragment } from 'react';
|
||||
import {
|
||||
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
Tooltip, TablePagination, Typography, Collapse, Menu
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined';
|
||||
import SwapHorizOutlinedIcon from '@mui/icons-material/SwapHorizOutlined';
|
||||
import EditLocationAltOutlinedIcon from '@mui/icons-material/EditLocationAltOutlined';
|
||||
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { deliveries, locations, tenantsList, riders } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'assigned', label: 'Assigned' },
|
||||
{ key: 'accepted', label: 'Accepted' },
|
||||
{ key: 'arrived', label: 'Arrived' },
|
||||
{ key: 'picked', label: 'Picked' },
|
||||
{ key: 'active', label: 'Active' },
|
||||
{ key: 'skipped', label: 'Skipped' },
|
||||
{ key: 'delivered', label: 'Delivered' },
|
||||
{ key: 'cancelled', label: 'Cancelled' }
|
||||
];
|
||||
|
||||
function ProductTable({ products = [] }) {
|
||||
return (
|
||||
<Box sx={{ m: 2, borderRadius: 1, border: 1, borderColor: 'divider', overflow: 'hidden' }}>
|
||||
<Typography variant="subtitle2" sx={{ px: 2, py: 1.25, bgcolor: 'grey.50', color: 'grey.800' }}>
|
||||
Products
|
||||
</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Product Name</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell align="center">Quantity</TableCell>
|
||||
<TableCell align="right">Cost</TableCell>
|
||||
<TableCell align="right">Price</TableCell>
|
||||
<TableCell align="right">Tax</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{products.map((p, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>{p.name}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption" color="text.secondary">{p.description}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">{p.qty}</TableCell>
|
||||
<TableCell align="right">{inr(p.cost)}</TableCell>
|
||||
<TableCell align="right">{inr(p.price)}</TableCell>
|
||||
<TableCell align="right">{p.tax}%</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(p.amount)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function DeliveryRow({ row, index }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [anchor, setAnchor] = useState(null);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
|
||||
<TableCell padding="checkbox">
|
||||
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{row.tenant}</TableCell>
|
||||
<TableCell>{row.location}</TableCell>
|
||||
<TableCell>{row.pickup}</TableCell>
|
||||
<TableCell>{row.drop}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<UserAvatar name={row.rider} size={26} />
|
||||
<Typography variant="body2">{row.rider}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>{row.notes || '—'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">{row.qty}</TableCell>
|
||||
<TableCell align="right">{row.cod ? inr(row.cod) : '—'}</TableCell>
|
||||
<TableCell align="right">{row.kms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="Actions">
|
||||
<IconButton size="small" onClick={(e) => setAnchor(e.currentTarget)}>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>
|
||||
<MenuItem onClick={() => setAnchor(null)}>
|
||||
<NotificationsActiveOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Notify Rider
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setAnchor(null)}>
|
||||
<SwapHorizOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Change Rider
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setAnchor(null)}>
|
||||
<EditLocationAltOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Update Delivery Status
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setAnchor(null)} sx={{ color: 'error.main' }}>
|
||||
<CancelOutlinedIcon fontSize="small" sx={{ mr: 1.5 }} /> Cancel Delivery
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<ProductTable products={row.products} />
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Deliveries() {
|
||||
const [tab, setTab] = useState(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [tenant, setTenant] = useState('all');
|
||||
const [location, setLocation] = useState('all');
|
||||
const [rider, setRider] = useState('all');
|
||||
const [headerLocation, setHeaderLocation] = useState('all');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(5);
|
||||
|
||||
const tabKey = TABS[tab].key;
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const c = {};
|
||||
TABS.forEach((t) => { c[t.key] = deliveries.filter((d) => d.status === t.key).length; });
|
||||
return c;
|
||||
}, []);
|
||||
|
||||
const stats = useMemo(() => ({
|
||||
created: deliveries.length,
|
||||
pending: deliveries.filter((d) => d.status === 'pending').length,
|
||||
delivered: deliveries.filter((d) => d.status === 'delivered').length,
|
||||
cancelled: deliveries.filter((d) => d.status === 'cancelled').length
|
||||
}), []);
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
deliveries.filter((d) => {
|
||||
const matchTab = d.status === tabKey;
|
||||
const matchTenant = tenant === 'all' || d.tenant === tenant;
|
||||
const matchLocation = location === 'all' || d.location === location;
|
||||
const matchRider = rider === 'all' || d.rider === rider;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[d.id, d.tenant, d.pickup, d.drop, d.rider, d.location].join(' ').toLowerCase().includes(search.toLowerCase());
|
||||
return matchTab && matchTenant && matchLocation && matchRider && matchSearch;
|
||||
}),
|
||||
[tabKey, tenant, location, rider, search]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Deliveries"
|
||||
breadcrumbs={[{ label: 'Deliveries' }]}
|
||||
action={
|
||||
<TextField
|
||||
select size="small" value={headerLocation} onChange={(e) => setHeaderLocation(e.target.value)}
|
||||
sx={{ minWidth: 180 }} label="Location"
|
||||
>
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Created Orders" value={stats.created} icon={Inventory2OutlinedIcon} caption="100%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending Orders" value={stats.pending} icon={HourglassEmptyOutlinedIcon} color="warning" caption="25%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered Orders" value={stats.delivered} icon={CheckCircleOutlineIcon} color="success" caption="25%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cancelled Orders" value={stats.cancelled} icon={CancelOutlinedIcon} color="error" caption="12.5%" /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Card sx={{ mt: 1.5 }}>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap>
|
||||
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
Jun 01 – Jun 05
|
||||
</Button>
|
||||
<TextField select size="small" value={tenant} onChange={(e) => { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant">
|
||||
<MenuItem value="all">All Tenants</MenuItem>
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" value={rider} onChange={(e) => { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider">
|
||||
<MenuItem value="all">All Riders</MenuItem>
|
||||
{riders.map((r) => <MenuItem key={r.id} value={r.name}>{r.name}</MenuItem>)}
|
||||
</TextField>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<TextField
|
||||
size="small" placeholder="Search deliveries…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||
sx={{ minWidth: 220 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
|
||||
{TABS.map((t, i) => (
|
||||
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox" />
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Tenant</TableCell>
|
||||
<TableCell>Order Location</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Drop</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell align="center">Qty</TableCell>
|
||||
<TableCell align="right">COD</TableCell>
|
||||
<TableCell align="right">Kms</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
<TableCell align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} sx={{ border: 'none' }}>
|
||||
<EmptyState title="No deliveries found" caption="Try adjusting filters or switching tabs." />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paged.map((row, i) => <DeliveryRow key={row.id} row={row} index={page * rpp + i} />)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
93
src/pages/Pricing.jsx
Normal file
93
src/pages/Pricing.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Autocomplete, TextField, Box,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { pricing, locations } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const SLAB_COLORS = { '0-5 km': 'info', '5-10 km': 'warning', '10+ km': 'success' };
|
||||
const NAME_COLORS = { Standard: 'primary', Express: 'warning', Bulk: 'success' };
|
||||
|
||||
export default function Pricing() {
|
||||
const [location, setLocation] = useState(null);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => (location ? pricing.filter((p) => p.location === location) : pricing),
|
||||
[location]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Pricing"
|
||||
breadcrumbs={[{ label: 'Pricing' }]}
|
||||
action={
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={locations}
|
||||
value={location}
|
||||
onChange={(_, v) => setLocation(v)}
|
||||
sx={{ minWidth: 240 }}
|
||||
renderInput={(params) => <TextField {...params} label="Filter by Location" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<MainCard noPadding>
|
||||
{filtered.length === 0 ? (
|
||||
<EmptyState title="No Pricing List" caption="No pricing configured for the selected location." />
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell>Pricing Id</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Slab</TableCell>
|
||||
<TableCell align="right">Base Price</TableCell>
|
||||
<TableCell align="right">MinKm</TableCell>
|
||||
<TableCell align="right">Price/Km</TableCell>
|
||||
<TableCell align="right">MaxKm</TableCell>
|
||||
<TableCell align="center">Min Orders</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filtered.map((p, i) => (
|
||||
<TableRow key={p.id} hover>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={p.location} color="info" variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'primary.main' }}>{p.pricingId}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={p.name} color={NAME_COLORS[p.name] || 'primary'} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={p.slab} color={SLAB_COLORS[p.slab] || 'default'} variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(p.basePrice)}</TableCell>
|
||||
<TableCell align="right">{p.minKm}</TableCell>
|
||||
<TableCell align="right">{inr(p.pricePerKm)}</TableCell>
|
||||
<TableCell align="right">{p.maxKm}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip size="small" label={p.minOrders} color="warning" variant="outlined" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
69
src/pages/Profile.jsx
Normal file
69
src/pages/Profile.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Grid, Card, CardContent, Stack, Typography, TextField } from '@mui/material';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
const PROFILE = {
|
||||
userId: 'U1001',
|
||||
userName: 'Aarav Menon',
|
||||
appLocation: 'Bengaluru',
|
||||
authName: 'admin@doormile.in',
|
||||
contactNo: '+91 98450 11223',
|
||||
email: 'aarav.menon@doormile.in',
|
||||
address: 'No. 7, Brigade Road, MG Road Area',
|
||||
location: 'Bengaluru',
|
||||
city: 'Bengaluru',
|
||||
state: 'Karnataka',
|
||||
postcode: '560001',
|
||||
role: 'Operations Administrator'
|
||||
};
|
||||
|
||||
const ro = (value) => ({
|
||||
fullWidth: true,
|
||||
variant: 'standard',
|
||||
value,
|
||||
InputProps: { readOnly: true }
|
||||
});
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="User Profile" breadcrumbs={[{ label: 'User Profile' }]} />
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction="row" spacing={2.5} alignItems="center">
|
||||
<UserAvatar name={PROFILE.userName} size={72} />
|
||||
<div>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.800' }}>{PROFILE.userName}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{PROFILE.role}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<MainCard title="Account Information">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="User ID" {...ro(PROFILE.userId)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="User Name" {...ro(PROFILE.userName)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="App Location" {...ro(PROFILE.appLocation)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="Auth Name" {...ro(PROFILE.authName)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="Contact No" {...ro(PROFILE.contactNo)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><TextField label="E-Mail" {...ro(PROFILE.email)} /></Grid>
|
||||
<Grid item xs={12}><TextField label="Address" {...ro(PROFILE.address)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={3}><TextField label="Location" {...ro(PROFILE.location)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={3}><TextField label="City" {...ro(PROFILE.city)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={3}><TextField label="State" {...ro(PROFILE.state)} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={3}><TextField label="Postcode" {...ro(PROFILE.postcode)} /></Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
162
src/pages/Requests.jsx
Normal file
162
src/pages/Requests.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Card, Stack, Button, Box, Collapse, Tabs, Tab, Typography, Grid,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { requests } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
function RequestRow({ row, index }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>{row.requestor}</TableCell>
|
||||
<TableCell>{row.bank}</TableCell>
|
||||
<TableCell>{row.ifsc}</TableCell>
|
||||
<TableCell>{row.refNo}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
|
||||
<TableCell>{row.reason}</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell sx={{ py: 0, borderBottom: open ? 1 : 0, borderColor: 'divider' }} colSpan={8}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ m: 2 }}>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab label="Client Details" />
|
||||
<Tab label="Client Pricing" />
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Contact Name</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.contact}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Address</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.address}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">City</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.city}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Zip Code</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.zip}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{tab === 1 && (
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700 } }}>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Skill</TableCell>
|
||||
<TableCell align="right">Cost/Hr</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{row.pricing.map((p, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>{p.category}</TableCell>
|
||||
<TableCell>{p.skill}</TableCell>
|
||||
<TableCell align="right">{inr(p.cost)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Requests() {
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(10);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const paged = requests.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Requests"
|
||||
breadcrumbs={[{ label: 'Requests' }]}
|
||||
action={
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpen(true)}>
|
||||
Create Request
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Requestor</TableCell>
|
||||
<TableCell>Bank</TableCell>
|
||||
<TableCell>IFSC</TableCell>
|
||||
<TableCell>Ref No</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
<TableCell>Reason</TableCell>
|
||||
<TableCell align="center" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((row, idx) => (
|
||||
<RequestRow key={row.id} row={row} index={page * rpp + idx} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={requests.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Create Request</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2.5} sx={{ mt: 0 }}>
|
||||
<Grid item xs={12} sm={6}><TextField label="Reference No" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Requestor" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Bank Name" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Amount" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Account No" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="IFSC Code" fullWidth /></Grid>
|
||||
<Grid item xs={12}><TextField label="Reason" fullWidth multiline minRows={3} /></Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setOpen(false)} color="inherit">Close</Button>
|
||||
<Button variant="contained" onClick={() => setOpen(false)}>Update</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
206
src/pages/Settings.jsx
Normal file
206
src/pages/Settings.jsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
Stack,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Typography,
|
||||
Divider,
|
||||
Snackbar,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
|
||||
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)'];
|
||||
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
|
||||
|
||||
function TabPanel({ value, index, children }) {
|
||||
if (value !== index) return null;
|
||||
return <Box sx={{ pt: 1 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const [tab, setTab] = useState(0);
|
||||
const [toast, setToast] = useState(false);
|
||||
|
||||
// General
|
||||
const [general, setGeneral] = useState({
|
||||
orgName: 'Doormile Logistics Pvt. Ltd.',
|
||||
supportEmail: 'support@doormile.in',
|
||||
contact: '+91 98450 11223',
|
||||
timezone: TIMEZONES[0],
|
||||
language: LANGUAGES[0]
|
||||
});
|
||||
|
||||
// Notifications
|
||||
const [notify, setNotify] = useState({
|
||||
newOrders: true,
|
||||
riderStatus: true,
|
||||
invoicePaid: true,
|
||||
weeklyDigest: false,
|
||||
emailAlerts: true,
|
||||
smsAlerts: false
|
||||
});
|
||||
|
||||
// Security
|
||||
const [security, setSecurity] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
twoFactor: false
|
||||
});
|
||||
|
||||
const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
|
||||
const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked }));
|
||||
const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked }));
|
||||
|
||||
const save = () => setToast(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Settings"
|
||||
breadcrumbs={[{ label: 'Settings' }]}
|
||||
action={
|
||||
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>
|
||||
Save Changes
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<MainCard noPadding>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={tab}
|
||||
onChange={(_, v) => setTab(v)}
|
||||
sx={{
|
||||
'& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 }
|
||||
}}
|
||||
>
|
||||
<Tab icon={<TuneOutlinedIcon fontSize="small" />} iconPosition="start" label="General" />
|
||||
<Tab icon={<NotificationsNoneIcon fontSize="small" />} iconPosition="start" label="Notifications" />
|
||||
<Tab icon={<LockOutlinedIcon fontSize="small" />} iconPosition="start" label="Security" />
|
||||
</Tabs>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={9}>
|
||||
{/* General */}
|
||||
<TabPanel value={tab} index={0}>
|
||||
<MainCard title="Organisation">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth label="Organisation Name" value={general.orgName} onChange={setG('orgName')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth label="Support Email" value={general.supportEmail} onChange={setG('supportEmail')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth label="Contact Number" value={general.contact} onChange={setG('contact')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField select fullWidth label="Timezone" value={general.timezone} onChange={setG('timezone')}>
|
||||
{TIMEZONES.map((t) => (
|
||||
<MenuItem key={t} value={t}>{t}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField select fullWidth label="Language" value={general.language} onChange={setG('language')}>
|
||||
{LANGUAGES.map((l) => (
|
||||
<MenuItem key={l} value={l}>{l}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</TabPanel>
|
||||
|
||||
{/* Notifications */}
|
||||
<TabPanel value={tab} index={1}>
|
||||
<MainCard title="Notification Preferences">
|
||||
<Stack divider={<Divider flexItem />} spacing={0}>
|
||||
{[
|
||||
{ k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' },
|
||||
{ k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' },
|
||||
{ k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' },
|
||||
{ k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' }
|
||||
].map((row) => (
|
||||
<Stack key={row.k} direction="row" alignItems="center" justifyContent="space-between" sx={{ py: 1.5 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{row.t}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{row.d}</Typography>
|
||||
</Box>
|
||||
<Switch checked={notify[row.k]} onChange={setN(row.k)} />
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Channels</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||
<FormControlLabel control={<Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} />} label="Email alerts" />
|
||||
<FormControlLabel control={<Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} />} label="SMS alerts" />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</TabPanel>
|
||||
|
||||
{/* Security */}
|
||||
<TabPanel value={tab} index={2}>
|
||||
<Stack spacing={2.5}>
|
||||
<MainCard title="Change Password">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth type="password" label="Current Password" value={security.currentPassword} onChange={setS('currentPassword')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} />
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth type="password" label="New Password" value={security.newPassword} onChange={setS('newPassword')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth type="password" label="Confirm New Password" value={security.confirmPassword} onChange={setS('confirmPassword')} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
<MainCard title="Two-Factor Authentication">
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>Authenticator app</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Require a one-time code at sign-in for extra security.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Snackbar
|
||||
open={toast}
|
||||
autoHideDuration={2500}
|
||||
onClose={() => setToast(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}>
|
||||
Settings saved successfully.
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
124
src/pages/auth/Login.jsx
Normal file
124
src/pages/auth/Login.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
Stack,
|
||||
Typography,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Link
|
||||
} from '@mui/material';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [show, setShow] = useState(false);
|
||||
const [auth, setAuth] = useState('admin@doormile.in');
|
||||
const [pwd, setPwd] = useState('');
|
||||
|
||||
return (
|
||||
<Grid container sx={{ minHeight: '100vh' }}>
|
||||
{/* Brand panel */}
|
||||
<Grid
|
||||
item
|
||||
md={6}
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
p: 6,
|
||||
color: '#fff',
|
||||
background: 'linear-gradient(150deg, #C01227 0%, #9E0E20 55%, #7E0B17 100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', width: 420, height: 420, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', top: -120, right: -120 }} />
|
||||
<Box sx={{ position: 'absolute', width: 280, height: 280, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', bottom: -80, left: -60 }} />
|
||||
<Logo onDark />
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Typography variant="h2" sx={{ color: '#fff', fontWeight: 800, lineHeight: 1.2 }}>
|
||||
Move every parcel,
|
||||
<br /> on time, every time.
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.85)', mt: 2, maxWidth: 420 }}>
|
||||
The command center for your last-mile operation — orders, riders, pricing and settlements in one corporate console.
|
||||
</Typography>
|
||||
<Stack spacing={1.5} sx={{ mt: 4 }}>
|
||||
{[
|
||||
{ icon: BoltIcon, t: 'AI-assisted route optimisation' },
|
||||
{ icon: LocalShippingOutlinedIcon, t: 'Real-time rider & delivery tracking' },
|
||||
{ icon: VerifiedOutlinedIcon, t: 'Automated client invoicing & payouts' }
|
||||
].map((f) => (
|
||||
<Stack key={f.t} direction="row" spacing={1.5} alignItems="center">
|
||||
<Box sx={{ width: 34, height: 34, borderRadius: 2, bgcolor: 'rgba(255,255,255,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<f.icon fontSize="small" />
|
||||
</Box>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.92)' }}>{f.t}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
© 2026 Doormile Logistics Pvt. Ltd.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* Form panel */}
|
||||
<Grid item xs={12} md={6} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 3, sm: 6 } }}>
|
||||
<Card sx={{ width: '100%', maxWidth: 420, p: { xs: 3, sm: 4.5 } }}>
|
||||
<Box sx={{ display: { xs: 'flex', md: 'none' }, mb: 2 }}><Logo /></Box>
|
||||
<Typography variant="h3" sx={{ fontWeight: 700 }}>Welcome back</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, mb: 3 }}>
|
||||
Sign in to your Doormile operations account.
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2.5}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Auth Name</Typography>
|
||||
<TextField fullWidth placeholder="Enter your auth name" value={auth} onChange={(e) => setAuth(e.target.value)} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 0.75 }}>Password</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
type={show ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
value={pwd}
|
||||
onChange={(e) => setPwd(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShow((s) => !s)} edge="end" size="small">
|
||||
{show ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<FormControlLabel control={<Checkbox defaultChecked size="small" />} label={<Typography variant="body2">Remember me</Typography>} />
|
||||
<Link href="#" underline="hover" variant="body2" color="primary">Forgot password?</Link>
|
||||
</Stack>
|
||||
<Button fullWidth size="large" variant="contained" onClick={() => navigate('/dashboard')}>
|
||||
Sign In
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
95
src/pages/customers/CreateCustomer.jsx
Normal file
95
src/pages/customers/CreateCustomer.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import { locations, tenantsList } from '@/data/mock';
|
||||
|
||||
const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971'];
|
||||
|
||||
export default function CreateCustomer() {
|
||||
const navigate = useNavigate();
|
||||
const [code, setCode] = useState('+91');
|
||||
const [topLocation, setTopLocation] = useState('');
|
||||
const [client, setClient] = useState('');
|
||||
const [form, setForm] = useState({
|
||||
name: '', phone: '', email: '', doorNo: '', address: '',
|
||||
location: '', city: '', state: '', postcode: '', landmark: ''
|
||||
});
|
||||
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create Customer"
|
||||
breadcrumbs={[{ label: 'Customers', to: '/customers' }, { label: 'Create Customer' }]}
|
||||
/>
|
||||
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2.5 }}>
|
||||
<TextField select size="small" label="Location" value={topLocation} onChange={(e) => setTopLocation(e.target.value)} sx={{ minWidth: 220 }}>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" label="Client" value={client} onChange={(e) => setClient(e.target.value)} sx={{ minWidth: 220 }}>
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<MainCard title="Customer Details">
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Name" value={form.name} onChange={set('name')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth size="small" label="Phone Number" value={form.phone} onChange={set('phone')}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<TextField
|
||||
select variant="standard" value={code} onChange={(e) => setCode(e.target.value)}
|
||||
InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }}
|
||||
>
|
||||
{COUNTRY_CODES.map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
|
||||
</TextField>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Email" value={form.email} onChange={set('email')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Door No" value={form.doorNo} onChange={set('doorNo')} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth size="small" label="Location" value={form.location} onChange={set('location')}>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Post Code" value={form.postcode} onChange={set('postcode')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={1.5} sx={{ mt: 3 }}>
|
||||
<Button variant="outlined" onClick={() => navigate('/customers')}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => navigate('/customers')}>Create</Button>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
245
src/pages/customers/Customers.jsx
Normal file
245
src/pages/customers/Customers.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Stack, Button, TextField, MenuItem, InputAdornment, Box,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
Tooltip, TablePagination, Typography,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, Divider
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { customers, locations } from '@/data/mock';
|
||||
|
||||
const EMPTY_FORM = {
|
||||
name: '', phone: '', address: '', location: '', city: '', state: '',
|
||||
postcode: '', landmark: '', lat: '', lng: ''
|
||||
};
|
||||
|
||||
export default function Customers() {
|
||||
const navigate = useNavigate();
|
||||
const [search, setSearch] = useState('');
|
||||
const [location, setLocation] = useState('all');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(5);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const [viewer, setViewer] = useState(null);
|
||||
|
||||
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
customers.filter((c) => {
|
||||
const matchLocation = location === 'all' || c.location === location;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[c.name, c.phone, c.email, c.address, c.location, c.city]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
return matchLocation && matchSearch;
|
||||
}),
|
||||
[search, location]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
const openEdit = (c) => {
|
||||
setForm({
|
||||
name: c.name, phone: c.phone, address: c.address, location: c.location,
|
||||
city: c.city, state: c.state, postcode: c.postcode, landmark: c.landmark || '',
|
||||
lat: c.lat || '', lng: c.lng || ''
|
||||
});
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const openView = (c) => {
|
||||
setViewer(c);
|
||||
setViewOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Customers"
|
||||
breadcrumbs={[{ label: 'Customers' }]}
|
||||
action={
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
|
||||
<TextField
|
||||
select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }}
|
||||
sx={{ minWidth: 160 }} label="Location"
|
||||
>
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small" placeholder="Search customers…" value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/customers/create')}>
|
||||
Add Customer
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<MainCard noPadding>
|
||||
{filtered.length === 0 ? (
|
||||
<EmptyState title="No Customers Found" caption="Try adjusting your search or location filter." />
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((c, i) => (
|
||||
<TableRow key={c.id} hover>
|
||||
<TableCell>{page * rpp + i + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<UserAvatar name={c.name} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{c.name}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{c.phone}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{c.email}</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 220 }}>
|
||||
<Typography variant="body2" noWrap>{c.address}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{c.city}, {c.state} {c.postcode}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{c.location}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="View"><IconButton size="small" onClick={() => openView(c)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Edit"><IconButton size="small" onClick={() => openEdit(c)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MainCard>
|
||||
|
||||
<Dialog open={viewOpen} onClose={() => setViewOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Customer Details</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
{viewer && (
|
||||
<>
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
|
||||
<UserAvatar name={viewer.name} size={56} />
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800' }}>{viewer.name}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{viewer.location}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Grid container spacing={2}>
|
||||
{[
|
||||
['Phone', viewer.phone],
|
||||
['Email', viewer.email],
|
||||
['Address', viewer.address],
|
||||
['City', viewer.city],
|
||||
['State', viewer.state],
|
||||
['Postcode', viewer.postcode],
|
||||
['Landmark', viewer.landmark || '—'],
|
||||
['Coordinates', viewer.lat && viewer.lng ? `${viewer.lat}, ${viewer.lng}` : '—']
|
||||
].map(([label, value]) => (
|
||||
<Grid item xs={12} sm={6} key={label}>
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{value}</Typography>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button variant="outlined" onClick={() => setViewOpen(false)}>Close</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => { setViewOpen(false); openEdit(viewer); }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={editOpen} onClose={() => setEditOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Edit Customer</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Grid container spacing={2.5} sx={{ mt: 0 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Customer Name" value={form.name} onChange={set('name')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth size="small" label="Contact Number" value={form.phone} onChange={set('phone')}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">+91</InputAdornment> }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth size="small" label="Location" value={form.location} onChange={set('location')}>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Postcode" value={form.postcode} onChange={set('postcode')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} />
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Latitude" value={form.lat} onChange={set('lat')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth size="small" label="Longitude" value={form.lng} onChange={set('lng')} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button variant="outlined" onClick={() => setEditOpen(false)}>Close</Button>
|
||||
<Button variant="contained" onClick={() => setEditOpen(false)}>Update</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
180
src/pages/invoice/InvoicePreview.jsx
Normal file
180
src/pages/invoice/InvoicePreview.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Box, Card, Stack, Button, Typography, Chip, Divider, IconButton,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Grid,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, TextField
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
|
||||
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import Logo from '@/components/Logo';
|
||||
import { invoices, invoiceLineItems, tenants } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
export default function InvoicePreview() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [payOpen, setPayOpen] = useState(false);
|
||||
|
||||
const invoice = invoices.find((i) => String(i.id) === String(id)) || invoices[0];
|
||||
const tenant = tenants[0];
|
||||
|
||||
const subTotal = invoiceLineItems.reduce((a, l) => a + l.amount, 0);
|
||||
const discount = Math.round(subTotal * 0.05);
|
||||
const taxable = subTotal - discount;
|
||||
const tax = Math.round(taxable * 0.18);
|
||||
const grandTotal = taxable + tax;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Invoice Details"
|
||||
breadcrumbs={[{ label: 'Invoice', to: '/invoice' }, { label: 'Details' }]}
|
||||
action={
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems="center">
|
||||
<IconButton onClick={() => navigate('/invoice')} sx={{ border: 1, borderColor: 'grey.300' }}>
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Chip label={invoice.invoiceId} sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', fontWeight: 600 }} />
|
||||
<Button variant="outlined" startIcon={<PaymentsOutlinedIcon />} onClick={() => setPayOpen(true)}>
|
||||
Update Payment
|
||||
</Button>
|
||||
<Button variant="contained" startIcon={<PrintOutlinedIcon />} onClick={() => window.print()}>
|
||||
Print
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Card sx={{ width: '100%', maxWidth: 860, p: { xs: 3, sm: 5 } }}>
|
||||
{/* Top band */}
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" spacing={3}>
|
||||
<Box>
|
||||
<Logo />
|
||||
<Typography variant="subtitle2" sx={{ mt: 2, fontWeight: 700, color: 'grey.800' }}>From</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>Doormile Logistics Pvt. Ltd.</Typography>
|
||||
<Typography variant="body2" color="text.secondary">No. 7, Brigade Road</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Bengaluru, Karnataka 560001</Typography>
|
||||
<Typography variant="body2" color="text.secondary">GSTIN: 29ABCDE1234F1Z5</Typography>
|
||||
<Typography variant="body2" color="text.secondary">billing@doormile.in</Typography>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: { sm: 'right' } }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: 'primary.main' }}>INVOICE</Typography>
|
||||
<Typography variant="subtitle2" sx={{ mt: 2, fontWeight: 700, color: 'grey.800' }}>To</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{tenant.name}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{tenant.contact}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{tenant.address}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{tenant.city} {tenant.postcode}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{tenant.email}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Invoice meta */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Typography variant="caption" color="text.secondary">Invoice No</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.invoiceId}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Typography variant="caption" color="text.secondary">Date</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.invoiceDate}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Typography variant="caption" color="text.secondary">Due Date</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.dueDate}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Typography variant="caption" color="text.secondary">Period</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{invoice.period}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Line items */}
|
||||
<TableContainer sx={{ mt: 3 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700 } }}>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Particulars</TableCell>
|
||||
<TableCell>Unit</TableCell>
|
||||
<TableCell align="center">Quantity</TableCell>
|
||||
<TableCell align="right">Rate</TableCell>
|
||||
<TableCell align="right">Other Charges</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{invoiceLineItems.map((l, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>{idx + 1}</TableCell>
|
||||
<TableCell>{l.particulars}</TableCell>
|
||||
<TableCell>{l.unit}</TableCell>
|
||||
<TableCell align="center">{l.qty}</TableCell>
|
||||
<TableCell align="right">{inr(l.rate)}</TableCell>
|
||||
<TableCell align="right">{inr(l.other)}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(l.amount)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Totals */}
|
||||
<Stack alignItems="flex-end" sx={{ mt: 3 }}>
|
||||
<Box sx={{ width: { xs: '100%', sm: 320 } }}>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
|
||||
<Typography variant="body2" color="text.secondary">Sub Total</Typography>
|
||||
<Typography variant="body2">{inr(subTotal)}</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
|
||||
<Typography variant="body2" color="text.secondary">Discount (5%)</Typography>
|
||||
<Typography variant="body2">- {inr(discount)}</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.75 }}>
|
||||
<Typography variant="body2" color="text.secondary">Tax (18% GST)</Typography>
|
||||
<Typography variant="body2">{inr(tax)}</Typography>
|
||||
</Stack>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ py: 0.5 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>Grand Total</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'primary.main' }}>{inr(grandTotal)}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Notes + accent */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.800' }}>Notes & Terms</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Payment is due within 15 days of the invoice date. Please make payments via NEFT/RTGS to the
|
||||
registered account. A 1.5% monthly interest applies to overdue balances. This is a
|
||||
computer-generated invoice and does not require a signature.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ mt: 3, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Update Payment Dialog */}
|
||||
<Dialog open={payOpen} onClose={() => setPayOpen(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Update Payment</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
||||
<TextField label="Reference No" type="number" fullWidth placeholder="Enter transaction reference" />
|
||||
<TextField label="Remarks" fullWidth multiline minRows={3} placeholder="Add a remark for this payment" />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setPayOpen(false)} color="inherit">Cancel</Button>
|
||||
<Button variant="contained" onClick={() => setPayOpen(false)}>Update</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
154
src/pages/invoice/Invoices.jsx
Normal file
154
src/pages/invoice/Invoices.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, Stack, Button, Box, Tabs, Tab,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
Tooltip, TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, Typography
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
|
||||
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { invoices } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'open', label: 'Open' },
|
||||
{ key: 'overdue', label: 'Overdue' },
|
||||
{ key: 'paid', label: 'Paid' }
|
||||
];
|
||||
|
||||
export default function Invoices() {
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(10);
|
||||
const [dateOpen, setDateOpen] = useState(false);
|
||||
const [from, setFrom] = useState(dayjs().startOf('month'));
|
||||
const [to, setTo] = useState(dayjs());
|
||||
|
||||
const totals = useMemo(() => {
|
||||
const sum = (s) => invoices.filter((i) => i.status === s).reduce((a, i) => a + i.amount, 0);
|
||||
return {
|
||||
billed: invoices.reduce((a, i) => a + i.amount, 0),
|
||||
paid: sum('paid'),
|
||||
open: sum('open'),
|
||||
overdue: sum('overdue')
|
||||
};
|
||||
}, []);
|
||||
|
||||
const counts = {
|
||||
all: invoices.length,
|
||||
open: invoices.filter((i) => i.status === 'open').length,
|
||||
overdue: invoices.filter((i) => i.status === 'overdue').length,
|
||||
paid: invoices.filter((i) => i.status === 'paid').length
|
||||
};
|
||||
|
||||
const tabKey = TABS[tab].key;
|
||||
const filtered = useMemo(
|
||||
() => invoices.filter((i) => (tabKey === 'all' ? true : i.status === tabKey)),
|
||||
[tabKey]
|
||||
);
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Invoice"
|
||||
breadcrumbs={[{ label: 'Invoice' }]}
|
||||
action={
|
||||
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} onClick={() => setDateOpen(true)} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
{from.format('MMM DD')} – {to.format('MMM DD')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Billed" value={inr(totals.billed)} icon={ReceiptLongOutlinedIcon} caption="All invoices" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Paid" value={inr(totals.paid)} icon={CheckCircleOutlineIcon} color="success" caption={`${counts.paid} invoices`} /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending / Open" value={inr(totals.open)} icon={HourglassEmptyOutlinedIcon} color="warning" caption={`${counts.open} invoices`} /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Overdue" value={inr(totals.overdue)} icon={ErrorOutlineOutlinedIcon} color="error" caption={`${counts.overdue} invoices`} /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Card sx={{ mt: 1.5 }}>
|
||||
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
|
||||
{TABS.map((t, i) => (
|
||||
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Invoice Id</TableCell>
|
||||
<TableCell>Invoice Date</TableCell>
|
||||
<TableCell>Due Date</TableCell>
|
||||
<TableCell align="center">Count</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((row, idx) => (
|
||||
<TableRow key={row.id} hover>
|
||||
<TableCell>{page * rpp + idx + 1}</TableCell>
|
||||
<TableCell>{row.client}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/invoice/${row.id}`)}>{row.invoiceId}</TableCell>
|
||||
<TableCell>{row.invoiceDate}</TableCell>
|
||||
<TableCell>{row.dueDate}</TableCell>
|
||||
<TableCell align="center">{row.count}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
|
||||
<TableCell><StatusChip status={row.status} /></TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="Preview">
|
||||
<IconButton size="small" color="primary" onClick={() => navigate(`/invoice/${row.id}`)}>
|
||||
<VisibilityOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Dialog open={dateOpen} onClose={() => setDateOpen(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Filter by Date</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Select a date range to filter invoices.
|
||||
</Typography>
|
||||
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
||||
<DatePicker label="From Date" value={from} onChange={(v) => v && setFrom(v)} slotProps={{ textField: { fullWidth: true } }} />
|
||||
<DatePicker label="To Date" value={to} onChange={(v) => v && setTo(v)} slotProps={{ textField: { fullWidth: true } }} />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setDateOpen(false)} color="inherit">Cancel</Button>
|
||||
<Button variant="contained" onClick={() => setDateOpen(false)}>Apply</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
src/pages/maintenance/ComingSoon.jsx
Normal file
80
src/pages/maintenance/ComingSoon.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, Stack, Typography, TextField, Button } from '@mui/material';
|
||||
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
const COUNTDOWN = [
|
||||
{ label: 'Days', value: 12 },
|
||||
{ label: 'Hours', value: 8 },
|
||||
{ label: 'Minutes', value: 45 },
|
||||
{ label: 'Seconds', value: 30 }
|
||||
];
|
||||
|
||||
export default function ComingSoon() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3} alignItems="center" sx={{ maxWidth: 620, width: '100%' }}>
|
||||
<Logo />
|
||||
<Typography variant="h2" sx={{ fontWeight: 800, color: 'grey.800' }}>Coming Soon</Typography>
|
||||
<Typography variant="body1" color="text.secondary">Something new is on its way</Typography>
|
||||
|
||||
{/* Countdown */}
|
||||
<Stack direction="row" spacing={{ xs: 1, sm: 2 }} alignItems="center" justifyContent="center">
|
||||
{COUNTDOWN.map((c, i) => (
|
||||
<Stack key={c.label} direction="row" spacing={{ xs: 1, sm: 2 }} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 64, sm: 84 },
|
||||
py: 2,
|
||||
borderRadius: 2,
|
||||
bgcolor: 'primary.lighter',
|
||||
border: 1,
|
||||
borderColor: 'primary.light'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" sx={{ fontWeight: 800, color: 'primary.main', lineHeight: 1 }}>
|
||||
{String(c.value).padStart(2, '0')}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
{c.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
{i < COUNTDOWN.length - 1 && (
|
||||
<Typography variant="h2" sx={{ fontWeight: 800, color: 'primary.main' }}>:</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Subscribe */}
|
||||
<Box sx={{ width: '100%', maxWidth: 480 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
||||
Be the first to be notified when Doormile launches.
|
||||
</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Button variant="contained" sx={{ whiteSpace: 'nowrap', px: 3 }}>Notify Me</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
34
src/pages/maintenance/Error404.jsx
Normal file
34
src/pages/maintenance/Error404.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Stack, Typography, Button } from '@mui/material';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
|
||||
export default function Error404() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography sx={{ fontWeight: 900, fontSize: { xs: '6rem', md: '9rem' }, lineHeight: 1, color: 'primary.main' }}>
|
||||
404
|
||||
</Typography>
|
||||
<Box sx={{ width: 64, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
|
||||
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Page Not Found</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
|
||||
The page you are looking for was moved, removed, renamed, or might never exist!
|
||||
</Typography>
|
||||
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
|
||||
Back To Home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
34
src/pages/maintenance/Error500.jsx
Normal file
34
src/pages/maintenance/Error500.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Stack, Typography, Button } from '@mui/material';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
|
||||
export default function Error500() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography sx={{ fontWeight: 900, fontSize: { xs: '6rem', md: '9rem' }, lineHeight: 1, color: 'primary.main' }}>
|
||||
500
|
||||
</Typography>
|
||||
<Box sx={{ width: 64, height: 4, borderRadius: 2, bgcolor: 'primary.main' }} />
|
||||
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Internal Server Error</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
|
||||
Server error 500. We are fixing the problem. Please try again at a later stage.
|
||||
</Typography>
|
||||
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
|
||||
Back To Home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
45
src/pages/maintenance/UnderConstruction.jsx
Normal file
45
src/pages/maintenance/UnderConstruction.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Stack, Typography, Button } from '@mui/material';
|
||||
import ConstructionOutlinedIcon from '@mui/icons-material/ConstructionOutlined';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
|
||||
export default function UnderConstruction() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 110,
|
||||
height: 110,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'primary.lighter',
|
||||
color: 'primary.main',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<ConstructionOutlinedIcon sx={{ fontSize: 56 }} />
|
||||
</Box>
|
||||
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>Under Construction</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 460 }}>
|
||||
Hey! Please check out this site later. We are doing some maintenance on it right now.
|
||||
</Typography>
|
||||
<Button variant="contained" size="large" startIcon={<HomeOutlinedIcon />} onClick={() => navigate('/dashboard')} sx={{ mt: 1 }}>
|
||||
Back To Home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
135
src/pages/orders/AssignOrders.jsx
Normal file
135
src/pages/orders/AssignOrders.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Stack, Button, IconButton, Typography, Box, TextField, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
|
||||
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
||||
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { orders, riders } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const ZONES = ['Zone A', 'Zone B', 'Zone C', 'Zone D'];
|
||||
const PROFIT = [42, 58, 36, 50, 28, 64];
|
||||
|
||||
// derive assignment rows from orders + riders mock
|
||||
const ROWS = orders.slice(0, 6).map((o, i) => ({
|
||||
...o,
|
||||
zone: ZONES[i % ZONES.length],
|
||||
rider: riders[i % riders.length].name,
|
||||
profit: PROFIT[i % PROFIT.length]
|
||||
}));
|
||||
|
||||
export default function AssignOrders() {
|
||||
const navigate = useNavigate();
|
||||
const [payment, setPayment] = useState('all');
|
||||
const [rider, setRider] = useState('auto');
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<IconButton onClick={() => navigate('/orders')} size="small"><ArrowBackIcon /></IconButton>
|
||||
<span>Assign Orders</span>
|
||||
</Stack>
|
||||
}
|
||||
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Assign Orders' }]}
|
||||
action={
|
||||
<Button variant="outlined" startIcon={<AutorenewIcon />}>Re-Assign</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Orders" value={8} icon={Inventory2OutlinedIcon} caption="To assign" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Riders" value={5} icon={TwoWheelerOutlinedIcon} color="info" caption="Available" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Zones" value={4} icon={MapOutlinedIcon} color="warning" caption="Covered" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Kilometer" value={64} icon={RouteOutlinedIcon} color="success" caption="Total route" /></Grid>
|
||||
</Grid>
|
||||
|
||||
<MainCard title="Assignment Plan" noPadding sx={{ mt: 1.5 }}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Zone</TableCell>
|
||||
<TableCell>Tenant</TableCell>
|
||||
<TableCell>Order Location</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Delivery</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell align="right">Profit</TableCell>
|
||||
<TableCell align="right">Charges</TableCell>
|
||||
<TableCell align="right">KMS</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ROWS.map((r) => (
|
||||
<TableRow key={r.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.id}</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={r.zone} sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />
|
||||
</TableCell>
|
||||
<TableCell>{r.tenant}</TableCell>
|
||||
<TableCell>{r.location}</TableCell>
|
||||
<TableCell>{r.pickup}</TableCell>
|
||||
<TableCell>{r.drop}</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{r.notes || '—'}</Typography></TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<UserAvatar name={r.rider} size={28} />
|
||||
<Typography variant="body2">{r.rider}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell><StatusChip status={r.payment} /></TableCell>
|
||||
<TableCell align="right" sx={{ color: 'success.main', fontWeight: 600 }}>{inr(r.profit)}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.charges)}</TableCell>
|
||||
<TableCell align="right">{r.kms}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</MainCard>
|
||||
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
<Grid container spacing={2.5} alignItems="center">
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField select fullWidth size="small" label="Choose Payment" value={payment} onChange={(e) => setPayment(e.target.value)}>
|
||||
<MenuItem value="all">All Payments</MenuItem>
|
||||
<MenuItem value="prepaid">Prepaid</MenuItem>
|
||||
<MenuItem value="cod">COD</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<TextField select fullWidth size="small" label="Choose Rider" value={rider} onChange={(e) => setRider(e.target.value)}>
|
||||
<MenuItem value="auto">Auto Assign</MenuItem>
|
||||
{riders.map((rd) => <MenuItem key={rd.id} value={rd.id}>{rd.name}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
<Button variant="outlined" startIcon={<ArrowBackIcon />} onClick={() => navigate('/orders')}>Back</Button>
|
||||
<Button variant="contained" startIcon={<PersonAddAltOutlinedIcon />} onClick={() => navigate('/orders')}>
|
||||
Assign Orders
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
254
src/pages/orders/CreateMultipleOrders.jsx
Normal file
254
src/pages/orders/CreateMultipleOrders.jsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box,
|
||||
RadioGroup, Radio, FormControlLabel, FormControl, FormLabel, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, ListItemButton,
|
||||
ListItemIcon, ListItemText, Checkbox, InputAdornment
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
import UploadFileOutlinedIcon from '@mui/icons-material/UploadFileOutlined';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { locations, tenantsList, tenants, customers } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00'];
|
||||
|
||||
// derive table rows from the customers mock
|
||||
const buildRows = () =>
|
||||
customers.slice(0, 4).map((c, i) => ({
|
||||
id: c.id,
|
||||
customer: c.name,
|
||||
address: c.address,
|
||||
qty: [2, 1, 3, 1][i],
|
||||
cash: [0, 1299, 0, 450][i],
|
||||
kms: [4.2, 12.8, 6.5, 9.1][i],
|
||||
charge: [86, 142, 98, 110][i]
|
||||
}));
|
||||
|
||||
export default function CreateMultipleOrders() {
|
||||
const navigate = useNavigate();
|
||||
const [date, setDate] = useState(dayjs());
|
||||
const [slot, setSlot] = useState('');
|
||||
const [dropMode, setDropMode] = useState('csv');
|
||||
const [rows, setRows] = useState(buildRows);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [picked, setPicked] = useState([]);
|
||||
|
||||
const totals = useMemo(
|
||||
() =>
|
||||
rows.reduce(
|
||||
(acc, r) => ({
|
||||
qty: acc.qty + r.qty,
|
||||
cash: acc.cash + r.cash,
|
||||
kms: +(acc.kms + r.kms).toFixed(1),
|
||||
charge: acc.charge + r.charge
|
||||
}),
|
||||
{ qty: 0, cash: 0, kms: 0, charge: 0 }
|
||||
),
|
||||
[rows]
|
||||
);
|
||||
|
||||
const removeRow = (id) => setRows((p) => p.filter((r) => r.id !== id));
|
||||
|
||||
const filteredCustomers = customers.filter(
|
||||
(c) => !search || `${c.name} ${c.location}`.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
const toggle = (id) => setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
|
||||
|
||||
const addSelected = () => {
|
||||
const existing = new Set(rows.map((r) => r.id));
|
||||
const additions = customers
|
||||
.filter((c) => picked.includes(c.id) && !existing.has(c.id))
|
||||
.map((c) => ({ id: c.id, customer: c.name, address: c.address, qty: 1, cash: 0, kms: 5, charge: 90 }));
|
||||
setRows((p) => [...p, ...additions]);
|
||||
setPicked([]);
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create Multiple Orders"
|
||||
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Create Multiple Orders' }]}
|
||||
/>
|
||||
|
||||
<Stack spacing={2.5}>
|
||||
<Card>
|
||||
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
|
||||
{/* top selects */}
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField select fullWidth label="App Location" defaultValue="">
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField select fullWidth label="Choose Client" defaultValue="">
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField select fullWidth label="Business Location" defaultValue="">
|
||||
{tenants.map((t) => <MenuItem key={t.id} value={t.id}>{t.address}, {t.city}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField fullWidth label="Pickup Location" value="Koramangala Hub, Bengaluru" InputProps={{ readOnly: true }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<DatePicker label="Delivery Date" value={date} onChange={setDate} slotProps={{ textField: { fullWidth: true } }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField select fullWidth label="Pickup Time Slot" value={slot} onChange={(e) => setSlot(e.target.value)}>
|
||||
{TIME_SLOTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>Drop</Typography>
|
||||
<Divider sx={{ mt: 1, mb: 2 }} />
|
||||
<FormControl>
|
||||
<FormLabel sx={{ mb: 1, fontSize: 14 }}>Add drop points using</FormLabel>
|
||||
<RadioGroup row value={dropMode} onChange={(e) => setDropMode(e.target.value)}>
|
||||
<FormControlLabel value="csv" control={<Radio />} label="Excel / CSV" />
|
||||
<FormControlLabel value="selection" control={<Radio />} label="Selection" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
{dropMode === 'csv' ? (
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
|
||||
<Button variant="contained" component="label" startIcon={<UploadFileOutlinedIcon />}>
|
||||
Upload CSV
|
||||
<input type="file" hidden accept=".csv,.xlsx" />
|
||||
</Button>
|
||||
<Chip
|
||||
label="orders_batch.csv"
|
||||
onDelete={() => {}}
|
||||
sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<Button variant="outlined" startIcon={<PersonAddAltOutlinedIcon />} onClick={() => setDialogOpen(true)}>
|
||||
Select Customers
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<MainCard title="Drop Points" noPadding>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell align="center">Quantity</TableCell>
|
||||
<TableCell align="right">Cash Collect</TableCell>
|
||||
<TableCell align="right">Kms</TableCell>
|
||||
<TableCell align="right">Charge</TableCell>
|
||||
<TableCell align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((r, i) => (
|
||||
<TableRow key={r.id} hover>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<UserAvatar name={r.customer} size={32} />
|
||||
<Typography variant="body2">{r.customer}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{r.address}</Typography></TableCell>
|
||||
<TableCell align="center">{r.qty}</TableCell>
|
||||
<TableCell align="right">{r.cash ? inr(r.cash) : '—'}</TableCell>
|
||||
<TableCell align="right">{r.kms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.charge)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="Remove">
|
||||
<IconButton size="small" color="error" onClick={() => removeRow(r.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow sx={{ '& td': { fontWeight: 700, borderTop: 2, borderColor: 'divider' } }}>
|
||||
<TableCell colSpan={3}>Total</TableCell>
|
||||
<TableCell align="center">{totals.qty}</TableCell>
|
||||
<TableCell align="right">{inr(totals.cash)}</TableCell>
|
||||
<TableCell align="right">{totals.kms}</TableCell>
|
||||
<TableCell align="right">{inr(totals.charge)}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</MainCard>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<TextField fullWidth label="Notes" placeholder="Notes for this batch of orders" multiline minRows={3} />
|
||||
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders')}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
{/* Customer selection dialog */}
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} fullWidth maxWidth="sm">
|
||||
<DialogTitle>Select Customers</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Search customers…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
sx={{ mb: 1.5 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<List disablePadding>
|
||||
{filteredCustomers.map((c) => (
|
||||
<ListItem key={c.id} disablePadding>
|
||||
<ListItemButton onClick={() => toggle(c.id)}>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
<Checkbox edge="start" checked={picked.includes(c.id)} tabIndex={-1} disableRipple />
|
||||
</ListItemIcon>
|
||||
<UserAvatar name={c.name} size={32} sx={{ mr: 1.5 }} />
|
||||
<ListItemText primary={c.name} secondary={`${c.address} · ${c.location}`} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={addSelected} disabled={!picked.length}>
|
||||
Add Selected{picked.length ? ` (${picked.length})` : ''}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
142
src/pages/orders/CreateOrder.jsx
Normal file
142
src/pages/orders/CreateOrder.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider,
|
||||
Autocomplete, FormControlLabel, Checkbox, Box
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { locations, tenantsList, tenants } from '@/data/mock';
|
||||
|
||||
const TIME_SLOTS = ['09:00 - 11:00', '11:00 - 13:00', '13:00 - 15:00', '15:00 - 17:00', '17:00 - 19:00'];
|
||||
|
||||
const SectionTitle = ({ children }) => (
|
||||
<>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>{children}</Typography>
|
||||
<Divider sx={{ mt: 1, mb: 2.5 }} />
|
||||
</>
|
||||
);
|
||||
|
||||
function AddressFields({ saveForLater, onSaveForLater }) {
|
||||
return (
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Contact Name" placeholder="Enter contact name" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Contact Number" placeholder="+91 ..." />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth label="Address" placeholder="Enter full address" multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Door No / Street" placeholder="e.g. 24, 1st Cross" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Location" defaultValue="">
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Postcode" placeholder="e.g. 560102" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Landmark" placeholder="Nearby landmark" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={saveForLater} onChange={(e) => onSaveForLater(e.target.checked)} />}
|
||||
label="Save for later"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreateOrder() {
|
||||
const navigate = useNavigate();
|
||||
const [deliveryDate, setDeliveryDate] = useState(dayjs());
|
||||
const [slot, setSlot] = useState('');
|
||||
const [savePickup, setSavePickup] = useState(false);
|
||||
const [saveDrop, setSaveDrop] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create Order"
|
||||
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: 'Create Order' }]}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
|
||||
{/* Top row */}
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Autocomplete
|
||||
options={locations}
|
||||
renderInput={(params) => <TextField {...params} label="Location" placeholder="Select location" />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Autocomplete
|
||||
options={tenantsList}
|
||||
renderInput={(params) => <TextField {...params} label="Client" placeholder="Select client" />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Autocomplete
|
||||
options={tenants.map((t) => `${t.name} — ${t.address}`)}
|
||||
renderInput={(params) => <TextField {...params} label="Business Location" placeholder="Select business location" />}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Pickup Details</SectionTitle>
|
||||
<AddressFields saveForLater={savePickup} onSaveForLater={setSavePickup} />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Drop Details</SectionTitle>
|
||||
<AddressFields saveForLater={saveDrop} onSaveForLater={setSaveDrop} />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Schedule</SectionTitle>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<DatePicker
|
||||
label="Delivery Date"
|
||||
value={deliveryDate}
|
||||
onChange={setDeliveryDate}
|
||||
slotProps={{ textField: { fullWidth: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Pickup Time Slot" value={slot} onChange={(e) => setSlot(e.target.value)}>
|
||||
{TIME_SLOTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth label="Notes" placeholder="Special instructions for this order" multiline minRows={3} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mt: 3 }} />
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate('/orders')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders')}>
|
||||
Create Order
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
170
src/pages/orders/OrderDetails.jsx
Normal file
170
src/pages/orders/OrderDetails.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, IconButton, Avatar,
|
||||
Step, Stepper, StepLabel, StepConnector, stepConnectorClasses, styled, Chip
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import CallOutlinedIcon from '@mui/icons-material/CallOutlined';
|
||||
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { orders, orderTimeline, deliveries } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const RedConnector = styled(StepConnector)(({ theme }) => ({
|
||||
[`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.grey[300], borderLeftWidth: 2, minHeight: 28 },
|
||||
[`&.${stepConnectorClasses.active} .${stepConnectorClasses.line}, &.${stepConnectorClasses.completed} .${stepConnectorClasses.line}`]:
|
||||
{ borderColor: theme.palette.primary.main }
|
||||
}));
|
||||
|
||||
function Dot({ active }) {
|
||||
return (
|
||||
<Box sx={{ width: 26, height: 26, borderRadius: '50%', bgcolor: active ? 'primary.main' : 'grey.300', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{active ? <CheckIcon sx={{ fontSize: 16 }} /> : <Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#fff' }} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrderDetails() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const order = orders.find((o) => o.id === id) || orders[1];
|
||||
const delivery = deliveries.find((d) => d.id === order.id) || deliveries[1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<IconButton onClick={() => navigate('/orders')} size="small"><ArrowBackIcon /></IconButton>
|
||||
<span>Order Details</span>
|
||||
</Stack>
|
||||
}
|
||||
breadcrumbs={[{ label: 'Orders', to: '/orders' }, { label: order.id }]}
|
||||
action={
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
<Button variant="outlined" color="error">Cancel Order</Button>
|
||||
<Button variant="contained" startIcon={<EditOutlinedIcon />}>Edit Order</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
{/* Left column */}
|
||||
<Grid item xs={12} md={5} lg={4}>
|
||||
<Stack spacing={2.5}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Order ID</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700 }}>{order.id}</Typography>
|
||||
</Box>
|
||||
<StatusChip status={order.status} size="medium" />
|
||||
</Stack>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Stack spacing={1.25}>
|
||||
<Row label="Created" value={order.date} />
|
||||
<Row label="Tenant" value={order.tenant} />
|
||||
<Row label="Payment" value={<StatusChip status={order.payment} />} />
|
||||
<Row label="Distance" value={`${order.kms} km`} />
|
||||
<Row label="Amount" value={<Typography variant="h5" color="primary.main">{inr(order.charges)}</Typography>} />
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<MainCard title="Customer">
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}>
|
||||
<UserAvatar name={order.customer} size={48} />
|
||||
<Box>
|
||||
<Typography variant="subtitle1">{order.customer}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Recipient</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack spacing={1.25}>
|
||||
<IconRow icon={PhoneOutlinedIcon} text="+91 98765 43210" />
|
||||
<IconRow icon={LocationOnOutlinedIcon} text={`${order.drop}, ${order.location}`} />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
||||
<MainCard title="Delivery Timeline">
|
||||
<Stepper orientation="vertical" connector={<RedConnector />} sx={{ ml: 0.5 }}>
|
||||
{orderTimeline.map((s) => (
|
||||
<Step key={s.label} active completed={s.done}>
|
||||
<StepLabel StepIconComponent={() => <Dot active={s.done} />}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: s.done ? 600 : 500, color: s.done ? 'text.primary' : 'text.secondary' }}>{s.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{s.time}</Typography>
|
||||
</Stack>
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</MainCard>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Right column */}
|
||||
<Grid item xs={12} md={7} lg={8}>
|
||||
<Stack spacing={2.5}>
|
||||
<MainCard title="Live Tracking" noPadding>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<MapPlaceholder height={380} label="In Transit" />
|
||||
<Stack direction="row" spacing={3} sx={{ mt: 2 }}>
|
||||
<RouteEnd color="#00A854" title="Pickup" text={order.pickup} />
|
||||
<RouteEnd color="#C01227" title="Drop" text={order.drop} />
|
||||
</Stack>
|
||||
</Box>
|
||||
</MainCard>
|
||||
|
||||
<MainCard title="Assigned Rider">
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ sm: 'center' }} justifyContent="space-between">
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<UserAvatar name={delivery.rider} size={56} />
|
||||
<Box>
|
||||
<Typography variant="subtitle1">{delivery.rider}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Rider · +91 98450 11223</Typography>
|
||||
<Box sx={{ mt: 0.5 }}><Chip size="small" label="On the way" sx={{ bgcolor: 'info.lighter', color: 'info.dark' }} /></Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button variant="contained" startIcon={<CallOutlinedIcon />}>Call Rider</Button>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Row = ({ label, value }) => (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="body2" color="text.secondary">{label}</Typography>
|
||||
{typeof value === 'string' ? <Typography variant="subtitle2">{value}</Typography> : value}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const IconRow = ({ icon: Icon, text }) => (
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<Icon sx={{ fontSize: 18, color: 'grey.500' }} />
|
||||
<Typography variant="body2">{text}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const RouteEnd = ({ color, title, text }) => (
|
||||
<Stack direction="row" spacing={1} alignItems="flex-start">
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '50%', bgcolor: color, mt: 0.5 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">{title}</Typography>
|
||||
<Typography variant="subtitle2">{text}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
195
src/pages/orders/OrdersList.jsx
Normal file
195
src/pages/orders/OrdersList.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, IconButton,
|
||||
Tooltip, TablePagination, Typography, SpeedDial, SpeedDialAction, Chip
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
|
||||
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { orders } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'created', label: 'Created' },
|
||||
{ key: 'pending', label: 'Pending' },
|
||||
{ key: 'delivered', label: 'Delivered' },
|
||||
{ key: 'cancelled', label: 'Cancelled' }
|
||||
];
|
||||
|
||||
export default function OrdersList() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tab, setTab] = useState(0);
|
||||
const [search, setSearch] = useState(searchParams.get('q') || '');
|
||||
const [tenant, setTenant] = useState('all');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(5);
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
const tabKey = TABS[tab].key;
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
orders.filter((o) => {
|
||||
const matchTab = tabKey === 'created' ? true : o.status === tabKey;
|
||||
const matchTenant = tenant === 'all' || o.tenant === tenant;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase());
|
||||
return matchTab && matchTenant && matchSearch;
|
||||
}),
|
||||
[tabKey, tenant, search]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
|
||||
const counts = {
|
||||
created: orders.length,
|
||||
pending: orders.filter((o) => o.status === 'pending').length,
|
||||
delivered: orders.filter((o) => o.status === 'delivered').length,
|
||||
cancelled: orders.filter((o) => o.status === 'cancelled').length
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Orders"
|
||||
breadcrumbs={[{ label: 'Orders' }]}
|
||||
action={
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
|
||||
<Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}>
|
||||
Create Multiple
|
||||
</Button>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>
|
||||
Create Order
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Created Orders" value={counts.created} icon={Inventory2OutlinedIcon} caption="100%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending Orders" value={counts.pending} icon={HourglassEmptyOutlinedIcon} color="warning" caption="25%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered Orders" value={counts.delivered} icon={CheckCircleOutlineIcon} color="success" caption="25%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cancelled Orders" value={counts.cancelled} icon={CancelOutlinedIcon} color="error" caption="12.5%" /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Card sx={{ mt: 1.5 }}>
|
||||
{/* filter toolbar */}
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||
<TextField
|
||||
size="small" placeholder="Search orders…" value={search} onChange={(e) => setSearch(e.target.value)}
|
||||
sx={{ minWidth: 240 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button variant="outlined" size="medium" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
Jun 01 – Jun 05
|
||||
</Button>
|
||||
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
|
||||
<MenuItem value="all">All Tenants</MenuItem>
|
||||
{[...new Set(orders.map((o) => o.tenant))].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{[...new Set(orders.map((o) => o.location))].map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
|
||||
{TABS.map((t, i) => (
|
||||
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{selected.length > 0 && (
|
||||
<Stack direction="row" alignItems="center" spacing={2} sx={{ px: 2, py: 1, bgcolor: 'primary.lighter' }}>
|
||||
<Typography variant="subtitle2" color="primary.dark">{selected.length} selected</Typography>
|
||||
<Button size="small" color="error" startIcon={<DeleteOutlineIcon />}>Delete</Button>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={selected.length > 0 && selected.length < paged.length}
|
||||
checked={paged.length > 0 && selected.length === paged.length}
|
||||
onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Tenant</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Drop</TableCell>
|
||||
<TableCell align="center">QTY</TableCell>
|
||||
<TableCell align="right">COD</TableCell>
|
||||
<TableCell align="right">KMS</TableCell>
|
||||
<TableCell align="right">Charges</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="center">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((o) => (
|
||||
<TableRow key={o.id} hover selected={selected.includes(o.id)}>
|
||||
<TableCell padding="checkbox"><Checkbox checked={selected.includes(o.id)} onChange={() => toggle(o.id)} /></TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/orders/${o.id}`)}>{o.id}</TableCell>
|
||||
<TableCell>{o.tenant}</TableCell>
|
||||
<TableCell>{o.location}</TableCell>
|
||||
<TableCell>{o.pickup}</TableCell>
|
||||
<TableCell>{o.drop}</TableCell>
|
||||
<TableCell align="center">{o.qty}</TableCell>
|
||||
<TableCell align="right">{o.cod ? inr(o.cod) : '—'}</TableCell>
|
||||
<TableCell align="right">{o.kms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{o.notes || '—'}</Typography></TableCell>
|
||||
<TableCell><StatusChip status={o.status} /></TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="View"><IconButton size="small" onClick={() => navigate(`/orders/${o.id}`)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Edit"><IconButton size="small"><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<SpeedDial ariaLabel="Order actions" icon={<TuneIcon />} sx={{ position: 'fixed', bottom: 28, right: 28 }} FabProps={{ color: 'primary' }}>
|
||||
<SpeedDialAction icon={<AutoAwesomeOutlinedIcon />} tooltipTitle="AI Optimisation" onClick={() => navigate('/orders/assign')} />
|
||||
<SpeedDialAction icon={<PersonAddAltOutlinedIcon />} tooltipTitle="Manual Assign" onClick={() => navigate('/orders/assign')} />
|
||||
<SpeedDialAction icon={<DeleteOutlineIcon />} tooltipTitle="Delete" />
|
||||
</SpeedDial>
|
||||
</>
|
||||
);
|
||||
}
|
||||
190
src/pages/reports/OrdersDetails.jsx
Normal file
190
src/pages/reports/OrdersDetails.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, IconButton, Tooltip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Typography,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import { ordersDetailReport, locations, tenantsList } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled'];
|
||||
|
||||
export default function OrdersDetails() {
|
||||
const [location, setLocation] = useState('all');
|
||||
const [tenant, setTenant] = useState('all');
|
||||
const [loc2, setLoc2] = useState('all');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(10);
|
||||
const [mapRow, setMapRow] = useState(null);
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
ordersDetailReport.filter((o) => {
|
||||
const matchStatus = status === 'all' || o.status === status;
|
||||
const matchTenant = tenant === 'all' || o.client === tenant;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase());
|
||||
return matchStatus && matchTenant && matchSearch;
|
||||
}),
|
||||
[status, tenant, search]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Orders Details"
|
||||
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Details' }]}
|
||||
action={
|
||||
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap>
|
||||
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
|
||||
<MenuItem value="all">All Tenants</MenuItem>
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
Jun 01 – Jun 05
|
||||
</Button>
|
||||
<TextField select size="small" value={status} onChange={(e) => { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status">
|
||||
{STATUSES.map((s) => <MenuItem key={s} value={s}>{s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField
|
||||
size="small" placeholder="Search orders…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button variant="contained" startIcon={<FileDownloadOutlinedIcon />} onClick={() => setExportOpen(true)}>
|
||||
Export Report
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell />
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Drop</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="center">Assigned</TableCell>
|
||||
<TableCell align="center">Accepted</TableCell>
|
||||
<TableCell align="center">Arrived</TableCell>
|
||||
<TableCell align="center">Picked</TableCell>
|
||||
<TableCell align="center">Active</TableCell>
|
||||
<TableCell align="center">Delivered</TableCell>
|
||||
<TableCell align="center">Cancelled</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell align="right">KMS</TableCell>
|
||||
<TableCell align="right">Charges</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((o, i) => (
|
||||
<TableRow key={o.id} hover>
|
||||
<TableCell>{page * rpp + i + 1}</TableCell>
|
||||
<TableCell padding="checkbox">
|
||||
<Tooltip title="View route">
|
||||
<IconButton size="small" color="primary" onClick={() => setMapRow(o)}><MapOutlinedIcon fontSize="small" /></IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>{o.client}</TableCell>
|
||||
<TableCell>{o.pickup}</TableCell>
|
||||
<TableCell>{o.drop}</TableCell>
|
||||
<TableCell><StatusChip status={o.status} /></TableCell>
|
||||
<TableCell align="center">{o.assigned}</TableCell>
|
||||
<TableCell align="center">{o.accepted}</TableCell>
|
||||
<TableCell align="center">{o.arrived}</TableCell>
|
||||
<TableCell align="center">{o.picked}</TableCell>
|
||||
<TableCell align="center">{o.active}</TableCell>
|
||||
<TableCell align="center">{o.delivered}</TableCell>
|
||||
<TableCell align="center">{o.cancelled}</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{o.notes || '—'}</Typography></TableCell>
|
||||
<TableCell align="right">{o.kms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[10, 25, 50]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Map dialog */}
|
||||
<Dialog open={!!mapRow} onClose={() => setMapRow(null)} fullScreen>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
Route — {mapRow?.id}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{mapRow?.pickup} → {mapRow?.drop}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton onClick={() => setMapRow(null)}><CloseIcon /></IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<MapPlaceholder height={520} label="Route" />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Export dialog */}
|
||||
<Dialog open={exportOpen} onClose={() => setExportOpen(false)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Export Report</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
The export will include {filtered.length} record(s) matching the current filters:
|
||||
</Typography>
|
||||
<Grid container spacing={1.5}>
|
||||
<Filter label="Location" value={location === 'all' ? 'All Locations' : location} />
|
||||
<Filter label="Tenant" value={tenant === 'all' ? 'All Tenants' : tenant} />
|
||||
<Filter label="Location (2)" value={loc2 === 'all' ? 'All Locations' : loc2} />
|
||||
<Filter label="Status" value={status === 'all' ? 'All Status' : status} />
|
||||
<Filter label="Date Range" value="Jun 01 – Jun 05" />
|
||||
<Filter label="Search" value={search || '—'} />
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setExportOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" startIcon={<FileDownloadOutlinedIcon />} onClick={() => setExportOpen(false)}>Download CSV</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Filter({ label, value }) {
|
||||
return (
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ textTransform: 'capitalize' }}>{value}</Typography>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
195
src/pages/reports/OrdersSummary.jsx
Normal file
195
src/pages/reports/OrdersSummary.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
import {
|
||||
Grid, Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography
|
||||
} from '@mui/material';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { ordersSummary, locations, tenantsList } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
// Show 0 in red, anything else normally.
|
||||
function NumCell({ value, align = 'center', bold = false }) {
|
||||
const zero = Number(value) === 0;
|
||||
return (
|
||||
<TableCell align={align} sx={{ fontWeight: bold ? 700 : 400, color: zero ? 'error.main' : 'inherit' }}>
|
||||
{value}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OrdersSummary() {
|
||||
const [open, setOpen] = useState({});
|
||||
const [location, setLocation] = useState('all');
|
||||
const [tenant, setTenant] = useState('all');
|
||||
const [loc2, setLoc2] = useState('all');
|
||||
|
||||
const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] }));
|
||||
|
||||
const totals = ordersSummary.reduce(
|
||||
(a, r) => ({
|
||||
oPending: a.oPending + r.orders.pending,
|
||||
oCancelled: a.oCancelled + r.orders.cancelled,
|
||||
oCompleted: a.oCompleted + r.orders.completed,
|
||||
dPending: a.dPending + r.deliveries.pending,
|
||||
dCancelled: a.dCancelled + r.deliveries.cancelled,
|
||||
dCompleted: a.dCompleted + r.deliveries.completed,
|
||||
collection: a.collection + r.collection,
|
||||
kms: a.kms + r.kms,
|
||||
actualKms: a.actualKms + r.actualKms,
|
||||
amount: a.amount + r.amount
|
||||
}),
|
||||
{ oPending: 0, oCancelled: 0, oCompleted: 0, dPending: 0, dCancelled: 0, dCompleted: 0, collection: 0, kms: 0, actualKms: 0, amount: 0 }
|
||||
);
|
||||
|
||||
const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Orders Summary"
|
||||
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Summary' }]}
|
||||
action={
|
||||
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
Jun 01 – Jun 05
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
|
||||
<MenuItem value="all">All Tenants</MenuItem>
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={headSx} rowSpan={2} />
|
||||
<TableCell sx={headSx} rowSpan={2}>#</TableCell>
|
||||
<TableCell sx={headSx} rowSpan={2}>Tenant / Location</TableCell>
|
||||
<TableCell sx={headSx} colSpan={3} align="center">Orders</TableCell>
|
||||
<TableCell sx={headSx} colSpan={3} align="center">Deliveries</TableCell>
|
||||
<TableCell sx={headSx} rowSpan={2} align="right">Collection Amt</TableCell>
|
||||
<TableCell sx={headSx} rowSpan={2} align="center">Kms / Actual</TableCell>
|
||||
<TableCell sx={headSx} rowSpan={2} align="right">Amount</TableCell>
|
||||
<TableCell sx={headSx} rowSpan={2} align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell sx={headSx} align="center">Pending</TableCell>
|
||||
<TableCell sx={headSx} align="center">Cancelled</TableCell>
|
||||
<TableCell sx={headSx} align="center">Completed</TableCell>
|
||||
<TableCell sx={headSx} align="center">Pending</TableCell>
|
||||
<TableCell sx={headSx} align="center">Cancelled</TableCell>
|
||||
<TableCell sx={headSx} align="center">Completed</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ordersSummary.map((r, idx) => (
|
||||
<Fragment key={r.id}>
|
||||
<TableRow hover>
|
||||
<TableCell padding="checkbox">
|
||||
<IconButton size="small" onClick={() => toggle(r.id)}>
|
||||
{open[r.id] ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{idx + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{r.tenant}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.location}</Typography>
|
||||
</TableCell>
|
||||
<NumCell value={r.orders.pending} />
|
||||
<NumCell value={r.orders.cancelled} />
|
||||
<NumCell value={r.orders.completed} />
|
||||
<NumCell value={r.deliveries.pending} />
|
||||
<NumCell value={r.deliveries.cancelled} />
|
||||
<NumCell value={r.deliveries.completed} />
|
||||
<TableCell align="right">{inr(r.collection)}</TableCell>
|
||||
<TableCell align="center">{r.kms} / {r.actualKms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.amount)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton size="small" onClick={() => toggle(r.id)}><VisibilityOutlinedIcon fontSize="small" /></IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} sx={{ py: 0, border: 0 }}>
|
||||
<Collapse in={!!open[r.id]} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ m: 1.5, p: 1.5, bgcolor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Riders</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={headSx}>#</TableCell>
|
||||
<TableCell sx={headSx}>Rider</TableCell>
|
||||
<TableCell sx={headSx} align="center">Orders</TableCell>
|
||||
<TableCell sx={headSx} align="center">Deliveries</TableCell>
|
||||
<TableCell sx={headSx} align="center">Pending</TableCell>
|
||||
<TableCell sx={headSx} align="center">Cancelled</TableCell>
|
||||
<TableCell sx={headSx} align="center">Completed</TableCell>
|
||||
<TableCell sx={headSx} align="right">Collection Amt</TableCell>
|
||||
<TableCell sx={headSx} align="center">Kms / Actual</TableCell>
|
||||
<TableCell sx={headSx} align="right">Charges</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{r.riders.map((rd, i) => (
|
||||
<TableRow key={rd.rider} hover>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>{rd.rider}</TableCell>
|
||||
<NumCell value={rd.orders} />
|
||||
<NumCell value={rd.deliveries} />
|
||||
<NumCell value={rd.pending} />
|
||||
<NumCell value={rd.cancelled} />
|
||||
<NumCell value={rd.completed} />
|
||||
<TableCell align="right">{inr(rd.collection)}</TableCell>
|
||||
<TableCell align="center">{rd.kms} / {rd.actualKms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(rd.charges)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* totals */}
|
||||
<TableRow sx={{ '& td': { bgcolor: 'primary.lighter', borderTop: 2, borderColor: 'primary.light' } }}>
|
||||
<TableCell />
|
||||
<TableCell colSpan={2} sx={{ fontWeight: 700 }}>Totals</TableCell>
|
||||
<NumCell value={totals.oPending} bold />
|
||||
<NumCell value={totals.oCancelled} bold />
|
||||
<NumCell value={totals.oCompleted} bold />
|
||||
<NumCell value={totals.dPending} bold />
|
||||
<NumCell value={totals.dCancelled} bold />
|
||||
<NumCell value={totals.dCompleted} bold />
|
||||
<TableCell align="right" sx={{ fontWeight: 700 }}>{inr(totals.collection)}</TableCell>
|
||||
<TableCell align="center" sx={{ fontWeight: 700 }}>{totals.kms} / {totals.actualKms}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700 }}>{inr(totals.amount)}</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
105
src/pages/reports/RidersLogs.jsx
Normal file
105
src/pages/reports/RidersLogs.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Box, Paper, Stack, Button, TextField, InputAdornment, IconButton, Checkbox, Typography, Divider, Tooltip
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import { ridersLive } from '@/data/mock';
|
||||
|
||||
// spread active riders across the map at distinct positions
|
||||
const POSITIONS = [
|
||||
{ x: '24%', y: '32%' },
|
||||
{ x: '58%', y: '28%' },
|
||||
{ x: '40%', y: '60%' },
|
||||
{ x: '72%', y: '55%' },
|
||||
{ x: '30%', y: '72%' },
|
||||
{ x: '64%', y: '78%' }
|
||||
];
|
||||
|
||||
export default function RidersLogs() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selected, setSelected] = useState(ridersLive.filter((r) => r.active).map((r) => r.userid));
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
ridersLive.filter((r) =>
|
||||
!search || [r.name, r.phone, r.userid].join(' ').toLowerCase().includes(search.toLowerCase())
|
||||
),
|
||||
[search]
|
||||
);
|
||||
|
||||
const allChecked = filtered.length > 0 && filtered.every((r) => selected.includes(r.userid));
|
||||
const someChecked = filtered.some((r) => selected.includes(r.userid)) && !allChecked;
|
||||
|
||||
const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
|
||||
const toggleAll = (checked) =>
|
||||
setSelected(checked ? [...new Set([...selected, ...filtered.map((r) => r.userid)])] : selected.filter((id) => !filtered.some((r) => r.userid === id)));
|
||||
|
||||
const mapRiders = ridersLive
|
||||
.filter((r) => r.active && selected.includes(r.userid))
|
||||
.map((r, i) => ({ ...POSITIONS[i % POSITIONS.length], active: true }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Riders Locations"
|
||||
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Locations' }]}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 2.5, alignItems: 'stretch' }}>
|
||||
{/* Left side panel */}
|
||||
<Paper variant="outlined" sx={{ width: { xs: '100%', md: 300 }, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<TextField
|
||||
fullWidth size="small" placeholder="Search Rider…" value={search} onChange={(e) => setSearch(e.target.value)}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Stack direction="row" alignItems="center" sx={{ px: 1, py: 0.5 }}>
|
||||
<Checkbox checked={allChecked} indeterminate={someChecked} onChange={(e) => toggleAll(e.target.checked)} />
|
||||
<Typography variant="subtitle2">All</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ pr: 1 }}>{selected.length} selected</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', maxHeight: { xs: 360, md: 620 } }}>
|
||||
{filtered.map((r) => (
|
||||
<Stack key={r.userid} direction="row" alignItems="flex-start" spacing={0.5} sx={{ px: 1, py: 1, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Checkbox size="small" checked={selected.includes(r.userid)} onChange={() => toggle(r.userid)} sx={{ mt: -0.5 }} />
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: r.active ? 'success.main' : 'grey.400', flexShrink: 0 }} />
|
||||
<Typography variant="subtitle2" noWrap>{r.name}</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{r.phone}</Typography>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="caption" color="text.secondary">{r.userid}</Typography>
|
||||
<Typography variant="caption" color={r.active ? 'success.main' : 'text.disabled'}>{r.lastLog}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', p: 2, textAlign: 'center' }}>No riders found</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Map area */}
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, p: 2, display: 'flex', flexDirection: 'column' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2 }}>
|
||||
<Tooltip title="Menu"><IconButton size="small"><MenuIcon /></IconButton></Tooltip>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.800', flexGrow: 1 }}>Riders Locations</Typography>
|
||||
<Button variant="contained" startIcon={<RefreshIcon />}>Refresh</Button>
|
||||
</Stack>
|
||||
<MapPlaceholder height={620} label="Riders Locations" showRoute={false} riders={mapRiders} />
|
||||
</Paper>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
167
src/pages/reports/RidersSummary.jsx
Normal file
167
src/pages/reports/RidersSummary.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
import {
|
||||
Card, Stack, Button, TextField, MenuItem, Box, Collapse, IconButton, Chip, Tooltip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography,
|
||||
Dialog, DialogTitle, DialogContent
|
||||
} from '@mui/material';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import RoomOutlinedIcon from '@mui/icons-material/RoomOutlined';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import { ridersSummary, locations } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
function NumCell({ value, align = 'center' }) {
|
||||
const zero = Number(value) === 0;
|
||||
return <TableCell align={align} sx={{ color: zero ? 'error.main' : 'inherit' }}>{value}</TableCell>;
|
||||
}
|
||||
|
||||
function KmsChips({ kms, actual }) {
|
||||
return (
|
||||
<Stack direction="row" spacing={0.5} justifyContent="center">
|
||||
<Chip size="small" label={`${kms} km`} sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />
|
||||
<Chip size="small" label={`${actual} km`} variant="outlined" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RidersSummary() {
|
||||
const [open, setOpen] = useState({});
|
||||
const [location, setLocation] = useState('all');
|
||||
const [mapRider, setMapRider] = useState(null);
|
||||
|
||||
const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] }));
|
||||
const totalAmount = ridersSummary.reduce((a, r) => a + r.amount, 0);
|
||||
|
||||
const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Riders Summary"
|
||||
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Summary' }]}
|
||||
action={
|
||||
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
|
||||
Jun 01 – Jun 05
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={headSx}>#</TableCell>
|
||||
<TableCell sx={headSx}>Rider</TableCell>
|
||||
<TableCell sx={headSx} align="center">Orders</TableCell>
|
||||
<TableCell sx={headSx} align="center">Pending</TableCell>
|
||||
<TableCell sx={headSx} align="center">Cancelled</TableCell>
|
||||
<TableCell sx={headSx} align="center">Delivered</TableCell>
|
||||
<TableCell sx={headSx} align="center">KMS</TableCell>
|
||||
<TableCell sx={headSx} align="right">Amount</TableCell>
|
||||
<TableCell sx={headSx} align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ridersSummary.map((r, idx) => (
|
||||
<Fragment key={r.id}>
|
||||
<TableRow hover>
|
||||
<TableCell>{idx + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<UserAvatar name={r.rider} size={32} />
|
||||
<Typography variant="subtitle2">{r.rider}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<NumCell value={r.orders} />
|
||||
<NumCell value={r.pending} />
|
||||
<NumCell value={r.cancelled} />
|
||||
<NumCell value={r.delivered} />
|
||||
<TableCell align="center"><KmsChips kms={r.kms} actual={r.actualKms} /></TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.amount)}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title={open[r.id] ? 'Collapse' : 'Expand'}>
|
||||
<IconButton size="small" onClick={() => toggle(r.id)}>
|
||||
{open[r.id] ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Location">
|
||||
<IconButton size="small" color="primary" onClick={() => setMapRider(r)}><RoomOutlinedIcon fontSize="small" /></IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} sx={{ py: 0, border: 0 }}>
|
||||
<Collapse in={!!open[r.id]} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ m: 1.5, p: 1.5, bgcolor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Client Summary</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={headSx}>#</TableCell>
|
||||
<TableCell sx={headSx}>Client</TableCell>
|
||||
<TableCell sx={headSx} align="center">All</TableCell>
|
||||
<TableCell sx={headSx} align="center">Pending</TableCell>
|
||||
<TableCell sx={headSx} align="center">Completed</TableCell>
|
||||
<TableCell sx={headSx} align="center">Cancelled</TableCell>
|
||||
<TableCell sx={headSx} align="center">Kms</TableCell>
|
||||
<TableCell sx={headSx} align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{r.clients.map((c, i) => (
|
||||
<TableRow key={c.client} hover>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>{c.client}</TableCell>
|
||||
<NumCell value={c.all} />
|
||||
<NumCell value={c.pending} />
|
||||
<NumCell value={c.completed} />
|
||||
<NumCell value={c.cancelled} />
|
||||
<TableCell align="center"><KmsChips kms={c.kms} actual={c.actualKms} /></TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(c.amount)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={1} sx={{ p: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Typography variant="subtitle2" color="text.secondary">Total Amount</Typography>
|
||||
<Typography variant="h5" color="primary.main" sx={{ fontWeight: 700 }}>{inr(totalAmount)}</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Dialog open={!!mapRider} onClose={() => setMapRider(null)} maxWidth="md" fullWidth>
|
||||
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{mapRider?.rider} — Location
|
||||
<IconButton onClick={() => setMapRider(null)}><CloseIcon /></IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<MapPlaceholder height={420} label="Rider Location" riders={[{ x: '50%', y: '45%', active: true }]} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
71
src/pages/riders/CreateRider.jsx
Normal file
71
src/pages/riders/CreateRider.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, CardContent, Stack, Button, TextField, Divider, InputAdornment
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
export default function CreateRider() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create Rider"
|
||||
breadcrumbs={[{ label: 'Riders', to: '/riders' }, { label: 'Create Rider' }]}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Admin Name" placeholder="Enter admin name" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
fullWidth label="Phone Number" placeholder="98450 11223"
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">+91</InputAdornment> }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Email Address" placeholder="rider@doormile.in" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} />
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth label="Address" placeholder="Enter full address" multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Suburb" placeholder="Enter suburb" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="City" placeholder="Enter city" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="State" placeholder="Enter state" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Post Code" placeholder="e.g. 560102" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Door No" placeholder="e.g. 24" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Landmark" placeholder="Nearby landmark" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: 3 }} />
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate('/riders')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders')}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
180
src/pages/riders/EditRider.jsx
Normal file
180
src/pages/riders/EditRider.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, CardContent, Stack, Button, TextField, MenuItem, Typography, Divider, Box, IconButton, InputAdornment
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { riders, tenantsList } from '@/data/mock';
|
||||
|
||||
const SHIFTS = ['Morning', 'Evening', 'Night'];
|
||||
const ACCOUNT_TYPES = ['Savings', 'Current'];
|
||||
const VEHICLES = ['Honda Activa', 'TVS Jupiter', 'Hero Splendor', 'Bajaj Pulsar', 'Honda Dio'];
|
||||
|
||||
const SectionTitle = ({ children }) => (
|
||||
<>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>{children}</Typography>
|
||||
<Divider sx={{ mt: 1, mb: 2.5 }} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default function EditRider() {
|
||||
const navigate = useNavigate();
|
||||
const rider = riders[0];
|
||||
const [firstName] = rider.name.split(' ');
|
||||
const lastName = rider.name.split(' ').slice(1).join(' ');
|
||||
const [insuranceExpiry, setInsuranceExpiry] = useState(dayjs().add(8, 'month'));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<IconButton size="small" onClick={() => navigate('/riders')}><ArrowBackIcon /></IconButton>
|
||||
<span>Edit Rider</span>
|
||||
</Stack>
|
||||
}
|
||||
breadcrumbs={[{ label: 'Riders', to: '/riders' }, { label: 'Edit Rider' }]}
|
||||
/>
|
||||
|
||||
<Card sx={{ mb: 10 }}>
|
||||
<CardContent sx={{ p: { xs: 2, md: 3 } }}>
|
||||
<SectionTitle>Contact Information</SectionTitle>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="First Name" defaultValue={firstName} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Last Name" defaultValue={lastName} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Phone Number" defaultValue={rider.phone} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Email" placeholder="rider@doormile.in" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth label="Address" defaultValue={rider.address} multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Location / Suburb" defaultValue={rider.address.split(',')[0]} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="City" defaultValue={(rider.address.split(',')[1] || '').trim()} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="State" placeholder="Enter state" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Identification No" placeholder="Aadhaar / ID number" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Choose Partner" defaultValue="">
|
||||
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Charges</SectionTitle>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Shift Type" defaultValue={rider.shift}>
|
||||
{SHIFTS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Base Fare" defaultValue={rider.fare} InputProps={{ startAdornment: <InputAdornment position="start">₹</InputAdornment> }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Additional Kms" placeholder="Per extra km" InputProps={{ startAdornment: <InputAdornment position="start">₹</InputAdornment> }} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Other Charges" placeholder="0" InputProps={{ startAdornment: <InputAdornment position="start">₹</InputAdornment> }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Bank Details</SectionTitle>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Account No" placeholder="Enter account number" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Account Name" defaultValue={rider.name} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Account Type" defaultValue="Savings">
|
||||
{ACCOUNT_TYPES.map((a) => <MenuItem key={a} value={a}>{a}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Bank Name" placeholder="Enter bank name" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="IFSC Code" placeholder="e.g. HDFC0001234" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Branch" placeholder="Enter branch" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<SectionTitle>Vehicle Details</SectionTitle>
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField select fullWidth label="Vehicle Name" defaultValue={rider.vehicle}>
|
||||
{VEHICLES.map((v) => <MenuItem key={v} value={v}>{v}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Vehicle No" defaultValue={rider.vehicleNo} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Model Year" placeholder="e.g. 2022" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Vehicle Color" placeholder="e.g. Black" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="License No" placeholder="Driving license number" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField fullWidth label="Insurance No" placeholder="Insurance policy number" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<DatePicker
|
||||
label="Insurance Expiry Date"
|
||||
value={insuranceExpiry}
|
||||
onChange={setInsuranceExpiry}
|
||||
slotProps={{ textField: { fullWidth: true } }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky', bottom: 0, py: 2, px: { xs: 2, md: 3 }, mt: -8,
|
||||
bgcolor: 'background.paper', borderTop: 1, borderColor: 'divider', zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
|
||||
<Button variant="outlined" startIcon={<ArrowBackIcon />} onClick={() => navigate('/riders')}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => navigate('/riders')}>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
221
src/pages/riders/Riders.jsx
Normal file
221
src/pages/riders/Riders.jsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useState, useMemo, Fragment } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip,
|
||||
TablePagination, Typography, Chip, Collapse
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
|
||||
import PowerSettingsNewOutlinedIcon from '@mui/icons-material/PowerSettingsNewOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { riders, riderLogs, locations } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'all', label: 'ALL' },
|
||||
{ key: 'active', label: 'Active' }
|
||||
];
|
||||
|
||||
export default function Riders() {
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [location, setLocation] = useState('all');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(5);
|
||||
const [expanded, setExpanded] = useState(null);
|
||||
|
||||
const tabKey = TABS[tab].key;
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
riders.filter((r) => {
|
||||
const matchTab = tabKey === 'all' ? true : r.status !== 'offline';
|
||||
const matchLocation = location === 'all' || r.address.includes(location);
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[r.id, r.userId, r.name, r.phone, r.address, r.vehicle, r.vehicleNo]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
return matchTab && matchLocation && matchSearch;
|
||||
}),
|
||||
[tabKey, location, search]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
const counts = {
|
||||
total: riders.length,
|
||||
active: riders.filter((r) => r.status === 'online').length,
|
||||
onDelivery: riders.filter((r) => r.status === 'on-delivery').length,
|
||||
offline: riders.filter((r) => r.status === 'offline').length,
|
||||
all: riders.length,
|
||||
activeTab: riders.filter((r) => r.status !== 'offline').length
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Riders"
|
||||
breadcrumbs={[{ label: 'Riders' }]}
|
||||
action={
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
|
||||
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location">
|
||||
<MenuItem value="all">All Locations</MenuItem>
|
||||
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
|
||||
</TextField>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders/create')}>
|
||||
Add Rider
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 1 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Riders" value={counts.total} icon={GroupsOutlinedIcon} caption="All registered" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active" value={counts.active} icon={CheckCircleOutlineIcon} color="success" caption="Online now" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="On Delivery" value={counts.onDelivery} icon={TwoWheelerOutlinedIcon} color="info" caption="In transit" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Offline" value={counts.offline} icon={PowerSettingsNewOutlinedIcon} color="error" caption="Not available" /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Card sx={{ mt: 1.5 }}>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||
<TextField
|
||||
size="small" placeholder="Search riders…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||
sx={{ minWidth: 240 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
|
||||
{TABS.map((t, i) => (
|
||||
<Tab
|
||||
key={t.key}
|
||||
label={<TabLabelCount label={t.label} count={t.key === 'all' ? counts.all : counts.activeTab} active={tab === i} />}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>S.NO</TableCell>
|
||||
<TableCell>User ID</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Vehicle</TableCell>
|
||||
<TableCell>Shift</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
<TableCell align="right">Fare</TableCell>
|
||||
<TableCell align="right">Fuel</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="center">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((r, i) => (
|
||||
<Fragment key={r.id}>
|
||||
<TableRow hover>
|
||||
<TableCell>{page * rpp + i + 1}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.userId}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<UserAvatar name={r.name} size={32} />
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{r.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.phone}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>{r.address}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{r.vehicle}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.vehicleNo}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{r.shift}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
||||
<Chip size="small" label={r.start} sx={{ bgcolor: 'success.lighter', color: 'success.main', fontWeight: 600 }} />
|
||||
<Chip size="small" label={r.end} sx={{ bgcolor: 'error.lighter', color: 'error.main', fontWeight: 600 }} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.fare)}</TableCell>
|
||||
<TableCell align="right">{inr(r.fuel)}</TableCell>
|
||||
<TableCell><StatusChip status={r.status} /></TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="Edit">
|
||||
<IconButton size="small" onClick={() => navigate(`/riders/${r.id}/edit`)}><EditOutlinedIcon fontSize="small" /></IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={expanded === r.id ? 'Hide logs' : 'View logs'}>
|
||||
<IconButton size="small" onClick={() => setExpanded(expanded === r.id ? null : r.id)}>
|
||||
{expanded === r.id ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} sx={{ p: 0, border: 0 }}>
|
||||
<Collapse in={expanded === r.id} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ p: 2.5, bgcolor: 'grey.50' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1.5 }}>Rider Logs</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell>Battery</TableCell>
|
||||
<TableCell>Charging</TableCell>
|
||||
<TableCell>Speed</TableCell>
|
||||
<TableCell>Accuracy</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
<TableCell>Order</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{riderLogs.map((log, li) => (
|
||||
<TableRow key={li}>
|
||||
<TableCell>{log.location}</TableCell>
|
||||
<TableCell>{log.battery}</TableCell>
|
||||
<TableCell>{log.charging}</TableCell>
|
||||
<TableCell>{log.speed}</TableCell>
|
||||
<TableCell>{log.accuracy}</TableCell>
|
||||
<TableCell>{log.time}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{log.order}</TableCell>
|
||||
<TableCell><StatusChip status={log.status} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
src/pages/tenants/CreateClient.jsx
Normal file
81
src/pages/tenants/CreateClient.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Grid, Stack, Button, TextField, MenuItem, InputAdornment } from '@mui/material';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
const COUNTRY_CODES = ['+91', '+1', '+44', '+61', '+971'];
|
||||
|
||||
export default function CreateClient() {
|
||||
const navigate = useNavigate();
|
||||
const [code, setCode] = useState('+91');
|
||||
const [form, setForm] = useState({
|
||||
adminName: '', phone: '', email: '', address: '',
|
||||
suburb: '', city: '', state: '', postcode: '', doorNo: '', landmark: ''
|
||||
});
|
||||
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Create Client"
|
||||
breadcrumbs={[{ label: 'Tenants', to: '/tenants' }, { label: 'Create Client' }]}
|
||||
/>
|
||||
|
||||
<MainCard title="Client Details">
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Admin Name" value={form.adminName} onChange={set('adminName')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
fullWidth size="small" label="Phone Number" value={form.phone} onChange={set('phone')}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<TextField
|
||||
select variant="standard" value={code} onChange={(e) => setCode(e.target.value)}
|
||||
InputProps={{ disableUnderline: true }} sx={{ minWidth: 56 }}
|
||||
>
|
||||
{COUNTRY_CODES.map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
|
||||
</TextField>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Email Address" value={form.email} onChange={set('email')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Door No" value={form.doorNo} onChange={set('doorNo')} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} multiline minRows={2} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Suburb" value={form.suburb} onChange={set('suburb')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="State" value={form.state} onChange={set('state')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Post Code" value={form.postcode} onChange={set('postcode')} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField fullWidth size="small" label="Landmark" value={form.landmark} onChange={set('landmark')} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={1.5} sx={{ mt: 3 }}>
|
||||
<Button variant="outlined" onClick={() => navigate('/tenants')}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => navigate('/tenants')}>Create</Button>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
258
src/pages/tenants/Tenants.jsx
Normal file
258
src/pages/tenants/Tenants.jsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useState, useMemo, Fragment } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card, Stack, Button, TextField, InputAdornment, Box, Tabs, Tab,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
Tooltip, TablePagination, Typography, Collapse, Grid
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatusChip from '@/components/StatusChip';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import TabLabelCount from '@/components/TabLabelCount';
|
||||
import { tenants, tenantPricing } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'active', label: 'Active' },
|
||||
{ key: 'pending', label: 'Pending' },
|
||||
{ key: 'inactive', label: 'Inactive' }
|
||||
];
|
||||
|
||||
function ReadField({ label, value }) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500, color: 'grey.800' }}>{value || '—'}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function PricingTab() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
|
||||
<Button variant="contained" startIcon={<AddIcon />}>Add Pricing</Button>
|
||||
</Stack>
|
||||
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1, overflow: 'hidden' }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Date</TableCell>
|
||||
<TableCell>Slab</TableCell>
|
||||
<TableCell align="right">Base Price</TableCell>
|
||||
<TableCell align="right">Min Kms</TableCell>
|
||||
<TableCell align="right">Price/Km</TableCell>
|
||||
<TableCell align="right">Other Charges</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tenantPricing.map((p, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{p.date}</TableCell>
|
||||
<TableCell>{p.slab}</TableCell>
|
||||
<TableCell align="right">{inr(p.basePrice)}</TableCell>
|
||||
<TableCell align="right">{p.minKms}</TableCell>
|
||||
<TableCell align="right">{inr(p.pricePerKm)}</TableCell>
|
||||
<TableCell align="right">{inr(p.otherCharges)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function EditTab({ tenant }) {
|
||||
const [form, setForm] = useState({
|
||||
name: tenant.name, contact: tenant.contact, phone: tenant.phone, email: tenant.email,
|
||||
address: tenant.address, city: tenant.city, postcode: tenant.postcode, lat: tenant.lat, lng: tenant.lng
|
||||
});
|
||||
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Tenant Name" value={form.name} onChange={set('name')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Person" value={form.contact} onChange={set('contact')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Number" value={form.phone} onChange={set('phone')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Email" value={form.email} onChange={set('email')} /></Grid>
|
||||
<Grid item xs={12}><TextField fullWidth size="small" label="Address" value={form.address} onChange={set('address')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="City" value={form.city} onChange={set('city')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="PostCode" value={form.postcode} onChange={set('postcode')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Latitude" value={form.lat} onChange={set('lat')} /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Longitude" value={form.lng} onChange={set('lng')} /></Grid>
|
||||
</Grid>
|
||||
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
<Button variant="contained">Update</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function TenantRow({ row, index }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inner, setInner] = useState(0);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
|
||||
<TableCell padding="checkbox">
|
||||
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<UserAvatar name={row.name} size={34} />
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }}>{row.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{row.volume} orders / mo</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{row.contact}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{row.phone}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{row.address}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{row.city} · {row.postcode}</Typography>
|
||||
</TableCell>
|
||||
<TableCell><StatusChip status={row.status} /></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ py: 0, borderBottom: open ? undefined : 'none' }}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ m: 2, borderRadius: 1, border: 1, borderColor: 'divider', overflow: 'hidden' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', bgcolor: 'grey.50' }}>
|
||||
<Tabs value={inner} onChange={(_, v) => setInner(v)} sx={{ px: 2 }}>
|
||||
<Tab label="Details" />
|
||||
<Tab label="Pricing" />
|
||||
<Tab label="Edit" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box sx={{ p: 2.5 }}>
|
||||
{inner === 0 && (
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Name" value={row.name} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Contact Person" value={row.contact} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Phone" value={row.phone} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="E-Mail" value={row.email} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Address" value={row.address} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="City" value={row.city} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="PostCode" value={row.postcode} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Latitude" value={row.lat} /></Grid>
|
||||
<Grid item xs={12} sm={6} md={4}><ReadField label="Longitude" value={row.lng} /></Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{inner === 1 && <PricingTab />}
|
||||
{inner === 2 && <EditTab tenant={row} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Tenants() {
|
||||
const navigate = useNavigate();
|
||||
const [tab, setTab] = useState(0);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(10);
|
||||
|
||||
const tabKey = TABS[tab].key;
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const c = {};
|
||||
TABS.forEach((t) => { c[t.key] = tenants.filter((d) => d.status === t.key).length; });
|
||||
return c;
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
tenants.filter((t) => {
|
||||
const matchTab = t.status === tabKey;
|
||||
const matchSearch =
|
||||
!search ||
|
||||
[t.name, t.contact, t.email, t.phone, t.city].join(' ').toLowerCase().includes(search.toLowerCase());
|
||||
return matchTab && matchSearch;
|
||||
}),
|
||||
[tabKey, search]
|
||||
);
|
||||
|
||||
const paged = filtered.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Tenants"
|
||||
breadcrumbs={[{ label: 'Tenants' }]}
|
||||
action={
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/tenants/create')}>
|
||||
Create Client
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
|
||||
<TextField
|
||||
size="small" placeholder="Search clients…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||
sx={{ minWidth: 260 }}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
|
||||
{TABS.map((t, i) => (
|
||||
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox" />
|
||||
<TableCell>S.No</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ border: 'none' }}>
|
||||
<EmptyState title="No tenants found" caption="Try a different tab or search term." />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paged.map((row, i) => <TenantRow key={row.id} row={row} index={page * rpp + i} />)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user