updated the ui for the mobile view

This commit is contained in:
2026-06-09 13:12:18 +05:30
parent 8513c7c2ec
commit f18b680247
29 changed files with 3664 additions and 225 deletions

View File

@@ -0,0 +1,140 @@
import PropTypes from 'prop-types';
import { Box, Paper, Stack, Typography } from '@mui/material';
// ============================================================================
// MobileCard — shared primitives that turn a desktop data-table row into an
// app-style card on phones. Used by every operator list page (deliveries,
// orders, customers, riders, tenants, …) so the mobile experience is
// consistent. Purely presentational: pages keep their own data + handlers and
// just slot content into these shells. Desktop layouts are untouched — these
// only render inside an `isMobile` branch.
//
// Tokens mirror the `DT` block in deliveries.js so cards match page surfaces.
// ============================================================================
const BORDER = '#e2e8f0';
const MUTED = '#94a3b8';
const PRIMARY_TEXT = '#0f172a';
// Vertical list wrapper — drop-in replacement for <TableContainer>/<TableBody>
// on mobile. `scroll` makes it an internal scroll region (matches the table's
// maxHeight behaviour); omit it to let the page scroll naturally.
export const MobileCardList = ({ children, scroll = false, onScroll, sx, ...rest }) => (
<Stack
spacing={1.25}
onScroll={onScroll}
sx={{
p: 1.5,
...(scroll && { maxHeight: 'calc(100vh - 220px)', overflowY: 'auto', overflowX: 'hidden' }),
...sx
}}
{...rest}
>
{children}
</Stack>
);
MobileCardList.propTypes = {
children: PropTypes.node,
scroll: PropTypes.bool,
onScroll: PropTypes.func,
sx: PropTypes.object
};
// Card shell — coloured accent rail on the left, a header slot (status badge /
// title / action buttons), then any field grid / collapse content as children.
export const MobileCard = ({ accent = '#662582', header, footer, selected = false, onClick, children, sx }) => (
<Paper
elevation={0}
onClick={onClick}
sx={{
position: 'relative',
overflow: 'hidden',
// Cards live inside a flex-column list; without this, a scroll-capped
// list (maxHeight) would SHRINK each card to a sliver (flex-shrink:1)
// and clip its content instead of scrolling. Keep natural height.
flexShrink: 0,
borderRadius: 2.5,
border: '1px solid',
borderColor: selected ? accent : BORDER,
background: selected ? `${accent}0a` : '#fff',
boxShadow: '0 4px 14px rgba(15,23,42,0.05)',
transition: 'border-color 0.15s, box-shadow 0.15s',
...sx
}}
>
<Box sx={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 3, bgcolor: accent }} />
<Box sx={{ p: 1.5, pl: 2 }}>
{header}
{children}
{footer}
</Box>
</Paper>
);
MobileCard.propTypes = {
accent: PropTypes.string,
header: PropTypes.node,
footer: PropTypes.node,
selected: PropTypes.bool,
onClick: PropTypes.func,
children: PropTypes.node,
sx: PropTypes.object
};
// Grid wrapper for MobileField cells. Two columns by default; pass `columns`
// to change. Keeps every card's body alignment identical.
export const MobileFieldGrid = ({ children, columns = 2, sx }) => (
<Box
sx={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
gap: 1,
mt: 1.25,
...sx
}}
>
{children}
</Box>
);
MobileFieldGrid.propTypes = {
children: PropTypes.node,
columns: PropTypes.number,
sx: PropTypes.object
};
// A single label/value cell. `full` makes it span the whole row; `value` can be
// a string/number or any node (chip, stack, etc.).
export const MobileField = ({ label, value, children, full = false, align = 'left' }) => (
<Box sx={{ gridColumn: full ? '1 / -1' : 'auto', minWidth: 0, textAlign: align }}>
<Typography
sx={{
fontSize: 9.5,
fontWeight: 800,
letterSpacing: 0.5,
textTransform: 'uppercase',
color: MUTED,
lineHeight: 1.4
}}
>
{label}
</Typography>
<Box sx={{ mt: 0.25, minWidth: 0 }}>
{children !== undefined ? (
children
) : (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: PRIMARY_TEXT }} noWrap>
{value ?? '—'}
</Typography>
)}
</Box>
</Box>
);
MobileField.propTypes = {
label: PropTypes.node,
value: PropTypes.node,
children: PropTypes.node,
full: PropTypes.bool,
align: PropTypes.string
};

View File

@@ -1,4 +1,4 @@
import { Grid } from '@mui/material';
import { Grid, useMediaQuery, useTheme } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import CircularLoader from 'components/CircularLoader';
import Loader from 'components/Loader';
@@ -6,6 +6,8 @@ import MainCard from 'components/MainCard';
import { getusers } from 'pages/api/api';
const ViewProfile = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const {
data: userdata,
isLoading,
@@ -23,8 +25,8 @@ const ViewProfile = () => {
<CircularLoader />
</>
)}
<MainCard>
<Grid container spacing={3}>
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={isMobile ? 2 : 3}>
<Grid item xs={12} sm={6} md={4}>
{userdata?.firstname}
</Grid>

View File

@@ -11,7 +11,9 @@ import {
TableContainer,
TableHead,
TableRow,
Typography
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdLocalOffer,
@@ -29,6 +31,7 @@ import { useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { getallpricing } from 'pages/api/api';
@@ -125,6 +128,8 @@ const MetricPill = ({ color, icon, label, width }) => (
// ==============================|| Pricing page ||============================== //
const ClientsPricing = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const containerRef = useRef();
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('All');
@@ -248,7 +253,7 @@ const ClientsPricing = () => {
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={12} sm={6} md={3}>
<Grid item key={item.key} xs={6} sm={6} md={3}>
<Paper
elevation={0}
sx={{
@@ -400,6 +405,132 @@ const ClientsPricing = () => {
background: '#fff'
}}
>
{isMobile ? (
rows.length === 0 && !isLoading ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdLocalOffer size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No pricing to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
</Typography>
</Stack>
) : (
<MobileCardList scroll>
{rows.map((row, index) => (
<MobileCard
key={row.pricingid || `${row.appname}-${index}`}
accent={BRAND}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary }}
noWrap
>
{row.appname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.pricingid}
</Typography>
</Stack>
</Stack>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted, flexShrink: 0 }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</Stack>
}
>
<Stack direction="row" spacing={0.75} sx={{ mt: 1, flexWrap: 'wrap', gap: 0.75 }}>
{row.applocation ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdPlace size={12} /> {row.applocation}
</Box>
) : null}
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdSpeed size={12} /> {row.slab || '—'}
</Box>
</Stack>
<MobileFieldGrid>
<MobileField label="Base Price">
<MetricPill
color={BRAND}
icon={<MdPriceCheck size={12} />}
label={formatRupees(row.baseprice)}
/>
</MobileField>
<MobileField label="Price / KM">
<MetricPill
color="#10b981"
icon={<MdAttachMoney size={12} />}
label={formatRupees(row.priceperkm)}
/>
</MobileField>
<MobileField label="Min KM">
<MetricPill
color="#f59e0b"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.minkm)} km`}
/>
</MobileField>
<MobileField label="Max KM">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.maxkm)} km`}
/>
</MobileField>
<MobileField label="Min Orders">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdReceiptLong size={14} color={DT.textMuted} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.minorder ?? '—'}
</Typography>
</Stack>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
</MobileCardList>
)
) : (
<TableContainer
ref={containerRef}
sx={{
@@ -597,6 +728,7 @@ const ClientsPricing = () => {
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
</>
);

View File

@@ -41,8 +41,10 @@ import {
TablePagination,
Skeleton,
Avatar,
Paper
Paper,
useMediaQuery
} from '@mui/material';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import {
EyeOutlined,
EyeInvisibleOutlined,
@@ -172,6 +174,7 @@ const Clients1 = () => {
const [rowsPerPage, setRowsPerPage] = React.useState(10);
// const [tenantList, settenantList] = useState([]);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [isloader, setisloader] = useState(false);
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('');
@@ -886,6 +889,7 @@ const Clients1 = () => {
>
<TableContainer
sx={{
display: { xs: 'none', md: 'block' },
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: '10px', height: '10px' },
'&::-webkit-scrollbar-thumb': {
@@ -1986,6 +1990,208 @@ const Clients1 = () => {
</TableBody>
</Table>
</TableContainer>
{/* ============================================= || Mobile cards (xs only) || ============================================= */}
{isMobile && (
<MobileCardList scroll sx={{ display: { xs: 'flex', md: 'none' } }}>
{getalltenantsIsLoading && (
<Stack alignItems="center" sx={{ py: 4 }}>
<Skeleton variant="rounded" width="100%" height={96} animation="wave" />
<Skeleton variant="rounded" width="100%" height={96} animation="wave" sx={{ mt: 1.25 }} />
</Stack>
)}
{tenantList?.length == 0 && !isloader ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No tenants to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{`No ${activeTabMeta.label.toLowerCase()} tenants for this filter.`}
</Typography>
</Stack>
) : (
tenantList?.map((row, index) => {
const rowStatusKey = value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive';
const rowStatusMeta = STATUS_META[rowStatusKey];
const RowStatusIcon = rowStatusMeta.icon;
const expanded = openRowIndex1 === index;
return (
<MobileCard
key={row.tenantid ?? index}
accent={rowStatusMeta.color}
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color="#6366f1" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: DT.textPrimary }} noWrap>
{row.tenantname}
</Typography>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
mt: 0.25,
pl: 0.25,
pr: 0.875,
py: 0.125,
borderRadius: 999,
bgcolor: tint(rowStatusMeta.color),
border: `1px solid ${edge(rowStatusMeta.color)}`,
color: rowStatusMeta.color,
alignSelf: 'flex-start'
}}
>
<AccentAvatar color={rowStatusMeta.color} size={16}>
<RowStatusIcon size={10} />
</AccentAvatar>
<Typography sx={{ fontWeight: 800, fontSize: 10, lineHeight: 1 }}>
{rowStatusMeta.label}
</Typography>
</Stack>
</Stack>
</Stack>
<Stack direction="row" spacing={0.5} sx={{ flexShrink: 0 }}>
{value0 == 0 && (
<IconButton
size="small"
sx={{
bgcolor: soft('#ef4444'),
color: '#ef4444',
border: `1px solid ${edge('#ef4444')}`,
'&:hover': { bgcolor: '#ef4444', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
setTimeout(() => {
tenantupdate(row.tenantid);
}, 100);
}}
>
<StopOutlined style={{ fontSize: 14 }} />
</IconButton>
)}
{value0 == 1 && (
<IconButton
size="small"
sx={{
bgcolor: soft('#f59e0b'),
color: '#f59e0b',
border: `1px solid ${edge('#f59e0b')}`,
'&:hover': { bgcolor: '#f59e0b', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
setDialogopen(true);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
getAppPricing(row.applolcationid);
}}
>
<IssuesCloseOutlined style={{ fontSize: 14 }} />
</IconButton>
)}
{value0 == 2 && (
<IconButton
size="small"
sx={{
bgcolor: soft('#10b981'),
color: '#10b981',
border: `1px solid ${edge('#10b981')}`,
'&:hover': { bgcolor: '#10b981', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
setTimeout(() => {
tenantupdate(row.tenantid);
}, 100);
}}
>
<FaRegCheckCircle style={{ fontSize: 14 }} />
</IconButton>
)}
<IconButton
size="small"
sx={{
bgcolor: expanded ? '#6366f1' : soft('#6366f1'),
color: expanded ? '#fff' : '#6366f1',
border: `1px solid ${edge('#6366f1')}`,
'&:hover': { bgcolor: '#6366f1', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
handleCollapseToggle1(index);
setOpenRowIndex2(-1);
setSelectedtenid(row.tenantid);
}}
>
{expanded ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
</IconButton>
{value0 !== 1 && (
<IconButton
size="small"
sx={{
bgcolor: soft('#8b5cf6'),
color: '#8b5cf6',
border: `1px solid ${edge('#8b5cf6')}`,
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
getAppPricing(row.applolcationid);
setDialogopen(true);
}}
>
<EditOutlined style={{ fontSize: 14 }} />
</IconButton>
)}
</Stack>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Tenant ID" value={`#${row.tenantid}`} />
<MobileField label="Contact" value={row.primarycontact || '—'} />
<MobileField label="Email" value={row.primaryemail || '—'} full />
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
{row.address || '—'}
</Typography>
</MobileField>
</MobileFieldGrid>
{expanded && (
<Box sx={{ mt: 1.25, pt: 1.25, borderTop: `1px solid ${DT.divider}` }}>
<MobileFieldGrid>
<MobileField label="Contact Person" value={row.firstname || '—'} />
<MobileField label="City" value={row.city || '—'} />
<MobileField label="Postcode" value={row.postcode || '—'} />
<MobileField label="Latitude" value={row.latitude || '—'} />
<MobileField label="Longitude" value={row.longitude || '—'} />
</MobileFieldGrid>
</Box>
)}
</MobileCard>
);
})
)}
</MobileCardList>
)}
{/* ============================================= || Pagination| ============================================= */}
{!searchword && tenantList?.length > 0 && (
<>
@@ -2003,7 +2209,16 @@ const Clients1 = () => {
)}
</Paper>
{/* // ==============================||( Client Pricing ) dialog (dialogopen) ||============================== // */}
<Dialog fullWidth={true} open={dialogopen} onClose={dialogclose} scroll={'paper'} maxWidth="sm" TransitionComponent={PopupTransition}>
<Dialog
fullWidth={true}
fullScreen={isMobile}
open={dialogopen}
onClose={dialogclose}
scroll={'paper'}
maxWidth="sm"
TransitionComponent={PopupTransition}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogTitle
sx={{
bgcolor: '#662582',

View File

@@ -1,6 +1,6 @@
import { React, useEffect, useState, useRef } from 'react';
import { useTheme } from '@mui/material/styles';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, IconButton, Autocomplete } from '@mui/material';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, IconButton, Autocomplete, useMediaQuery } from '@mui/material';
import MainCard from 'components/MainCard';
import axios from 'axios';
import Loader from 'components/Loader';
@@ -12,6 +12,8 @@ import LocationAutocomplete from 'components/nearle_components/LocationAutocompl
import { OpenToast } from 'components/third-party/OpenToast';
const CreateCustomer = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [appId, setAppId] = useState(0);
const locationRef = useRef(null);
const [mobilenumber, setMobilenumber] = useState('');
@@ -268,10 +270,10 @@ const CreateCustomer = () => {
<Typography variant="h3">Create Customer</Typography>
</Stack>
</Grid>
<MainCard>
<Grid container spacing={3}>
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={{ xs: 2, md: 3 }}>
<Grid item xs={12}>
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, md: 3 }}>
{/* ===================================================== || Choose location || ===================================================== */}
<Grid item xs={12} md={6}>
<LocationAutocomplete ref={locationRef} locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} sx={{}} />
@@ -491,9 +493,15 @@ const CreateCustomer = () => {
</Grid>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={2}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
if (appId === '') {
opentoast('Select Applocation ');

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography } from '@mui/material';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
// third-party
// import { PatternFormat } from 'react-number-format';
@@ -32,6 +32,7 @@ import { useNavigate } from 'react-router';
const Createclient = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// const [role, setRole] = useState('');
const [mobilenumber, setMobilenumber] = useState('');
const [emailaddress, setEmailaddress] = useState('');
@@ -299,8 +300,8 @@ const Createclient = () => {
<Typography variant="h3">Create Client</Typography>
</Stack>
</Grid>
<MainCard>
<Grid container spacing={3}>
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={isMobile ? 2 : 3}>
{/* <Grid item xs={12} sm={4} >
<MainCard title="Personal Information" sx={{ height: '100%' }}>
<Grid container spacing={3}>
@@ -396,8 +397,9 @@ const Createclient = () => {
<MainCard
// title="Contact Information"
sx={{ height: '100%' }}
contentSX={{ p: { xs: 1.5, md: 2.5 } }}
>
<Grid container spacing={3}>
<Grid container spacing={isMobile ? 2 : 3}>
{/* <Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-first-name">Business Name</InputLabel>
@@ -589,8 +591,13 @@ const Createclient = () => {
</MainCard>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={2}>
<Button variant="contained" onClick={() => createprofile()}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
Create
</Button>
</Stack>

View File

@@ -25,7 +25,9 @@ import {
TextField,
Autocomplete,
Avatar,
Paper
Paper,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdPeopleAlt,
@@ -50,6 +52,7 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { getallcustomers, getcustomersummary } from 'pages/api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / pricing pages so every
@@ -124,6 +127,8 @@ const autocompleteService = { current: null };
// ==============================|| MUI TABLE - ENHANCED ||============================== //
export default function Customers() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const containerRef = useRef();
const loadMoreRef = useRef();
const [rowsPerPage] = useState(50);
@@ -474,7 +479,7 @@ export default function Customers() {
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={12} sm={4}>
<Grid item key={item.key} xs={6} sm={4}>
<Paper
elevation={0}
sx={{
@@ -626,6 +631,109 @@ export default function Customers() {
background: '#fff'
}}
>
{isMobile ? (
<MobileCardList scroll onScroll={handleScroll}>
{customersIsLoading && <LoaderWithImage />}
{rows?.length === 0 && !customersIsLoading ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No customers to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the directory.'}
</Typography>
</Stack>
) : (
rows?.map((row, index) => (
<MobileCard
key={row.customerid || `${row.firstname}-${index}`}
accent="#662582"
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color="#662582" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.firstname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
</Typography>
</Stack>
</Stack>
<Tooltip title="Edit customer" placement="top">
<IconButton
size="small"
onClick={() => {
setSelectedCustomer(row);
setTimeout(() => setOpen(true), 0);
}}
sx={{
bgcolor: soft('#8b5cf6'),
color: '#8b5cf6',
border: `1px solid ${edge('#8b5cf6')}`,
flexShrink: 0,
'&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
}}
>
<MdEdit size={16} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Contact" value={row.contactno || '—'} />
<MobileField label="Location">
{row.suburb ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdLocationOn size={12} /> {row.suburb}
</Box>
) : (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textMuted }}></Typography>
)}
</MobileField>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
{row.address || '—'}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))
)}
{rows?.length !== 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more customers
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<TableContainer
ref={containerRef}
onScroll={handleScroll}
@@ -820,6 +928,7 @@ export default function Customers() {
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
{/* ======================================== || Edit Dialog || ======================================== */}
<Dialog
@@ -829,6 +938,8 @@ export default function Customers() {
aria-describedby="alert-dialog-description"
maxWidth="lg"
fullWidth
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogTitle
id="alert-dialog-title"

View File

@@ -61,7 +61,8 @@ import {
Backdrop,
MenuItem,
Menu,
Paper
Paper,
useMediaQuery
} from '@mui/material';
import { PopupTransition } from 'components/@extended/Transitions';
@@ -96,6 +97,7 @@ import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — extracted from the polished "Batch" dropdown so every
@@ -296,6 +298,9 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
const Deliveries = () => {
const userid = localStorage.getItem('userid');
const theme = useTheme();
// Below `md` we swap the wide data table for an app-style card list. Desktop
// (md and up) keeps the exact same table — no behaviour change either way.
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const loadMoreRef = useRef();
const containerRef = useRef();
const [deliverylist, setDeliverylist] = useState([]);
@@ -1592,7 +1597,263 @@ const Deliveries = () => {
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
const showSelect = tabstatus == 'Created';
const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
return (
return isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdInventory2 size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No deliveries to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{selectedBatch === 'all'
? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.`
: `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`}
</Typography>
</Stack>
)}
{filteredRows.length === 0 && countSourceLoading && (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading deliveries
</Typography>
</Stack>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
const isOpen = productCollapse?.orderid === row?.orderid;
const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1, py: 0.25, borderRadius: 999, bgcolor: tint(c), color: c, fontWeight: 700, fontSize: 11, border: `1px solid ${edge(c)}`, whiteSpace: 'nowrap' });
return (
<MobileCard
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
accent={rowStatusMeta.color}
selected={isSelected}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
{showSelect && (
<Checkbox
size="small"
sx={{ p: 0.25 }}
onChange={(e) => {
if (e.target.checked) {
let arr = deliverylist;
arr.push({ ...row, sno: deliverylist.length + 1 });
setDeliverylist([...arr]);
} else {
let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
if (res) {
let arr = deliverylist;
arr.splice(res.sno - 1, 1);
arr.map((val, i) => {
val.sno = i + 1;
});
setDeliverylist([...arr]);
}
}
}}
checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
/>
)}
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{ display: 'inline-flex', pl: 0.5, pr: 1, py: 0.25, borderRadius: 999, bgcolor: tint(rowStatusMeta.color), border: `1px solid ${edge(rowStatusMeta.color)}`, color: rowStatusMeta.color }}
>
<AccentAvatar color={rowStatusMeta.color} size={18}>
<RowStatusIcon size={11} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
{rowStatusMeta.label}
</Typography>
</Stack>
</Stack>
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
{row.deliverytype == 'C' && (
<IconButton
size="small"
onClick={() => {
if (productCollapse?.orderid === row.orderid) {
setProductCollapse(null);
setOrderHeaderId(null);
} else {
setProductCollapse(row);
setOrderHeaderId(row.orderheaderid);
}
}}
sx={{ borderRadius: 999, bgcolor: tint('#06b6d4'), color: '#06b6d4', border: `1px solid ${edge('#06b6d4')}`, '&:hover': { bgcolor: soft('#06b6d4') } }}
>
{isOpen ? <KeyboardArrowUpOutlined fontSize="small" /> : <KeyboardArrowDownOutlined fontSize="small" />}
</IconButton>
)}
{showAction && (
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{ borderRadius: 999, bgcolor: tint('#6366f1'), color: '#6366f1', border: `1px solid ${edge('#6366f1')}`, '&:hover': { bgcolor: soft('#6366f1') } }}
>
<EditOutlined />
</IconButton>
)}
</Stack>
</Stack>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.tenantname}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Order / Location" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {row.deliveryid}
</Typography>
</MobileField>
<MobileField label="Pickup">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupcontactno}
</Typography>
</MobileField>
<MobileField label="Drop">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverycontactno}
</Typography>
</MobileField>
<MobileField label="Rider" full>
{row.ridername ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={24}>
<MdDirectionsBike size={13} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.ridername}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
ID #{row.userid} · {row.ridercontact || '—'}
</Typography>
</Stack>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
Unassigned
</Typography>
)}
</MobileField>
<MobileField label="ETA" value={row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} />
<MobileField label="Transit">
<Box sx={chipSx('#06b6d4')}>{row.transitminutes || 0}m</Box>
</MobileField>
<MobileField label="Kms · plan / act">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
</Stack>
</MobileField>
<MobileField label="Amount · chg / amt">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}> {row.deliverycharges?.toFixed(2) ?? '0.00'}</Box>
<Box sx={chipSx('#10b981')}> {row.deliveryamt?.toFixed(2) ?? '0.00'}</Box>
</Stack>
</MobileField>
<MobileField label="Qty" value={row.Quantity || '—'} />
<MobileField label="COD">
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
</Typography>
</MobileField>
<MobileField label="Step">
{row.step ? (
<Box sx={{ ...chipSx('#6366f1'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</MobileField>
{row.notes && (
<MobileField label="Notes" full>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.notes}</Typography>
</MobileField>
)}
</MobileFieldGrid>
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#6366f1" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#6366f1' }}>
Product Details
</Typography>
</Stack>
<Stack spacing={1}>
{orderdetails?.details?.map((product, idx2) => (
<Stack key={idx2} direction="row" alignItems="center" spacing={1.25} sx={{ p: 1, borderRadius: 1.5, bgcolor: '#fff', border: `1px solid ${DT.divider}` }}>
<Box component="img" src={product?.productimage || 'https://via.placeholder.com/40'} alt={product?.productname} sx={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 1.5, border: `1px solid ${DT.divider}`, flexShrink: 0 }} />
<Stack sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }} noWrap>
{product?.productname || 'Unnamed'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
Qty {product?.orderqty || 0} · {product?.price || 0}
</Typography>
</Stack>
<Typography variant="body2" sx={{ fontWeight: 800, color: DT.textPrimary, flexShrink: 0 }}>
{(product?.productsumprice + product?.taxamount).toFixed(2) || 0}
</Typography>
</Stack>
))}
<Stack direction="row" justifyContent="space-between" sx={{ pt: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textSecondary }}>Total Amount</Typography>
<Typography variant="body2" sx={{ fontWeight: 800, color: '#10b981' }}>
{orderdetails?.pricedetails?.orderamount?.toFixed(2)}
</Typography>
</Stack>
</Stack>
</Box>
)}
</MobileCard>
);
})}
{countSourceRows?.length != 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{countIsFetchingNext || countHasNext ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
· End of list ·
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: { xs: 1100, lg: 1300, xl: 1400 } }}>
<TableHead>
<TableRow
@@ -2024,94 +2285,6 @@ const Deliveries = () => {
<EditOutlined />
</IconButton>
</Tooltip>
<Menu
anchorEl={menuAnchorEl}
open={menuOpen}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
elevation: 0,
sx: {
minWidth: 220,
borderRadius: 2,
mt: 0.75,
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden',
'& .MuiMenuItem-root': {
fontSize: 13,
fontWeight: 600,
py: 1.1,
px: 1.5,
gap: 1,
transition: 'background-color 0.15s',
'&:hover': { bgcolor: DT.surfaceAlt }
}
}
}}
>
{selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
handleMenuClose();
}}
>
<AccentAvatar color="#0ea5e9" size={22}><MdNotificationsActive size={13} /></AccentAvatar>
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
<MenuItem
onClick={() => {
if (!appId) {
opentoast('Please select a location first!', 'warning');
locationRef.current?.focus();
return;
}
setChangeDialogOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#8b5cf6" size={22}><MdDirectionsBike size={13} /></AccentAvatar>
Change Rider
</MenuItem>
)}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#10b981" size={22}><MdCheckCircle size={13} /></AccentAvatar>
Update Status
</MenuItem>
)}
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
onClick={() => {
setCancelDeliveryOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#ef4444" size={22}><MdCancel size={13} /></AccentAvatar>
Cancel Delivery
</MenuItem>
)}
</Menu>
</Stack>
</TableCell>
)}
@@ -2238,6 +2411,96 @@ const Deliveries = () => {
</TableContainer>
</Paper>
{/* Shared row-action menu — single instance reused by both the desktop
table rows and the mobile cards (both trigger handleMenuOpen). */}
<Menu
anchorEl={menuAnchorEl}
open={menuOpen}
onClose={handleMenuClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
elevation: 0,
sx: {
minWidth: 220,
borderRadius: 2,
mt: 0.75,
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
overflow: 'hidden',
'& .MuiMenuItem-root': {
fontSize: 13,
fontWeight: 600,
py: 1.1,
px: 1.5,
gap: 1,
transition: 'background-color 0.15s',
'&:hover': { bgcolor: DT.surfaceAlt }
}
}
}}
>
{selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
handleMenuClose();
}}
>
<AccentAvatar color="#0ea5e9" size={22}><MdNotificationsActive size={13} /></AccentAvatar>
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
<MenuItem
onClick={() => {
if (!appId) {
opentoast('Please select a location first!', 'warning');
locationRef.current?.focus();
return;
}
setChangeDialogOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#8b5cf6" size={22}><MdDirectionsBike size={13} /></AccentAvatar>
Change Rider
</MenuItem>
)}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#10b981" size={22}><MdCheckCircle size={13} /></AccentAvatar>
Update Status
</MenuItem>
)}
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
onClick={() => {
setCancelDeliveryOpen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#ef4444" size={22}><MdCancel size={13} /></AccentAvatar>
Cancel Delivery
</MenuItem>
)}
</Menu>
{/* =============================== || cancel dialog || =============================== */}
<Dialog
open={cancelDeliveryOpen}
@@ -2435,10 +2698,11 @@ const Deliveries = () => {
<Dialog
open={open}
onClose={() => setOpen(false)}
fullScreen={isMobile}
PaperProps={{
elevation: 0,
sx: {
borderRadius: 3,
borderRadius: { xs: 0, sm: 3 },
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,
@@ -2559,11 +2823,12 @@ const Deliveries = () => {
onClose={dialogclose}
scroll="paper"
maxWidth="sm"
fullScreen={isMobile}
TransitionComponent={PopupTransition}
PaperProps={{
elevation: 0,
sx: {
borderRadius: 3,
borderRadius: { xs: 0, sm: 3 },
border: '1px solid',
borderColor: DT.borderSubtle,
boxShadow: DT.shadowPop,

View File

@@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
import { useTheme, useMediaQuery } from '@mui/material';
import {
MdPublic,
MdSwapHoriz,
@@ -60,6 +61,13 @@ function CompareDataPanel({
setExpandedSeqGroups,
onClose
}) {
// Mobile flag — gates only layout (a className on the root <aside> that a
// scoped media-query block in Dispatch.css uses to stack the side-by-side
// timing/clock columns vertically and let stat chips wrap). No data,
// handler, or derivation behaviour changes on any breakpoint.
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// All derivations live in a single useMemo so the cost of re-running
// them is paid only when an upstream input actually changes — not on
// every parent render (e.g. cursor moving over the map, sync toggle
@@ -294,7 +302,10 @@ function CompareDataPanel({
} = view;
return (
<aside id="compare-data-panel" className="compare-data-panel">
<aside
id="compare-data-panel"
className={`compare-data-panel${isMobile ? ' cdp-is-mobile' : ''}`}
>
<div className="cdp-head">
<div className="cdp-head-title">
<span

View File

@@ -9244,6 +9244,39 @@
padding: 2px 8px;
}
/* ============================================================
Compare data panel — mobile layout (gated by the cdp-is-mobile
class added in CompareDataPanel.js when viewport < md). Desktop
(>= md) renders without this class, so nothing here applies.
Only stacking / wrapping — no behaviour, colours, or content
change. The panel occupies the full-width compare column on
phones; the side-by-side clock and stat grids stack vertically
and the chip rows are allowed to wrap so nothing overflows.
============================================================ */
.dispatch-container .compare-data-panel.cdp-is-mobile {
border-radius: 0;
}
/* 3-column clock (First | duration | Last) → single column stack. */
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-timing-clock {
grid-template-columns: 1fr;
gap: 14px;
}
/* The center track is horizontal between two side columns on desktop;
give it a sane height when it becomes a full-width stacked row. */
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-clock-track {
min-width: 0;
height: 48px;
}
/* Timing stats (avg/stop + avg speed) → stack instead of side by side.
(The chip rows — highlight-meta / dev-meta / step-deltas / trip-stats —
already carry flex-wrap on desktop, so no extra wrap rules are needed.) */
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-timing-stats {
grid-template-columns: 1fr;
}
/* Sidebar Rider Card Est. Meters badge */
.dispatch-container .rcard-est-meters {
display: inline-flex;
@@ -10691,4 +10724,82 @@
color: #b45309;
font-weight: 600;
font-size: 10.5px;
}
/* =========================================================================
Mobile / app-like layout (≤ 768px). Purely a LAYOUT pass — no behaviour
changes. On phones the side-by-side map + sidebar collapses into a single
vertical stack: the live map takes the top of the screen full-width, and
the rider/batch sidebar flows below it full-width and scrolls. Desktop
(≥ md) is untouched; every rule here is gated inside this media query.
========================================================================= */
@media (max-width: 768px) {
/* The standalone page un-does MainCard's 24px padding via negative margin
+ 100vw-ish sizing; keep that, just allow the inner body to grow with
its stacked children instead of being clipped to a single row height. */
.dispatch-container {
height: auto;
min-height: calc(100vh - 88px);
overflow-y: auto;
overflow-x: hidden;
}
/* Stack map (first) over sidebar (second). #body is a flex row on desktop;
flip it to a column so the children lay out vertically. */
.dispatch-container #body {
flex-direction: column;
overflow: visible;
}
/* Map: full-width, fixed viewport-relative height so Leaflet has an
explicit box to size its canvas against. Order it first. */
.dispatch-container #map-wrap,
.dispatch-container #map-wrap.compare-split {
order: 1;
flex: 0 0 auto;
width: 100%;
min-width: 0;
height: 48vh;
min-height: 320px;
margin-right: 0;
border-radius: 0;
}
/* Sidebar: full-width below the map, natural height, internal scroll.
Force it visible even if the collapse state is set (the horizontal
collapse-to-width:0 animation is meaningless when stacked). */
.dispatch-container #sidebar,
.dispatch-container #body.sidebar-collapsed #sidebar {
order: 2;
width: 100%;
flex: 1 1 auto;
flex-basis: auto;
min-width: 0;
max-height: none;
border-right: 0;
border-top: 1px solid var(--border);
border-right-color: var(--border);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* The collapse peek-tab is anchored to a left:Npx column edge that no
longer exists once stacked — hide it so it doesn't float over the map. */
.dispatch-container .sidebar-toggle-tab {
display: none;
}
/* Header — let it wrap cleanly instead of overflowing a fixed-height row. */
.dispatch-container #hdr {
height: auto;
min-height: 42px;
flex-wrap: wrap;
padding: 8px 16px;
gap: 8px;
}
/* The BI / analysis view is its own scroll surface — let it grow. */
.dispatch-container #dispatch-analysis {
overflow-y: visible;
}
}

View File

@@ -17,7 +17,9 @@ import {
Tabs,
TextField,
Tooltip,
Typography
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import { useMutation, useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
@@ -278,6 +280,8 @@ const Preview = () => {
const navigate = useNavigate();
const location = useLocation();
const stateData = location.state || {};
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes
// through this state. The Dispatch tab renders from it, the Reconcile tab
@@ -553,7 +557,12 @@ const Preview = () => {
</Backdrop>
<Box sx={{ py: 1.25, px: 2, borderBottom: '1px solid #eef2f6' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack
direction={{ xs: 'column', md: 'row' }}
alignItems={{ xs: 'stretch', md: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
@@ -567,11 +576,16 @@ const Preview = () => {
Assign Orders
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={1}
sx={{ width: { xs: '100%', md: 'auto' } }}
>
<Autocomplete
options={tuningTypes || []}
getOptionLabel={(option) => option.type}
sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
sx={{ minWidth: { xs: 0, sm: 250 }, maxWidth: 600, flex: 1, width: { xs: '100%', sm: 'auto' } }}
renderInput={(params) => <TextField {...params} label="Hyper Tuning" />}
onChange={(e, val, reason) => {
if (reason === 'clear') handleCreateDelivery(null);
@@ -582,6 +596,7 @@ const Preview = () => {
variant="contained"
color="primary"
startIcon={<IoReload />}
fullWidth={isMobile}
onClick={() => {
setIsLoading(true);
handleCreateDelivery('reshuffle');
@@ -600,7 +615,12 @@ const Preview = () => {
</Box>
<Box sx={{ px: 2, borderBottom: '1px solid #eef2f6' }}>
<Tabs value={tabValue} onChange={(e, v) => setTabValue(v)} sx={{ minHeight: 40 }}>
<Tabs
value={tabValue}
onChange={(e, v) => setTabValue(v)}
variant={isMobile ? 'fullWidth' : 'standard'}
sx={{ minHeight: 40 }}
>
<Tab label="Dispatch" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
<Tab label="Reconcile" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
</Tabs>
@@ -647,8 +667,13 @@ const Preview = () => {
{reconcileRiders.map((r) => {
const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
return (
<Card key={r.rider_id} sx={{ p: 2, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.25 }}>
<Card key={r.rider_id} sx={{ p: { xs: 1.5, md: 2 }, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 1.25, flexWrap: 'wrap', gap: 1 }}
>
<Stack direction="row" alignItems="center" gap={1.25}>
<Box
sx={{
@@ -732,7 +757,8 @@ const Preview = () => {
startIcon={<MdSwapHoriz />}
onClick={handleReconcile}
disabled={reconcileLoading || dirtyRiderIds.size === 0}
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
fullWidth={isMobile}
sx={{ minWidth: { xs: 0, sm: 220 }, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
>
{reconcileLoading
? 'Reconciling...'
@@ -748,22 +774,35 @@ const Preview = () => {
</Box>
<Box sx={{ px: 2, py: 1.25, borderTop: '1px solid #eef2f6' }}>
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
<Stack
direction={{ xs: 'column-reverse', sm: 'row' }}
gap={{ xs: 1, sm: 2 }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="end"
>
<Button
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
fullWidth={isMobile}
onClick={() => navigate(-1)}
>
Back
</Button>
<Button variant="contained" onClick={handleFinalCreateDelivery}>
<Button variant="contained" fullWidth={isMobile} onClick={handleFinalCreateDelivery}>
Assign Orders
</Button>
</Stack>
</Box>
<Dialog open={changeDialogOpen} onClose={() => setChangeDialogOpen(false)} maxWidth="xs" fullWidth>
<Dialog
open={changeDialogOpen}
onClose={() => setChangeDialogOpen(false)}
maxWidth="xs"
fullWidth
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogTitle sx={{ fontWeight: 700 }}>Change Rider</DialogTitle>
<DialogContent>
<Typography sx={{ mb: 2, fontSize: 13, color: 'text.secondary' }}>
@@ -779,9 +818,11 @@ const Preview = () => {
renderInput={(params) => <TextField {...params} label="New rider" placeholder="Pick a rider" />}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setChangeDialogOpen(false)}>Cancel</Button>
<Button variant="contained" disabled={!selectedNewRider} onClick={confirmChangeRider}>
<DialogActions sx={{ px: 3, pb: 2, flexDirection: { xs: 'column-reverse', sm: 'row' }, gap: { xs: 1, sm: 0 } }}>
<Button fullWidth={isMobile} onClick={() => setChangeDialogOpen(false)}>
Cancel
</Button>
<Button variant="contained" fullWidth={isMobile} disabled={!selectedNewRider} onClick={confirmChangeRider}>
Change Rider
</Button>
</DialogActions>

View File

@@ -21,8 +21,10 @@ import {
TablePagination,
TableRow,
Tooltip,
Typography
Typography,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MdReceiptLong,
MdDashboard,
@@ -40,6 +42,7 @@ import { fetchinvoiceinsight, fetchdeliverylist } from 'pages/api/api';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with deliveries / tenants / customers / pricing /
@@ -101,6 +104,8 @@ function formatNumberToRupees(value) {
const Invoice = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [billStatus, setBillStatus] = useState(0);
@@ -523,19 +528,183 @@ const Invoice = () => {
background: '#fff'
}}
>
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 880, md: 1060 } }}>
{isMobile ? (
<>
{isDeliveryLoading ? (
<Box sx={{ p: 1.5 }}>
<OrdersTableSkeleton col={4} />
</Box>
) : pagedList.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdReceiptLong size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No invoices to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword
? 'Try a different keyword.'
: `No ${activeMeta.label.toLowerCase()} invoices for this filter.`}
</Typography>
</Stack>
) : (
<MobileCardList>
{pagedList.map((item, index) => {
const overdue =
billStatus === 2 ||
(item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3);
return (
<MobileCard
key={item.invoiceno || index}
accent={BRAND}
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.tenantname || '—'}
</Typography>
{item.contactperson && (
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{item.contactperson}
</Typography>
)}
</Stack>
</Stack>
<Tooltip title="Preview invoice" placement="left">
<IconButton
size="small"
onClick={() => {
setIsLoader(true);
setTimeout(() => {
setIsLoader(false);
navigate('/nearle/invoice/preview', { state: item });
}, 500);
}}
sx={{
flexShrink: 0,
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
<MdVisibility size={16} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Invoice ID">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {item.invoiceno || '—'}
</Box>
</MobileField>
<MobileField label="Amount" align="right">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800,
justifyContent: 'center'
}}
>
<MdCurrencyRupee size={11} />
{formatNumberToRupees(item.totalamount).replace('₹', '').trim()}
</Box>
</MobileField>
<MobileField label="Invoice Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={DT.textMuted} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Due Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={overdue && billStatus !== 3 ? '#ef4444' : DT.textMuted} />
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: overdue && billStatus !== 3 ? '#ef4444' : DT.textPrimary
}}
noWrap
>
{item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Items">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#14b8a6'),
border: `1px solid ${edge('#14b8a6')}`,
color: '#14b8a6',
fontSize: 11,
fontWeight: 800,
minWidth: 44,
justifyContent: 'center'
}}
>
<MdInventory2 size={11} /> {item.itemcount ?? 0}
</Box>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</>
) : (
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 880, md: 1060 } }}>
<TableHead>
<TableRow
sx={{
@@ -754,9 +923,10 @@ const Invoice = () => {
);
})
)}
</TableBody>
</Table>
</TableContainer>
</TableBody>
</Table>
</TableContainer>
)}
<Divider />
<Stack

View File

@@ -1,6 +1,7 @@
import React, { useRef, useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
// import nearleLogo from '../../../assets/images/nearleLogo.png';
import logo_nearle1 from '../../../assets/images/logo-nearle1.png';
import axios from 'axios';
@@ -56,6 +57,7 @@ const InvoicePreview = () => {
const [refnumber, setRefnumber] = useState('');
const [remarks, setRemarks] = useState('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
setselected(location.state);
}, []);
@@ -96,7 +98,13 @@ const InvoicePreview = () => {
return (
<>
<Stack direction="row" justifyContent="Space-between" alignItems={'center'} spacing={2} sx={{ px: 2.5, py: 1, bgcolor: '#eeeeee' }}>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="Space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
sx={{ px: { xs: 1.5, md: 2.5 }, py: 1, bgcolor: '#eeeeee' }}
>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Tooltip title="back">
<IconButton
@@ -123,10 +131,11 @@ const InvoicePreview = () => {
</Stack>
</Stack>
<Stack direction={'row'} spacing={2}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ width: { xs: '100%', md: 'auto' } }}>
<Button
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
@@ -148,6 +157,7 @@ const InvoicePreview = () => {
startIcon={<PrinterFilled />}
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
@@ -162,8 +172,12 @@ const InvoicePreview = () => {
/>
</Stack>
</Stack>
<Box sx={{ pb: 2.5, border: '1px solid #eee' }}>
<div ref={componentRef} style={{ width: '100%' }}>
<Box sx={{ pb: 2.5, border: '1px solid #eee', overflowX: { xs: 'auto', md: 'visible' } }}>
{/* minWidth keeps the invoice at a legible fixed layout on phones —
the parent's overflowX:auto then lets it scroll horizontally
instead of squishing the header into vertical slivers. 720px sits
within the print page width, so printing is unaffected. */}
<div ref={componentRef} style={{ width: '100%', minWidth: 720 }}>
<Box id="print" sx={{ p: 2.5 }}>
<Box sx={{ pb: 2.5 }}>
<Stack

View File

@@ -19,6 +19,7 @@ import {
Link
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import AnimateButton from 'components/@extended/AnimateButton';
import logo from 'assets/images/logo-nearle1.png';
@@ -32,6 +33,7 @@ import { enqueueSnackbar } from 'notistack';
const Login = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [alertmessage, setAlertmessage] = useState('');
@@ -224,9 +226,9 @@ const Login = () => {
item
xs={12}
// sx={{ ml: 3, mt: 3 }}
sx={{ ml: 3, mt: 1 }}
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
>
<img src={logo} alt="legendary" width="200px" />
<img src={logo} alt="legendary" width={isMobile ? '160px' : '200px'} />
</Grid>
<Grid item xs={12}>
<Grid
@@ -242,8 +244,9 @@ const Login = () => {
{/* <AuthCard>{children}</AuthCard> */}
<Box
sx={{
width: { xs: '100%', sm: 'auto' },
maxWidth: { xs: 400, lg: 475 },
margin: { xs: 2.5, md: 3 },
margin: { xs: 2, sm: 2.5, md: 3 },
'& > *': {
flexGrow: 1,
flexBasis: '50%'
@@ -254,10 +257,10 @@ const Login = () => {
sx={{
position: 'relative',
border: '1px solid',
borderRadius: 1,
borderRadius: { xs: 2, md: 1 },
borderColor: theme.palette.divider,
boxShadow: 'inherit',
p: 2,
boxShadow: { xs: '0 14px 40px rgba(15, 23, 42, 0.10)', md: 'inherit' },
p: { xs: 2, md: 2 },
width: '100%'
}}
>

View File

@@ -19,6 +19,8 @@ import {
} from '@mui/material';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { useLocation, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import MainCard from 'components/MainCard';
@@ -38,8 +40,150 @@ import { HiOutlineArrowLeft } from 'react-icons/hi';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
// Mobile-only rendering of the optimised-orders preview. Mirrors the exact same
// fields, chips and tooltips as the desktop table rows — no behaviour added,
// purely a card layout for phones. Renders for both aiMode 1 and normal mode;
// the Zone / Rider fields are gated on aiMode just like the table columns.
const MobileOrdersList = ({ list, aiMode }) => {
if (!list || list.length === 0) {
return (
<Stack alignItems="center" sx={{ py: 4 }}>
<Empty />
</Stack>
);
}
return (
<MobileCardList>
{list.map((val, index) => {
const typeColor =
val.ordertype == 'Economy' ? 'success' : val.ordertype == 'Risky' ? 'error' : 'primary';
return (
<MobileCard
key={index}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: '#94a3b8' }}>#{index + 1}</Typography>
{aiMode == 1 && <Chip size="small" color="primary" label={val.zone_name} />}
</Stack>
<Chip size="small" label={val.ordertype} color={typeColor} />
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Tenant" full>
<Tooltip title={val.tenantaddress}>
<Stack>
<Typography variant="body1" noWrap>
{val.tenantname}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{val.tenantsuburb}
</Typography>
<Typography noWrap variant="body2">
{val.applocation}
</Typography>
</Stack>
</Tooltip>
</MobileField>
<MobileField label="Order Location" full>
<Tooltip title={val.locationaddress} placement="top">
<Typography variant="body1" noWrap>
{`${val.locationname}-(${val.locationsuburb})`}
</Typography>
</Tooltip>
<Tooltip title="Order Id">
<Typography variant="body2" noWrap>
{val.orderid}
</Typography>
</Tooltip>
<Stack display={'flex'} flexDirection={'row'} gap={3}>
<Tooltip title="Ordered date">
<Stack>
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(val.orderdate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(val.orderdate).utc().format('hh:mm A')}
</Typography>
</Stack>
</Tooltip>
-
<Tooltip title="Delivery date">
<Stack>
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(val.deliverydate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(val.deliverydate).utc().format('hh:mm A')}
</Typography>
</Stack>
</Tooltip>
</Stack>
</MobileField>
<MobileField label="Pickup">
<Stack direction="column">
<Typography variant="caption">{val.pickupcustomer}</Typography>
<Typography variant="caption">{val.pickupcontactno}</Typography>
<Tooltip title={val.pickupaddress}>
<Typography variant="caption">{val.pickupsuburb || val.pickupaddress.slice(0, 20)}</Typography>
</Tooltip>
</Stack>
</MobileField>
<MobileField label="Delivery">
<Stack direction="column">
<Typography variant="caption">{val.deliverycustomer}</Typography>
<Typography variant="caption">{val.deliverycontactno}</Typography>
<Tooltip title={val.deliveryaddress}>
<Typography variant="caption">{val.deliverysuburb || val.deliveryaddress.slice(0, 20)}</Typography>
</Tooltip>
</Stack>
</MobileField>
{val.ordernotes ? <MobileField label="Notes" value={val.ordernotes} full /> : null}
{aiMode == 1 && (
<MobileField label="Rider" full>
<Typography sx={{ whiteSpace: 'nowrap' }}>{val.username}</Typography>
<Typography>ID : {val.userid}</Typography>
</MobileField>
)}
<MobileField label="Profit">
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
<Tooltip title="Charges" placement="top">
<Chip size="small" label={`${val.deliverycharge.toFixed(2)} `} color="error" />
</Tooltip>
<Tooltip title="Amount" placement="left">
<Chip size="small" label={`${val.deliveryamt.toFixed(2)} `} color="success" />
</Tooltip>
</Stack>
</MobileField>
<MobileField label="KMS">
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
<Tooltip title="KMS" placement="top">
<Chip size="small" label={`${val.kms} km`} color="error" />
</Tooltip>
<Tooltip title="Cumulative Kms" placement="right">
<Chip size="small" label={`${val.cumulativekms} km`} color="success" />
</Tooltip>
</Stack>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
);
};
const OrdersPreview = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const location = useLocation();
console.log('location.state', location.state);
@@ -246,6 +390,9 @@ const OrdersPreview = () => {
</Grid>
</Stack>
)}
{isMobile ? (
<MobileOrdersList list={finaldeliveryList} aiMode={aiMode} />
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 250px)',
@@ -549,6 +696,7 @@ const OrdersPreview = () => {
</TableBody>
</Table>
</TableContainer>
)}
<Divider />
{aiMode == 0 && (
<Grid container spacing={2} sx={{ p: 2 }}>
@@ -600,9 +748,16 @@ const OrdersPreview = () => {
</Grid>
)}
<Divider />
<Stack display={'flex'} flexDirection={'row'} gap={2} alignItems={'center'} justifyContent={'end'} sx={{ p: 2 }}>
<Stack
display={'flex'}
flexDirection={{ xs: 'column', sm: 'row' }}
gap={2}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent={'end'}
sx={{ p: 2 }}
>
<Button
sx={{}}
fullWidth={isMobile}
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
@@ -612,7 +767,13 @@ const OrdersPreview = () => {
>
Back
</Button>
<Button sx={{ my: 2 }} variant="contained" disabled={aiMode === 0 && (!rider || !payment)} onClick={handleManualCreateDelivery}>
<Button
fullWidth={isMobile}
sx={{ my: { xs: 0, sm: 2 } }}
variant="contained"
disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleManualCreateDelivery}
>
Assign Orders
</Button>
</Stack>

View File

@@ -22,7 +22,8 @@ import {
FormGroup,
FormControlLabel,
Box,
Card
Card,
useMediaQuery
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { Empty } from 'antd';
@@ -185,6 +186,7 @@ const Createorder1 = () => {
const loaded = React.useRef(false);
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const locationRef = useRef(null);
const tenantRef = useRef(null);
const [inputValue1, setInputValue1] = React.useState('');
@@ -2439,9 +2441,10 @@ const Createorder1 = () => {
setSearchCustList('');
}}
fullWidth
fullScreen={isMobile}
sx={{
'& .MuiDialog-paper': {
borderRadius: '16px',
borderRadius: { xs: 0, sm: '16px' },
overflow: 'hidden'
}
}}
@@ -2567,10 +2570,13 @@ const Createorder1 = () => {
onClose={() => {
setOpen(false);
}}
fullWidth
sx={{
'& .MuiDialog-paper': {
borderRadius: '16px',
borderRadius: { xs: 3, sm: '16px' },
p: 1.5,
m: { xs: 1.5, sm: 4 },
width: { xs: 'calc(100% - 24px)', sm: 'auto' },
maxWidth: '400px'
}
}}

View File

@@ -88,6 +88,8 @@ dayjs.extend(utc);
// import HeartFilled from '@mui/icons-material/Favorite';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import {
CloseOutlined,
WarningOutlined,
@@ -177,6 +179,7 @@ const Details = () => {
}, [state.orderheaderid, state.tenantid]);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// const fetchorderdetails = async () => {
// setLoading(true);
@@ -704,7 +707,13 @@ const Details = () => {
const [deletepassword, setDeletepassword] = useState('');
return (
<Dialog open={open} onClose={() => handleClose(false)} maxWidth="xs">
<Dialog
open={open}
onClose={() => handleClose(false)}
maxWidth="xs"
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogContent sx={{ mt: 2, my: 1 }}>
<Stack alignItems="center" spacing={3.5}>
<Avatar color="error" sx={{ width: 72, height: 72, fontSize: '1.75rem' }}>
@@ -778,7 +787,8 @@ const Details = () => {
open={dialogopen}
onClose={dialogclose}
scroll={'paper'}
// fullScreen
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
TransitionComponent={PopupTransition}
>
<DialogTitle>
@@ -880,6 +890,115 @@ const Details = () => {
<Typography>No Staffs Available</Typography>
)}
</>
) : isMobile ? (
<MobileCardList>
{stafflist.map((val, i) => {
const isSelected = staffarr.find((res) => res.userid == val.userid) ? true : false;
return (
<MobileCard key={i} accent="#662582" selected={isSelected}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar alt="" src={''} sx={{ width: 32, height: 32 }} />
<Stack direction="column">
<Typography variant="subtitle2">{val.firstname}</Typography>
<Typography variant="caption" color="textSecondary">
{val.contactno}
</Typography>
</Stack>
</Stack>
{val.orderdetailid !== orderdetailid ? (
<Checkbox
disabled={val.orderdetailid !== 0}
checked={isSelected}
onClick={(e) => {
console.log(currentshiftobj);
if (currentshiftobj.remaining >= 0) {
if (e.target.checked && currentshiftobj.remaining != 0) {
let arr = staffarr;
arr.push({
userid: val.userid,
orderdetailid,
productid,
shiftid: currentshiftobj.shiftid,
userrate: currentshiftobj.price,
productrate: val.rolecost,
firstname: val.firstname
});
setStaffarr([...arr]);
let obj = currentshiftobj;
obj.assigned++;
obj.remaining = obj.shifts - obj.assigned;
setCurrentshiftobj({ ...obj });
} else if (
currentshiftobj.assigned != currentshiftobj.shifts ||
(currentshiftobj.remaining === 0 && !e.target.checked)
) {
let arr = staffarr;
let index = arr.findIndex((val1) => val1.userid === val.userid);
arr.splice(index, 1);
setStaffarr([...arr]);
let obj = currentshiftobj;
obj.assigned--;
obj.remaining = obj.shifts - obj.assigned;
setCurrentshiftobj({ ...obj });
}
console.log(staffarr);
}
}}
/>
) : (
<Stack direction="row" alignItems="center">
<Chip label="Assigned" color="success" size="small" sx={{ mr: 1 }} />
<Tooltip title="Unassign">
<IconButton
color="error"
onClick={() => {
console.log(val);
unassign(val);
}}
>
<CloseOutlined />
</IconButton>
</Tooltip>
<Tooltip title="Send Notification">
<IconButton
color="info"
onClick={() => {
console.log(val);
notificationpush(val);
}}
>
<NotificationOutlined />
</IconButton>
</Tooltip>
</Stack>
)}
</Stack>
<MobileFieldGrid>
<MobileField label="Category">
<Stack direction="column" alignItems="flex-start" spacing={0.25}>
<Typography variant="caption" color="textSecondary">
{val.cateoryname}
</Typography>
<Chip label={val.subcategoryname} color="info" size="small" variant="light" />
</Stack>
</MobileField>
<MobileField label="Price" value={val.rolecost} />
<MobileField label="Experience" value={`${val.experience} Years`} />
<MobileField label="Level">
<Chip label={val.levelofexperience} size="small" color="primary" variant="light" />
</MobileField>
<MobileField label="City" value={val.city} />
{val.orderid && (
<MobileField label="Order">
<Chip label={val.orderid} color="warning" size="small" variant="light" />
</MobileField>
)}
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
) : (
<TableContainer>
<Table>
@@ -1081,14 +1200,19 @@ const Details = () => {
)}
</DialogContent>
<DialogActions>
<Stack sx={{ mt: 2 }} direction="row" justifyContent="flex-end" spacing={5}>
<Stack
sx={{ mt: 2, width: { xs: '100%', sm: 'auto' } }}
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
spacing={{ xs: 1.5, sm: 5 }}
>
{stafflist.length > 0 && (
<>
<Button1 sx={{ width: '130px' }} variant="contained" onClick={assignok}>
<Button1 sx={{ width: { xs: '100%', sm: '130px' } }} variant="contained" onClick={assignok}>
OK
</Button1>
<Button1
sx={{ width: '130px' }}
sx={{ width: { xs: '100%', sm: '130px' } }}
color="warning"
variant="contained"
onClick={() => {
@@ -1107,7 +1231,7 @@ const Details = () => {
</>
)}
<Button1
sx={{ width: '130px' }}
sx={{ width: { xs: '100%', sm: '130px' } }}
color="error"
variant="contained"
onClick={() => {
@@ -1195,7 +1319,11 @@ const Details = () => {
</Stack>
</Stack>
<Stack direction="row" spacing={2} sx={{ mt: { md: 0, xs: 2 } }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ mt: { md: 0, xs: 2 }, width: { xs: '100%', md: 'auto' } }}
>
{/* <Typography>{dayjs(startdate).$d.toString()}</Typography> */}
{/* <Typography>{startdate}</Typography> */}
{/* <Typography> {dayjs().$d.toString()}</Typography> */}
@@ -1206,7 +1334,7 @@ const Details = () => {
<Button1
variant="outlined"
color="info"
sx={{ borderRadius: '40px' }}
sx={{ borderRadius: '40px', width: { xs: '100%', sm: 'auto' } }}
startIcon={<BorderColorIcon color="info" />}
onClick={(e) => {
e.stopPropagation();
@@ -1269,7 +1397,7 @@ const Details = () => {
setOpen(true);
}
}}
sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 } }}
sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 }, width: { xs: '100%', sm: 'auto' } }}
startIcon={<CancelOutlinedIcon />}
>
Cancel Order
@@ -1418,6 +1546,184 @@ const Details = () => {
})}
</Stack>
</Stack>
{isMobile ? (
<MobileCardList>
{val5.orderdetails.length === 0 && (
<MobileCard accent="#662582">
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
</MobileCard>
)}
{val5.orderdetails.map((row, i) => (
<MobileCard key={i + 1} accent="#662582" sx={{ opacity: row.status == 0 ? '' : '0.7' }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Stack direction="column">
<Typography sx={{ fontSize: 10, fontWeight: 800, color: '#94a3b8' }}>#{i + 1}</Typography>
<Typography variant="subtitle1">{row.productname}</Typography>
</Stack>
<Stack direction="row" alignItems="center">
<Tooltip title="Expand">
<IconButton
aria-label="expand row"
style={{ color: theme.palette.primary.main }}
onClick={() => {
setStafflist([]);
setExpandopen(expandopen[0] === j && expandopen[1] === i ? ['', ''] : [j, i]);
fetchstafflist(row.orderdetailid);
}}
>
{expandopen[0] === j && expandopen[1] === i ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton>
</Tooltip>
{orderstatus === 'cancelled' && (
<IconButton sx={{ minWidth: '10px !important' }} disabled>
<EditTwoTone />
</IconButton>
)}
{row.status === 1 && (
<Tooltip title="Cancelled">
<IconButton sx={{ minWidth: '10px !important' }}>
<CancelOutlinedIcon color="error" />
</IconButton>
</Tooltip>
)}
{row.supplyqty > row.orderqty && (
<Tooltip title="Assigned count is greater than ordered count">
<IconButton color="warning">
<WarningOutlined />
</IconButton>
</Tooltip>
)}
</Stack>
</Stack>
<MobileFieldGrid>
<MobileField label="Start Date">
<Stack direction="column">
<Typography variant="body2">{dayjs(row.starttime).format('MM/DD/YYYY')}</Typography>
<Typography variant="caption">{dayjs(row.starttime).format('hh:mm A')}</Typography>
</Stack>
</MobileField>
<MobileField label="End Date">
<Stack direction="column">
<Typography variant="body2">{dayjs(row.endtime).format('MM/DD/YYYY')}</Typography>
<Typography variant="caption">{dayjs(row.endtime).format('hh:mm A')}</Typography>
</Stack>
</MobileField>
<MobileField label="Unpaid Break" value={row.unpaidbreak || 0} />
<MobileField label="Count">
<Chip label={row.orderqty} color="success" variant="light" size="small" />
</MobileField>
<MobileField label="Assigned">
<Chip
label={row.supplyqty}
color={row.supplyqty === 0 ? 'error' : 'warning'}
variant="light"
size="small"
/>
</MobileField>
<MobileField label="Price" value={`$${row.price}`} />
<MobileField label="Amount" value={`$${row.landingamount}`} />
</MobileFieldGrid>
<Collapse in={expandopen[0] === j && expandopen[1] === i} timeout="auto" unmountOnExit>
<Divider sx={{ my: 1.5 }} />
{stafflist.length === 0 ? (
loading ? (
<Stack alignItems={'center'}>
<CircularProgress />
</Stack>
) : (
<Stack sx={{ p: 1 }}>
<Typography>No Staffs has been Assigned</Typography>
</Stack>
)
) : (
<Stack spacing={1}>
{stafflist.map((val, si) => (
<MobileCard key={si} accent="#0ea5e9">
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Stack direction="column">
<Typography variant="subtitle2">{val.staffname}</Typography>
<Grid>
<Chip label={val.productname} color="info" variant="light" size="small" />
</Grid>
</Stack>
<Stack direction="row">
{val.orderstatus === 'pending' && <Chip label="Pending" color="error" size="small" />}
{val.orderstatus === 'cancelled' && (
<Chip label="Cancelled" color="secondary" size="small" />
)}
{val.orderstatus === 'completed' && (
<Chip label="Completed" color="primary" size="small" />
)}
{val.orderstatus === 'processing' && (
<Chip label="Processing" color="primary" size="small" />
)}
{val.orderstatus === 'assigned' && <Chip label="Assigned" color="warning" size="small" />}
{val.orderstatus === 'confirmed' && (
<Chip label="Confirmed" color="success" size="small" />
)}
{val.orderstatus === 'active' && <Chip label="Active" color="info" size="small" />}
{val.orderstatus === 'closed' && <Chip label="Closed" color="info" size="small" />}
</Stack>
</Stack>
<MobileFieldGrid>
<MobileField label="Start Time">
<Stack direction="column">
<Typography variant="body2">{dayjs(val.Starttime).format('MM/DD/YYYY')}</Typography>
<Typography variant="caption">{dayjs(val.Starttime).format('hh:mm A')}</Typography>
</Stack>
</MobileField>
<MobileField label="End Time">
<Stack direction="column">
<Typography variant="body2">{dayjs(val.Endtime).format('MM/DD/YYYY')}</Typography>
<Typography variant="caption">{dayjs(val.Endtime).format('hh:mm A')}</Typography>
</Stack>
</MobileField>
<MobileField label="Pay Rate" value={val.rolecost} />
<MobileField label="Hours Worked" value={val.hoursworked} />
<MobileField label="Clockin">
<Stack spacing={0.5} alignItems="flex-start">
<Chip
label={val.clockin ? dayjs(val.clockin).format('MM/DD/YYYY') : ''}
color="primary"
variant="light"
size="small"
/>
<Chip
label={val.clockin ? dayjs(val.clockin).format('hh:mm A') : ''}
color="info"
variant="light"
size="small"
/>
</Stack>
</MobileField>
<MobileField label="Clockout">
<Stack spacing={0.5} alignItems="flex-start">
<Chip
label={val.clockout ? dayjs(val.clockout).format('MM/DD/YYYY') : ''}
color="primary"
variant="light"
size="small"
/>
<Chip
label={val.clockout ? dayjs(val.clockout).format('hh:mm A') : ''}
color="info"
variant="light"
size="small"
/>
</Stack>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
</Stack>
)}
</Collapse>
</MobileCard>
))}
</MobileCardList>
) : (
<TableContainer>
<Table>
<TableHead>
@@ -1870,6 +2176,7 @@ const Details = () => {
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
</Grid>
</Fragment>

View File

@@ -5,6 +5,7 @@ import * as XLSX from 'xlsx';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { enqueueSnackbar } from 'notistack';
import {
@@ -65,6 +66,7 @@ import { MdOutlineCloudUpload } from 'react-icons/md';
import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import AnimateButton from 'components/@extended/AnimateButton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import './OrdersRedesign.css';
var utc = require('dayjs/plugin/utc');
@@ -88,6 +90,7 @@ const cellBodySx = {
const MultipleOrders = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const locationRef = useRef(null);
const tenantRef = useRef(null);
const userid = localStorage.getItem('userid');
@@ -711,11 +714,12 @@ const MultipleOrders = () => {
<Box
className="orders-workspace-bg"
sx={{
height: 'calc(100vh - 64px)',
height: { xs: 'auto', md: 'calc(100vh - 64px)' },
minHeight: { xs: 'calc(100vh - 64px)', md: 0 },
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
p: { xs: 1, sm: 1.25 },
overflow: { xs: 'visible', md: 'hidden' },
p: { xs: 0.75, sm: 1.25 },
gap: 1
}}
>
@@ -752,7 +756,7 @@ const MultipleOrders = () => {
<Grid
container
spacing={1.25}
sx={{ flex: 1, minHeight: 0, overflow: 'hidden' }}
sx={{ flex: 1, minHeight: 0, overflow: { xs: 'visible', md: 'hidden' } }}
>
{/* ============================== LEFT 50% : Input fields ============================== */}
<Grid
@@ -760,13 +764,13 @@ const MultipleOrders = () => {
xs={12}
md={6}
sx={{
height: '100%',
height: { xs: 'auto', md: '100%' },
minHeight: 0,
display: 'flex',
flexDirection: 'column',
gap: 2.25,
overflowY: 'auto',
pr: 0.75
gap: { xs: 1.25, md: 2.25 },
overflowY: { xs: 'visible', md: 'auto' },
pr: { xs: 0, md: 0.75 }
}}
>
{/* Card: Setup (Location / Client / Business) */}
@@ -1283,7 +1287,7 @@ const MultipleOrders = () => {
item
xs={12}
md={6}
sx={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}
sx={{ height: { xs: 'auto', md: '100%' }, minHeight: 0, display: 'flex', flexDirection: 'column' }}
>
<Card
className="orders-card"
@@ -1293,7 +1297,7 @@ const MultipleOrders = () => {
flexDirection: 'column',
overflow: 'hidden',
p: 1.25,
maxHeight: 685
maxHeight: { xs: 'none', md: 685 }
}}
>
{/* Preview header (sticky inside card) */}
@@ -1483,7 +1487,101 @@ const MultipleOrders = () => {
{/* Scrollable preview body */}
<Box sx={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
{previewMode === 'drops' && (
{previewMode === 'drops' && isMobile && (
<MobileCardList sx={{ p: 0 }}>
{dropCust.map((customer, index) => (
<MobileCard
key={customer.customerid || customer.firstname || index}
accent="#65387a"
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#1e293b', lineHeight: 1.2 }}>
{index + 1}. {customer.firstname}
</Typography>
<Typography sx={{ fontSize: 12, color: '#64748b', mt: 0.25 }}>
{customer.address}
</Typography>
</Box>
<Tooltip title="Remove">
<IconButton
size="small"
onClick={() => handleCheckboxChange1(customer)}
sx={{ color: '#ef4444', p: 0.5, flexShrink: 0 }}
>
<CloseOutlined style={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Qty">
{uploadType === 0 ? (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: '#0f172a' }}>
{customer.quantity ?? '—'}
</Typography>
) : (
<TextField
size="small"
type="number"
value={customer.quantity || ''}
onChange={(e) => handleQuantityChange(customer.customerid, e.target.value)}
inputProps={{ min: 0 }}
fullWidth
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }}
/>
)}
</MobileField>
<MobileField label="Cash">
{uploadType === 0 ? (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: '#0f172a' }}>
{`${Number(customer.collectionamt || 0).toFixed(2)}`}
</Typography>
) : (
<TextField
size="small"
type="number"
value={customer.collectionamt ? customer.collectionamt : ''}
placeholder="0"
onChange={(e) => {
const v = Number(e.target.value);
handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0);
}}
inputProps={{ min: 0 }}
InputProps={{
startAdornment: <InputAdornment position="start"></InputAdornment>
}}
fullWidth
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }}
/>
)}
</MobileField>
<MobileField label="Km" value={customer.distance} />
<MobileField label="Charge" align="right">
<Typography sx={{ fontSize: 13, fontWeight: 700, color: '#1e293b' }}>
{Number(customer?.totalcharge || 0).toFixed(2)}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
<MobileCard accent="#65387a" sx={{ bgcolor: '#fafbfc' }}>
<MobileFieldGrid columns={2}>
<MobileField label="Total Qty" value={totalQty} />
<MobileField label="Total Cash" value={`${Number(totalCash).toFixed(2)}`} />
<MobileField label="Total Km" value={totaldist} />
<MobileField label="Total Charge" align="right">
<Typography sx={{ fontSize: 14, fontWeight: 800, color: '#65387a' }}>
{Number(totalAmt).toFixed(2)}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
</MobileCardList>
)}
{previewMode === 'drops' && !isMobile && (
<TableContainer
component={Paper}
sx={{ borderRadius: '10px', border: '1px solid #eef2f6', boxShadow: 'none' }}
@@ -1604,7 +1702,37 @@ const MultipleOrders = () => {
</TableContainer>
)}
{previewMode === 'preview' && (
{previewMode === 'preview' && isMobile && (
<MobileCardList sx={{ p: 0 }}>
{users.map((u, i) => (
<MobileCard
key={i}
accent="#d97706"
header={
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#1e293b', lineHeight: 1.2 }}>
{i + 1}. {u.firstname || '—'}
</Typography>
<Typography sx={{ fontSize: 12, color: '#64748b', mt: 0.25 }}>
{u.address || '—'}
</Typography>
</Box>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Contact" value={u.contactno || '—'} />
<MobileField label="Qty" value={u.quantity ?? '—'} />
<MobileField
label="Cash"
value={u.collectionamt != null ? `${Number(u.collectionamt).toFixed(2)}` : '—'}
/>
</MobileFieldGrid>
</MobileCard>
))}
</MobileCardList>
)}
{previewMode === 'preview' && !isMobile && (
<TableContainer
component={Paper}
sx={{ borderRadius: '10px', border: '1px solid #eef2f6', boxShadow: 'none' }}
@@ -1819,9 +1947,10 @@ const MultipleOrders = () => {
open={isCustomerOpen}
onClose={() => setIsCustomerOpen(false)}
fullWidth
fullScreen={isMobile}
sx={{
'& .MuiDialog-paper': {
borderRadius: '16px',
borderRadius: { xs: 0, sm: '16px' },
overflow: 'hidden'
}
}}
@@ -1863,7 +1992,7 @@ const MultipleOrders = () => {
</Stack>
</DialogTitle>
<Divider />
<DialogContent sx={{ p: 2.5, bgcolor: '#fafbfc', minHeight: 400, maxHeight: 600 }}>
<DialogContent sx={{ p: 2.5, bgcolor: '#fafbfc', minHeight: 400, maxHeight: { xs: 'none', sm: 600 } }}>
{customerlist?.length === 0 ? (
<Stack alignItems="center" justifyContent="center" sx={{ minHeight: 300 }}>
<Empty description="No saved customers found for this client" />

View File

@@ -20,10 +20,12 @@ import {
Accordion,
AccordionSummary,
AccordionActions,
AccordionDetails
AccordionDetails,
useMediaQuery
} from '@mui/material';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { useTheme } from '@mui/material/styles';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { useLocation, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import MainCard from 'components/MainCard';
@@ -60,6 +62,7 @@ dayjs.extend(utc);
const OptimisedOrderPreview = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const location = useLocation();
console.log('location.state', location.state);
@@ -266,7 +269,13 @@ const OptimisedOrderPreview = () => {
<CircularLoader />
</>
)}
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{}}>
<Stack
direction={{ xs: 'column', md: 'row' }}
alignItems={{ xs: 'stretch', md: 'center' }}
justifyContent="space-between"
spacing={{ xs: 1.5, md: 0 }}
sx={{}}
>
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
@@ -312,7 +321,7 @@ const OptimisedOrderPreview = () => {
</li>
);
}}
style={{ width: 400 }}
sx={{ width: { xs: '100%', md: 400 } }}
renderInput={(params) => (
<TextField
{...params}
@@ -405,8 +414,117 @@ const OptimisedOrderPreview = () => {
</Tooltip>
</Stack>
</AccordionSummary>
<AccordionDetails>
<TableContainer sx={{ p: 0, m: 0 }}>
<AccordionDetails sx={{ p: { xs: 0, md: 1 } }}>
{isMobile ? (
<MobileCardList>
{orders.map((val, i) => {
const typeAccent =
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#662582';
return (
<MobileCard key={i} accent={typeAccent}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Chip size="small" label={`#${i + 1}`} variant="light" color="primary" />
<Typography variant="subtitle2" noWrap sx={{ minWidth: 0 }}>
{`${val.locationname}-(${val.locationsuburb})`}
</Typography>
</Stack>
<Chip
size="small"
label={val.ordertype}
icon={
val.ordertype === 'Economy' ? (
<EnergySavingsLeafIcon />
) : val.ordertype === 'Risky' ? (
<WarningIcon />
) : (
<BoltIcon />
)
}
color={val.ordertype === 'Economy' ? 'success' : val.ordertype === 'Risky' ? 'error' : 'primary'}
variant="light"
/>
</Stack>
<MobileFieldGrid>
<MobileField label="Order Id" value={val.orderid} />
<MobileField label="Rider">
<Typography sx={{ fontSize: 13, fontWeight: 600 }} noWrap>
{val.rider}
</Typography>
<Typography variant="caption">ID : {val.userid}</Typography>
</MobileField>
<MobileField label="Pickup">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="caption" noWrap>
{val.pickupcustomer}
</Typography>
<Typography variant="caption" noWrap>
{val.pickupcontactno}
</Typography>
<Tooltip title={val.pickupaddress}>
<Typography variant="caption" noWrap sx={{ cursor: 'pointer' }}>
{val.pickupsuburb || val.pickupaddress.slice(0, 20)}
</Typography>
</Tooltip>
<Chip
size="small"
label={dayjs(val.pickupslot).format('DD/MM/YYYY hh:mm A')}
variant="light"
sx={{ color: '#0a803b', bgcolor: '#c3f3c7', alignSelf: 'flex-start' }}
/>
</Stack>
</MobileField>
<MobileField label="Delivery">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="caption" noWrap>
{val.deliverycustomer}
</Typography>
<Typography variant="caption" noWrap>
{val.deliverycontactno}
</Typography>
<Tooltip title={val.deliveryaddress}>
<Typography variant="caption" noWrap sx={{ cursor: 'pointer' }}>
{val.deliverysuburb || val.deliveryaddress.slice(0, 20)}
</Typography>
</Tooltip>
<Chip
size="small"
label={dayjs(val.expecteddeliverytime).format('DD/MM/YYYY hh:mm A')}
variant="light"
sx={{ color: '#DD2C00', background: '#FBE9E7', alignSelf: 'flex-start' }}
/>
</Stack>
</MobileField>
{val.ordernotes ? <MobileField full label="Notes" value={val.ordernotes} /> : null}
<MobileField label="Profit">
<Chip
size="small"
label={`${parseFloat(val?.profit).toFixed(2)}`}
variant="light"
sx={{ color: '#009688', bgcolor: '#b2dfdb' }}
/>
</MobileField>
<MobileField label="Charges">
<Chip
size="small"
label={`${val.deliverycharge.toFixed(2)} `}
variant="light"
sx={{ color: '#0074e7', bgcolor: '#cce3fa' }}
/>
</MobileField>
<MobileField label="KMS">
<Chip size="small" label={`${val.kms} km`} color="error" variant="light" />
</MobileField>
<MobileField label="Cumulative KMS">
<Chip size="small" label={`${val.cumulativekms} km`} color="success" variant="light" />
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
) : (
<TableContainer sx={{ p: 0, m: 0 }}>
<Table stickyHeader>
<TableHead>
<TableRow>
@@ -579,16 +697,24 @@ const OptimisedOrderPreview = () => {
))}
</TableBody>
</Table>
</TableContainer>
</TableContainer>
)}
</AccordionDetails>
</Accordion>
);
})}
<Divider />
<Stack display={'flex'} flexDirection={'row'} gap={2} alignItems={'center'} justifyContent={'end'} sx={{ p: 2 }}>
<Stack
display={'flex'}
flexDirection={{ xs: 'column', sm: 'row' }}
gap={2}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent={'end'}
sx={{ p: 2 }}
>
<Button
sx={{}}
sx={{ width: { xs: '100%', sm: 'auto' } }}
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
@@ -598,7 +724,12 @@ const OptimisedOrderPreview = () => {
>
Back
</Button>
<Button sx={{ my: 2 }} variant="contained" disabled={aiMode === 0 && (!rider || !payment)} onClick={handleFinalCreateDelivery}>
<Button
sx={{ my: { xs: 0, sm: 2 }, width: { xs: '100%', sm: 'auto' } }}
variant="contained"
disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleFinalCreateDelivery}
>
Assign Orders
</Button>
</Stack>

View File

@@ -110,6 +110,7 @@ import {
getallriders
} from '../../api/api';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { useNavigate } from 'react-router';
import CSVExport from 'components/third-party/ReactTable';
import Dispatch from '../dispatch/Dispatch';
@@ -204,6 +205,7 @@ const ORDERS_STATUS_TABS = [
const Orders = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const loadMoreRef = useRef();
const containerRef = useRef();
@@ -1419,6 +1421,233 @@ const Orders = () => {
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
{isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{rows?.length === 0 && !fetchOrdersIsLoading && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: dtSoft('#94a3b8'), color: DT.textMuted }}>
<MdLocalShipping size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No {currentStatus} orders
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
</Typography>
</Stack>
)}
{fetchOrdersIsLoading && (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading orders
</Typography>
</Stack>
)}
{rows?.map((row, index) => {
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
const handleCheckbox = (e) => {
if (!appId) {
OpenToast('Please select a location first!', 'warning', 2000);
locationRef.current?.focus();
return;
}
if (e.target.checked) {
setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]);
} else {
setDeliverylist((prev) =>
prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 }))
);
}
};
const meta = ROW_STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
color: BRAND,
icon: MdHistoryToggleOff
};
const StatusIcon = meta.icon;
const chipSx = (c) => ({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.25,
borderRadius: 999,
bgcolor: dtTint(c),
color: c,
fontWeight: 700,
fontSize: 11,
border: `1px solid ${dtEdge(c)}`,
whiteSpace: 'nowrap'
});
return (
<MobileCard
key={row.sno}
accent={meta.color}
selected={isItemSelected}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
{currentStatus === 'created' && (
<Checkbox
size="small"
sx={{ p: 0.25, color: dtEdge(BRAND), '&.Mui-checked': { color: BRAND } }}
onChange={handleCheckbox}
checked={isItemSelected}
/>
)}
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: dtTint(meta.color),
border: `1px solid ${dtEdge(meta.color)}`,
color: meta.color,
fontSize: 10.5,
fontWeight: 800,
whiteSpace: 'nowrap'
}}
>
<StatusIcon size={11} /> {meta.label}
</Box>
</Stack>
{row.orderstatus === 'created' && (
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
{row.deliverytype === 'C' && (
<IconButton
size="small"
onClick={() => {
if (productCollapse?.orderid === row.orderid) {
setProductCollapse(null);
} else {
setProductCollapse(row);
}
}}
sx={{
borderRadius: 999,
bgcolor: dtTint('#06b6d4'),
color: '#06b6d4',
border: `1px solid ${dtEdge('#06b6d4')}`,
'&:hover': { bgcolor: dtSoft('#06b6d4') }
}}
>
{productCollapse?.orderid === row.orderid ? (
<KeyboardArrowUpOutlined fontSize="small" />
) : (
<KeyboardArrowDownOutlined fontSize="small" />
)}
</IconButton>
)}
<IconButton
size="small"
disabled={isItemSelected}
onClick={() => {
setCancelDialog(true);
setOrderheaderid(row.orderheaderid);
}}
sx={{
borderRadius: 999,
bgcolor: dtTint('#ef4444'),
color: '#ef4444',
border: `1px solid ${dtEdge('#ef4444')}`,
'&:hover': { bgcolor: dtSoft('#ef4444') },
'&.Mui-disabled': { color: theme.palette.secondary.main }
}}
>
<CloseOutlined />
</IconButton>
</Stack>
)}
</Stack>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.tenantname}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Order / Location" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {dayjs(row.pickupslot).utc().format('DD/MM/YYYY')} {dayjs(row.pickupslot).format('hh:mm A')}
</Typography>
</MobileField>
<MobileField label="Pickup">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupcontactno}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupsuburb || row.pickupaddress}
</Typography>
</MobileField>
<MobileField label="Drop">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverycontactno}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverysuburb || row.deliveryaddress}
</Typography>
</MobileField>
<MobileField label="Qty" value={row.quantity || '—'} />
<MobileField label="KMS">
<Box sx={chipSx('#06b6d4')}>{row.kms || 0} km</Box>
</MobileField>
<MobileField label="COD">
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
</Typography>
</MobileField>
<MobileField label="Charges">
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.deliverycharge ? '#ef4444' : DT.textMuted }}>
{row.deliverycharge ? `${row.deliverycharge.toFixed(2)}` : '—'}
</Typography>
</MobileField>
{row.ordernotes && (
<MobileField label="Notes" full>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.ordernotes}
</Typography>
</MobileField>
)}
</MobileFieldGrid>
</MobileCard>
);
})}
{rows?.length != 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more orders
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: { xs: 960, md: 1180 } }}>
<TableHead>
<TableRow
@@ -1770,6 +1999,7 @@ const Orders = () => {
)}
</TableBody>
</Table>
)}
</TableContainer>
</Paper>
{/* ============================================= || Orders Preview Dialog | ============================================= */}

View File

@@ -30,8 +30,10 @@ import {
TextField,
Tooltip,
Typography,
Autocomplete
Autocomplete,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MdAssignment,
MdMyLocation,
@@ -71,6 +73,7 @@ import LocationAutocomplete from 'components/nearle_components/LocationAutocompl
import dayjs from 'dayjs';
import { OpenToast } from 'components/third-party/OpenToast';
import TableLoader from 'components/nearle_components/TableLoader';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
@@ -418,6 +421,8 @@ function kalmanSmoothGps(pings, options = {}) {
}
export default function OrdersDetails() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const loadMoreRef = useRef();
const containerRef = useRef();
const locationRef = useRef(null);
@@ -1313,6 +1318,267 @@ export default function OrdersDetails() {
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
{isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{fetchDeliveriesIsLoading ? (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading orders
</Typography>
</Stack>
) : rows?.length == 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdAssignment size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No orders to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
</Typography>
</Stack>
) : (
rows?.map((row, index) => {
const statusKey = String(row.orderstatus || '').toLowerCase();
const rowStatusMeta = STATUS_META[statusKey] || {
label: row.orderstatus || '—',
color: BRAND,
icon: MdAssignment
};
const StatusIcon = rowStatusMeta.icon;
const cancelled = statusKey === 'cancelled';
const isDelivered = row.orderstatus === 'delivered';
return (
<MobileCard
key={row.deliveryid || `${row.orderid}-${index}`}
accent={rowStatusMeta.color}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
<AccentAvatar color={BRAND} size={32}>
<MdGroups size={16} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.tenantname}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 700 }}>
#{row.orderid}
</Typography>
</Stack>
</Stack>
<Tooltip
title={isDelivered ? 'View rider route' : 'Available for delivered orders'}
placement="top"
>
<span>
<IconButton
size="small"
disabled={!isDelivered}
onClick={() => {
if (isDelivered) {
getdeliverylogs(row.deliveryid);
setMapTenant(row);
}
}}
sx={{
flexShrink: 0,
bgcolor: isDelivered ? soft(BRAND) : soft('#94a3b8'),
color: isDelivered ? BRAND : DT.textMuted,
border: `1px solid ${isDelivered ? edge(BRAND) : edge('#94a3b8')}`,
'&:hover': {
bgcolor: isDelivered ? BRAND : soft('#94a3b8'),
color: isDelivered ? '#fff' : DT.textMuted
}
}}
>
<MdMap size={14} />
</IconButton>
</span>
</Tooltip>
</Stack>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint(rowStatusMeta.color),
border: `1px solid ${edge(rowStatusMeta.color)}`,
color: rowStatusMeta.color,
fontSize: 11,
fontWeight: 800
}}
>
<StatusIcon size={12} /> {rowStatusMeta.label}
</Box>
{row.ridername && (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#8b5cf6'),
border: `1px solid ${edge('#8b5cf6')}`,
color: '#8b5cf6',
fontSize: 11,
fontWeight: 800
}}
>
<MdDirectionsBike size={12} /> {row.ridername}
</Box>
)}
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')}
</Typography>
</Stack>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Pickup">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupcontactno}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')}
</Typography>
{row.applocation && (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.75,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 10,
fontWeight: 800,
width: 'fit-content'
}}
>
<MdPlace size={11} /> {row.applocation}
</Box>
)}
</Stack>
</MobileField>
<MobileField label="Drop">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverycontactno}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')}
</Typography>
</Stack>
</MobileField>
<MobileField label="Assigned">
<StampCell value={row.assigntime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="Accepted">
<StampCell value={row.acceptedtime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="Arrived">
<StampCell value={row.arrivaltime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="Picked">
<StampCell
value={row.pickuptime}
formatDate={formatDate}
formatTime={formatTime}
success={row.deliverystatus === 'active'}
/>
</MobileField>
<MobileField label="Active">
<StampCell value={row.starttime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="Delivered">
<StampCell value={row.deliverytime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="Cancelled">
<StampCell value={row.canceltime} formatDate={formatDate} formatTime={formatTime} />
</MobileField>
<MobileField label="KMS · plan / act / rider" full>
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
<MetricPill
color="#ef4444"
icon={<MdStraighten size={11} />}
label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
tooltip="KMS"
/>
<MetricPill
color="#10b981"
icon={<MdStraighten size={11} />}
label={`${row.cumulativekms ?? 0} km`}
tooltip="Actual KMS"
/>
<MetricPill
color="#0ea5e9"
icon={<MdStraighten size={11} />}
label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
tooltip="Rider KMS"
/>
</Stack>
</MobileField>
<MobileField label="Charges · chg / amt" full>
<Stack direction="row" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>
<MetricPill
color="#ef4444"
icon={<MdCurrencyRupee size={11} />}
label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`}
tooltip="Delivery Charge"
/>
<MetricPill
color="#10b981"
icon={<MdCurrencyRupee size={11} />}
label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`}
tooltip="Delivery Amount"
/>
</Stack>
</MobileField>
{row.ordernotes && (
<MobileField label="Notes" full>
<Stack direction="row" spacing={0.5} alignItems="flex-start">
<MdNoteAlt size={12} color={DT.textMuted} style={{ marginTop: 2, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.ordernotes}
</Typography>
</Stack>
</MobileField>
)}
</MobileFieldGrid>
</MobileCard>
);
})
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: 1600 }}>
<TableHead>
<TableRow
@@ -1659,6 +1925,7 @@ export default function OrdersDetails() {
)}
</TableBody>
</Table>
)}
<Divider />
{rows?.length !== 0 && (
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}>
@@ -1682,7 +1949,8 @@ export default function OrdersDetails() {
onClose={() => setReportDialog(false)}
fullWidth
maxWidth="sm"
PaperProps={{ sx: { borderRadius: 2.5, overflow: 'hidden' } }}
fullScreen={isMobile}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 }, overflow: 'hidden' } }}
>
<DialogTitle
sx={{

View File

@@ -22,7 +22,9 @@ import {
TableRow,
TextField,
Tooltip,
Typography
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdAssignment,
@@ -52,6 +54,7 @@ import Loader from 'components/Loader';
import DateFilterDialog from 'components/DateFilterDialog';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { OpenToast } from 'components/third-party/OpenToast';
@@ -198,6 +201,9 @@ function formatNumberToRupees(value) {
// Orders Summary
// ============================================================================
export default function OrdersReport() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const locationRef = useRef(null);
const tenantRef = useRef(null);
@@ -716,6 +722,251 @@ export default function OrdersReport() {
background: '#fff'
}}
>
{isMobile ? (
<>
{isLoadingReports && (
<MobileCardList>
{[0, 1, 2, 3].map((i) => (
<MobileCard key={i} accent={DT.borderSubtle}>
<Box sx={{ height: 96 }} />
</MobileCard>
))}
</MobileCardList>
)}
{!isLoadingReports && filteredRows.length === 0 && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdAssignment size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No {isLocationGroup ? 'locations' : 'tenants'} to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', maxWidth: 360 }}>
{debouncedSearch
? 'Try a different keyword.'
: isErrorReports
? 'Something went wrong fetching the summary. Try a different filter.'
: 'Pick a zone, tenant, or date range to load the summary.'}
</Typography>
</Stack>
)}
{!isLoadingReports && filteredRows.length > 0 && (
<MobileCardList>
{filteredRows.map((row, index) => {
const rowKey = isLocationGroup ? row.locationname : row.tenantname;
const isOpen = openRow === rowKey;
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
const rowAccent = isLocationGroup ? '#0ea5e9' : BRAND;
return (
<MobileCard
key={`${rowKey}-${index}`}
accent={rowAccent}
selected={isOpen}
header={
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={rowAccent} size={36}>
{isLocationGroup ? <MdLocationOn size={18} /> : <MdStore size={18} />}
</AccentAvatar>
<Stack sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{(isLocationGroup ? row.locationname : row.tenantname) || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{isLocationGroup ? row.locationid : row.tenantid}
</Typography>
</Stack>
<Tooltip title={isOpen ? 'Hide rider breakdown' : 'View rider breakdown'} placement="top">
<IconButton
size="small"
onClick={() => {
const isOpening = !isOpen;
setOpenRow(isOpening ? rowKey : null);
if (!isOpening) return;
setRidersdata([]);
if (isLocationGroup) {
getriderlocationsummary(row.locationid);
} else {
getuserreportsummary(row.tenantid);
}
}}
sx={{
flexShrink: 0,
bgcolor: isOpen ? BRAND : soft(BRAND),
color: isOpen ? '#fff' : BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
{isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid columns={3}>
<MobileField label="All" align="center">
<CountCell value={row.totalorders} color={BRAND} icon={<MdLocalShipping size={11} />} />
</MobileField>
<MobileField label="Ord Pending" align="center">
<CountCell value={row.Orderspending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
</MobileField>
<MobileField label="Ord Cancelled" align="center">
<CountCell value={row.orderscancelled} color="#ef4444" icon={<MdCancel size={11} />} />
</MobileField>
<MobileField label="Ord Completed" align="center">
<CountCell value={row.orderscompleted} color="#10b981" icon={<MdCheckCircle size={11} />} />
</MobileField>
<MobileField label="Del Pending" align="center">
<CountCell value={row.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
</MobileField>
<MobileField label="Del Cancelled" align="center">
<CountCell value={row.deliveriescancelled} color="#ef4444" icon={<MdCancel size={11} />} />
</MobileField>
<MobileField label="Del Completed" align="center">
<CountCell value={row.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={11} />} />
</MobileField>
<MobileField label="Collection" align="center">
<MetricPill
color="#f59e0b"
icon={<MdCurrencyRupee size={11} />}
label={Number(row.collectionamt || 0).toFixed(2)}
tooltip="Collection Amount"
minWidth={90}
/>
</MobileField>
<MobileField label="Amount" align="center">
<MetricPill
color={BRAND}
icon={<MdCurrencyRupee size={11} />}
label={formatNumberToRupees(amount).replace('₹', '').trim()}
tooltip="Total Amount"
minWidth={100}
/>
</MobileField>
<MobileField label="KMS" align="center">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={11} />}
label={`${Number(row.kms || 0).toFixed(2)} km`}
tooltip="KMS"
/>
</MobileField>
<MobileField label="Actual KMS" align="center">
<MetricPill
color="#10b981"
icon={<MdStraighten size={11} />}
label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
tooltip="Actual KMS"
/>
</MobileField>
</MobileFieldGrid>
{/* Collapsible rider breakdown — mobile */}
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<Box sx={{ mt: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color={BRAND} size={24} selected>
<MdGroups size={12} />
</AccentAvatar>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Rider Breakdown
</Typography>
</Stack>
{!loading && (!ridersdata || ridersdata.length === 0) ? (
<Typography variant="caption" sx={{ color: DT.textMuted, display: 'block', py: 1, textAlign: 'center' }}>
No rider activity for this row.
</Typography>
) : (
<Stack spacing={1}>
{ridersdata.map((sub, sidx) => {
const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0);
return (
<MobileCard key={`${sub.userid}-${sidx}`} accent={BRAND}>
<Stack direction="row" alignItems="center" spacing={0.75}>
<AccentAvatar color={BRAND} size={28}>
<MdPerson size={14} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{sub.ridercontact || `ID #${sub.userid}`}
</Typography>
</Stack>
</Stack>
<MobileFieldGrid columns={3}>
<MobileField label="Orders" align="center">
<CountCell value={sub.orderscreated} color={BRAND} icon={<MdLocalShipping size={10} />} />
</MobileField>
<MobileField label="Deliveries" align="center">
<CountCell value={sub.totalorders} color="#0ea5e9" icon={<MdLocalShipping size={10} />} />
</MobileField>
<MobileField label="Pending" align="center">
<CountCell value={sub.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={10} />} />
</MobileField>
<MobileField label="Cancelled" align="center">
<CountCell value={sub.deliveriescancelled} color="#ef4444" icon={<MdCancel size={10} />} />
</MobileField>
<MobileField label="Completed" align="center">
<CountCell value={sub.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={10} />} />
</MobileField>
<MobileField label="Collection" align="center">
<MetricPill
color="#f59e0b"
icon={<MdCurrencyRupee size={10} />}
label={Number(sub.collectionamt || 0).toFixed(2)}
tooltip="Collection"
minWidth={80}
/>
</MobileField>
<MobileField label="KMS" align="center">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={10} />}
label={`${Number(sub.kms || 0).toFixed(2)} km`}
tooltip="KMS"
/>
</MobileField>
<MobileField label="Actual KMS" align="center">
<MetricPill
color="#10b981"
icon={<MdStraighten size={10} />}
label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`}
tooltip="Actual KMS"
/>
</MobileField>
<MobileField label="Amount" align="center">
<MetricPill
color={BRAND}
icon={<MdCurrencyRupee size={10} />}
label={formatNumberToRupees(subAmount).replace('₹', '').trim()}
tooltip="Total Amount"
minWidth={100}
/>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</Stack>
)}
</Box>
</Collapse>
</MobileCard>
);
})}
</MobileCardList>
)}
</>
) : (
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 280px)', md: 'calc(100vh - 240px)' },
@@ -1162,6 +1413,7 @@ export default function OrdersReport() {
</TableBody>
</Table>
</TableContainer>
)}
{/* ============================================= || Total Bar || ============================================= */}
{filteredRows.length > 0 && (

View File

@@ -25,6 +25,7 @@ import { useQuery } from '@tanstack/react-query';
import { fetchRidersLogs } from 'pages/api/api';
import RiderLocationMap from './RiderLocationMap';
import MainCard from 'components/MainCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs';
import error500 from 'assets/images/maintenance/Error500.png';
@@ -34,6 +35,7 @@ const drawerWidth = 350;
const RidersLogs = () => {
const theme = useTheme();
const isDesktop = useMediaQuery('(min-width:900px)');
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(false);
const [selectedRiders, setSelectedRiders] = useState([]);
const [riderSearch, setRiderSearch] = useState('');
@@ -74,7 +76,8 @@ const RidersLogs = () => {
ModalProps={{ keepMounted: true }}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
width: isMobile ? '100vw' : drawerWidth,
maxWidth: isMobile ? '100vw' : drawerWidth,
position: 'absolute',
left: 0,
top: 0,
@@ -142,7 +145,8 @@ const RidersLogs = () => {
<Divider />
</Fragment>
))
: riders?.map((row) => {
: !isMobile &&
riders?.map((row) => {
return (
<Fragment key={row.userid}>
<ListItem
@@ -210,6 +214,61 @@ const RidersLogs = () => {
);
})}
</List>
{/* Mobile: rider rows rendered as app-style cards (same selection behaviour) */}
{isMobile && !ridersIsLoading && !riderIsFetching && (
<MobileCardList>
{riders?.map((row) => {
const isActive = row.status == 'active';
const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
return (
<MobileCard
key={row.userid}
accent={isActive ? '#10b981' : '#ef4444'}
selected={isSelected}
header={
<Stack direction="row" alignItems="flex-start" spacing={1}>
<Checkbox
sx={{
p: 0.5,
color: isActive ? 'green' : 'red',
'&.Mui-checked': { color: isActive ? 'green' : 'red' }
}}
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedRiders([row]);
} else {
setSelectedRiders(riders);
}
}}
/>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography noWrap sx={{ fontWeight: 600 }}>
{row.username?.slice(0, 25) || ''}
{row.username?.length > 25 && '...'}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{row.contactno || '##########'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="User ID">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: isActive ? 'success.main' : 'error.main' }} noWrap>
{row.userid}
</Typography>
</MobileField>
<MobileField label="Status" value={isActive ? 'Active' : 'Inactive'} />
<MobileField label="Last Log" value={dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')} full />
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</Drawer>
{/* AppBar */}

View File

@@ -22,7 +22,9 @@ import {
TableHead,
TableRow,
Tooltip,
Typography
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdDirectionsBike,
@@ -53,6 +55,7 @@ import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import RidersRoutes from './RidersRoutes';
import { OpenToast } from 'components/third-party/OpenToast';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with deliveries / tenants / customers / pricing /
@@ -182,6 +185,9 @@ function formatNumberToRupees(value) {
// ==============================|| Riders Summary ||============================== //
export default function RidersSummary() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [locaName, setLocoName] = useState('All');
@@ -584,6 +590,249 @@ export default function RidersSummary() {
background: '#fff'
}}
>
{isMobile ? (
<MobileCardList scroll>
{isLoadingReports && (
<Stack alignItems="center" sx={{ py: 4 }}>
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
Loading riders
</Typography>
</Stack>
)}
{(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdDirectionsBike size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'}
</Typography>
</Stack>
) : (
filteredRows.map((row, index) => {
const isOpen = openRow === row.userid;
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
const riderName = `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || '—';
return (
<MobileCard
key={row.userid || index}
accent={BRAND}
selected={isOpen}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
#{String(index + 1).padStart(2, '0')} · ID #{row.userid}
</Typography>
</Stack>
</Stack>
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
<Tooltip title="View planned route" placement="top">
<IconButton
size="small"
onClick={() => {
setSelectedRider({
userid: row?.userid,
name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}`
});
setLogDetails(null);
setMapOpen(true);
getuserdeliverylogs(row?.userid);
}}
sx={{
bgcolor: soft('#0ea5e9'),
color: '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
>
<MdMap size={14} />
</IconButton>
</Tooltip>
<Tooltip title={isOpen ? 'Collapse breakdown' : 'Expand breakdown'} placement="top">
<IconButton
size="small"
onClick={() => {
const isOpening = !isOpen;
setOpenRow(isOpening ? row.userid : null);
if (isOpening) fetchTenantSummary(row.userid);
}}
sx={{
bgcolor: isOpen ? BRAND : soft(BRAND),
color: isOpen ? '#fff' : BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
{isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
</IconButton>
</Tooltip>
</Stack>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Orders">
<CountCell value={row.totalorders} color={BRAND} icon={<MdLocalShipping size={11} />} />
</MobileField>
<MobileField label="Delivered">
<CountCell value={row.delivered} color="#10b981" icon={<MdCheckCircle size={11} />} />
</MobileField>
<MobileField label="Pending">
<CountCell value={row.pending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
</MobileField>
<MobileField label="Cancelled">
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
</MobileField>
<MobileField label="KMS">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={11} />}
label={`${Number(row.kms || 0).toFixed(2)} km`}
tooltip="KMS"
/>
</MobileField>
<MobileField label="Actual KMS">
<MetricPill
color="#10b981"
icon={<MdStraighten size={11} />}
label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
tooltip="Actual KMS"
/>
</MobileField>
<MobileField label="Amount" full>
<MetricPill
color={BRAND}
icon={<MdCurrencyRupee size={11} />}
label={formatNumberToRupees(amount).replace('₹', '').trim()}
tooltip="Total Amount"
minWidth={100}
/>
</MobileField>
</MobileFieldGrid>
{/* per-tenant breakdown */}
{isOpen && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<Paper
elevation={0}
sx={{
mt: 1.25,
borderRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
boxShadow: DT.shadowSoft
}}
>
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{
px: 1.5,
py: 1,
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
borderBottom: `1px solid ${DT.borderSubtle}`
}}
>
<AccentAvatar color={BRAND} size={24} selected>
<MdGroups size={12} />
</AccentAvatar>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Tenant Breakdown
</Typography>
</Stack>
<Stack divider={<Divider />}>
{loading && (
<Typography variant="caption" sx={{ color: DT.textMuted, textAlign: 'center', py: 2 }}>
Loading
</Typography>
)}
{!loading && (!tenantData || tenantData.length === 0) ? (
<Typography variant="caption" sx={{ color: DT.textMuted, textAlign: 'center', py: 2 }}>
No tenant breakdown available.
</Typography>
) : (
tenantData?.map((sub, sidx) => (
<Box key={`${sub.tenantname}-${sidx}`} sx={{ p: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mb: 1 }}>
<AccentAvatar color="#0ea5e9" size={24}>
<MdGroups size={12} />
</AccentAvatar>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{sub.tenantname || '—'}
</Typography>
</Stack>
<MobileFieldGrid columns={2}>
<MobileField label="All">
<CountCell value={sub.totalorders} color={BRAND} icon={<MdLocalShipping size={10} />} />
</MobileField>
<MobileField label="Completed">
<CountCell value={sub.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={10} />} />
</MobileField>
<MobileField label="Pending">
<CountCell value={sub.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={10} />} />
</MobileField>
<MobileField label="Cancelled">
<CountCell value={sub.deliveriescancelled} color="#ef4444" icon={<MdCancel size={10} />} />
</MobileField>
<MobileField label="KMS">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={10} />}
label={`${Number(sub.kms || 0).toFixed(2)} km`}
tooltip="KMS"
/>
</MobileField>
<MobileField label="Actual KMS">
<MetricPill
color="#10b981"
icon={<MdStraighten size={10} />}
label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`}
tooltip="Actual KMS"
/>
</MobileField>
<MobileField label="Amount" full>
<MetricPill
color={BRAND}
icon={<MdCurrencyRupee size={10} />}
label={formatNumberToRupees(
Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0)
)
.replace('₹', '')
.trim()}
tooltip="Total Amount"
minWidth={100}
/>
</MobileField>
</MobileFieldGrid>
</Box>
))
)}
</Stack>
</Paper>
</Collapse>
)}
</MobileCard>
);
})
)}
</MobileCardList>
) : (
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
@@ -940,6 +1189,7 @@ export default function RidersSummary() {
</TableBody>
</Table>
</TableContainer>
)}
{/* ============================================= || Total Bar || ============================================= */}
{filteredRows.length > 0 && (

View File

@@ -29,8 +29,11 @@ import {
CircularProgress,
DialogTitle,
FormLabel,
DialogActions
DialogActions,
useMediaQuery,
useTheme
} from '@mui/material';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { Autocomplete as Autocomplete1 } from '@mui/material';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -178,6 +181,8 @@ const Requests = () => {
};
function EnhancedTable() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [order, setOrder] = React.useState('asc');
const [orderBy, setOrderBy] = React.useState('calories');
const [selected, setSelected] = React.useState([]);
@@ -741,9 +746,10 @@ const Requests = () => {
open={dialogopen}
onClose={dialogclose}
scroll={'paper'}
// fullScreen
fullScreen={isMobile}
maxWidth="sm"
TransitionComponent={PopupTransition}
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
>
<DialogTitle>Create Request</DialogTitle>
@@ -892,7 +898,77 @@ const Requests = () => {
width: '100%'
}}
>
<TableContainer sx={{ width: '100%', borderBottom: 1, borderColor: 'divider' }}>
{isMobile && (
<MobileCardList scroll>
{loading &&
[0, 1, 2, 3, 4].map((item) => (
<MobileCard key={item} accent="#662582">
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="circular" width={32} height={32} />
<Stack sx={{ flex: 1 }}>
<Skeleton animation="wave" width="60%" />
<Skeleton animation="wave" width="40%" />
</Stack>
</Stack>
<MobileFieldGrid>
<MobileField label="Amount" value={<Skeleton animation="wave" width={50} />} />
<MobileField label="Ref No" value={<Skeleton animation="wave" width={50} />} />
</MobileFieldGrid>
</MobileCard>
))}
{!loading &&
visibleRows.map((row, index) => {
const isItemSelected = isSelected(row.sno);
return (
<MobileCard
key={row.sno}
accent="#662582"
selected={isItemSelected}
onClick={(event) => handleClick(event, row.sno)}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#66258218', color: '#662582', fontSize: 13 }}>
{row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'}
</Avatar>
<Stack sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#0f172a' }} noWrap>
{row.requestor || '—'}
</Typography>
<Typography sx={{ fontSize: 11, color: '#94a3b8' }} noWrap>
#{row.sno}
</Typography>
</Stack>
</Stack>
{row.amount != null && (
<Chip label={row.amount} size="small" sx={{ bgcolor: '#66258218', color: '#662582', fontWeight: 700 }} />
)}
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Bank" value={row.bankname} />
<MobileField label="Account No" value={row.accountno} />
<MobileField label="IFSC" value={row.ifsccode} />
<MobileField label="Ref No" value={row.referenceno} />
<MobileField label="Reason" value={row.reason} full />
</MobileFieldGrid>
</MobileCard>
);
})}
{!loading && visibleRows.length === 0 && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: '#f1f5f9', color: '#94a3b8' }} />
<Typography sx={{ fontSize: 15, fontWeight: 700, color: '#0f172a' }}>No requests to show</Typography>
<Typography sx={{ fontSize: 13, color: '#94a3b8' }}>Requests will appear here once available.</Typography>
</Stack>
)}
</MobileCardList>
)}
<TableContainer sx={{ width: '100%', borderBottom: 1, borderColor: 'divider', display: isMobile ? 'none' : 'block' }}>
<Table sx={{ minWidth: 750 }} aria-labelledby="tableTitle" size={'medium'}>
<EnhancedTableHead
numSelected={selected.length}
@@ -1727,6 +1803,8 @@ const Requests = () => {
);
}
const outerTheme = useTheme();
const isMobile = useMediaQuery(outerTheme.breakpoints.down('md'));
const [tabvalue, setTabvalue] = useState(0);
const [rows, setRows] = useState([]);
const [clientapproved, setClientApproved] = useState([]);
@@ -1860,10 +1938,16 @@ const Requests = () => {
xs={12}
// sx={{ mb: -2.25 }}
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }}
>
<Typography variant="h3">Payment Requests</Typography>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
// setDialogopen(true)
}}
@@ -1882,8 +1966,6 @@ const Requests = () => {
> */}
<Stack
// direction={{xs:'column',md:'row'}}
// alignItems={{xs:'flex-end',md:'center'}}
alignItems="center"
justifyContent="space-between"
direction="row"
@@ -1891,7 +1973,9 @@ const Requests = () => {
// borderBottom: 1, borderColor: 'divider',
p: 2,
// m:2,
width: '100%'
width: '100%',
flexWrap: 'wrap',
gap: 1
}}
>
<Tabs value={tabvalue} onChange={handleChangetab} variant="scrollable" scrollButtons="auto">
@@ -1908,7 +1992,7 @@ const Requests = () => {
/>
</Tabs>
<FormControl sx={{ width: 250, display: { xs: 'none', md: 'flex' } }}>
<FormControl sx={{ width: { xs: '100%', md: 250 } }}>
<OutlinedInput
size="small"
id="header-search"

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
// material-ui
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography } from '@mui/material';
import { Box, Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
// third-party
// import { PatternFormat } from 'react-number-format';
@@ -47,6 +47,8 @@ const Createrider = () => {
const [tenantinfo, setTenantinfo] = useState({});
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// Geocode.setApiKey('AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8');
@@ -256,8 +258,14 @@ const Createrider = () => {
<>
{loading && <Loader />}
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', sm: 'center' }}
spacing={1}
>
<Typography variant="h3">Create Rider</Typography>
</Stack>
</Grid>
@@ -437,14 +445,20 @@ const Createrider = () => {
</MainCard>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="flex-end" alignItems="center" spacing={2}>
<Button variant="contained" onClick={() => createprofile()}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
Create
</Button>
</Stack>
</Grid>
</Grid>
</MainCard>
</Box>
</>
);
};

View File

@@ -13,7 +13,9 @@ import {
Stack,
TextField,
Typography,
Autocomplete
Autocomplete,
useMediaQuery,
useTheme
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -34,6 +36,8 @@ import dayjs from 'dayjs';
import CircularLoader from 'components/CircularLoader';
const EditRider = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const location = useLocation();
const [riderdata, setRiderdata] = useState(null);
const navigate = useNavigate();
@@ -347,15 +351,17 @@ const EditRider = () => {
<MainCard
title={
<Stack
direction="row"
direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between"
alignItems="center"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }}
sx={{ backgroundColor: 'secondary.lighter', width: '100%', height: '100%', p: 2 }}
>
<Typography variant="h3">Edit Rider </Typography>
<Button
startIcon={<ArrowBackIcon />}
variant="outlined"
fullWidth={isMobile}
onClick={() => {
setRiderdata(null);
navigate('/nearle/riders');
@@ -375,7 +381,7 @@ const EditRider = () => {
scrollBehavior: 'smooth'
}}
>
<Stack display={'flex'} spacing={5}>
<Stack display={'flex'} spacing={{ xs: 2.5, sm: 5 }}>
{/* || =========================================== || Contact Information || =========================================== || */}
<MainCard
title={
@@ -384,7 +390,7 @@ const EditRider = () => {
</Typography>
}
>
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, sm: 3 }}>
{/* ========================== || First Name || ========================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
@@ -582,7 +588,7 @@ const EditRider = () => {
id="combo-box-demo"
options={partnerlist}
getOptionLabel={(option) => `${option.locationname}`}
sx={{ width: 300, height: '30px', ml: 3, zIndex: '100' }}
sx={{ width: { xs: '100%', sm: 300 }, height: '30px', ml: { xs: 0, sm: 3 }, zIndex: '100' }}
onChange={(event, value) => {
if (value) {
console.log(value);
@@ -630,7 +636,7 @@ const EditRider = () => {
</Typography>
}
>
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, sm: 3 }}>
{/* ========================== || Shift Type || ========================== */}
<Grid item xs={12} sm={6}>
@@ -754,7 +760,7 @@ const EditRider = () => {
</Typography>
}
>
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, sm: 3 }}>
{' '}
{/* ========================== || Account No|| ========================== */}
<Grid item xs={12} sm={6}>
@@ -903,7 +909,7 @@ const EditRider = () => {
</Typography>
}
>
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, sm: 3 }}>
{/* ========================== || Vehicle Name || ========================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
@@ -1080,12 +1086,13 @@ const EditRider = () => {
borderTop: 'none'
}}
>
<Stack direction="row" justifyContent="flex-end" spacing={2}>
<Button startIcon={<ArrowBackIcon />} variant="outlined" onClick={() => navigate('/nearle/riders')}>
<Stack direction={{ xs: 'column-reverse', sm: 'row' }} justifyContent="flex-end" spacing={2}>
<Button startIcon={<ArrowBackIcon />} variant="outlined" fullWidth={isMobile} onClick={() => navigate('/nearle/riders')}>
Back to Riders
</Button>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
updateRider();
}}

View File

@@ -20,8 +20,10 @@ import {
Grid,
Box,
Skeleton,
Divider
Divider,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
var utc = require('dayjs/plugin/utc');
import dayjs from 'dayjs';
dayjs.extend(utc);
@@ -45,6 +47,7 @@ import {
MdTwoWheeler
} from 'react-icons/md';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import CircularLoader from 'components/CircularLoader';
import { fetchAllRiders, getallridersummary, getriderstatus } from 'pages/api/api';
@@ -137,6 +140,8 @@ const KPI_META = (summary) => [
const Riders = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const loadMoreRef = useRef();
const containerRef = useRef();
const [searchword, setSearchword] = useState('');
@@ -567,6 +572,307 @@ const Riders = () => {
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
{isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{allRidersLoading && (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading riders
</Typography>
</Stack>
)}
{rows?.length === 0 && !allRidersLoading && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdTwoWheeler size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{searchword
? 'Try a different keyword.'
: `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`}
</Typography>
</Stack>
)}
{rows?.length !== 0 &&
rows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
const expanded = logsRow === row.userid;
return (
<MobileCard
key={row.userid ?? index}
accent={statusMeta.color}
selected={expanded}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 11
}}
>
#{row?.userid}
</Box>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(statusMeta.color),
border: `1px solid ${edge(statusMeta.color)}`,
color: statusMeta.color
}}
>
<AccentAvatar color={statusMeta.color} size={18}>
<StatusIcon size={11} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
{statusMeta.label}
</Typography>
</Stack>
</Stack>
{roleid == 1 && (
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
navigate('/nearle/riders/edit', { state: { riderdata: row } });
}}
>
<MdEdit size={14} />
</IconButton>
{tabvalue != 0 && (
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: expanded ? '#0ea5e9' : soft('#0ea5e9'),
color: expanded ? '#fff' : '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => {
if (row.userid == logsRow) {
setLogsRow(null);
} else {
setLogsRow(row.userid);
getRiderLogs(row.userid);
}
}}
>
{expanded ? <MdKeyboardArrowUp size={14} /> : <MdKeyboardArrowDown size={14} />}
</IconButton>
)}
</Stack>
)}
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: soft(BRAND),
color: BRAND,
fontWeight: 800,
fontSize: 16,
border: `1px solid ${edge(BRAND)}`,
flexShrink: 0
}}
>
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.username || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.contactno || '—'}
</Typography>
</Box>
</Stack>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textSecondary }} noWrap>
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
</Typography>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.city || ''}
</Typography>
</MobileField>
<MobileField label="Vehicle">
<Stack direction="row" alignItems="center" spacing={0.75}>
<AccentAvatar color="#0ea5e9" size={22}>
<MdTwoWheeler size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.vehicleno || '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Shift" value={`#${row.shiftid ?? '—'}`} />
<MobileField label="Time">
<Stack spacing={0.5}>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 10.5,
fontWeight: 800
}}
>
{row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'}
</Box>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#ef4444'),
border: `1px solid ${edge('#ef4444')}`,
color: '#ef4444',
fontSize: 10.5,
fontWeight: 800
}}
>
{row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
</Box>
</Stack>
</MobileField>
<MobileField label="Fare" value={row.basefare ?? '—'} />
<MobileField label="Fuel" value={row.fuelcharge ?? '—'} />
</MobileFieldGrid>
{expanded && tabvalue !== 0 && (
<Box
sx={{
mt: 1.5,
p: 1.25,
borderRadius: 2,
bgcolor: tint(BRAND),
border: `1px solid ${edge(BRAND)}`
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.25 }}>
<AccentAvatar color={BRAND} size={26}>
<MdGpsFixed size={14} />
</AccentAvatar>
<Typography
sx={{
fontWeight: 800,
color: DT.textSecondary,
letterSpacing: 0.6,
textTransform: 'uppercase',
fontSize: 11
}}
>
Live telemetry {row.username || `Rider #${row.userid}`}
</Typography>
</Stack>
<Grid container spacing={1.25}>
<LogChip
color="#ef4444"
icon={MdLocationOn}
label="Location"
value={riderLogsdata?.latitude ? `${riderLogsdata.latitude}, ${riderLogsdata.longitude}` : '—'}
/>
<LogChip
color="#10b981"
icon={MdBatteryStd}
label="Battery"
value={riderLogsdata?.battery ? `${riderLogsdata.battery}%` : 'N/A'}
/>
<LogChip
color={riderLogsdata?.is_charging ? '#10b981' : '#94a3b8'}
icon={MdPowerSettingsNew}
label="Charging"
value={riderLogsdata?.is_charging ? 'Charging' : 'Not Charging'}
/>
<LogChip
color="#0ea5e9"
icon={MdSpeed}
label="Speed"
value={riderLogsdata?.speed !== undefined ? `${riderLogsdata.speed} km/h` : '—'}
/>
<LogChip
color="#8b5cf6"
icon={MdGpsFixed}
label="Accuracy"
value={riderLogsdata?.accuracy !== undefined ? `${riderLogsdata.accuracy} m` : '—'}
/>
<LogChip color="#f59e0b" icon={MdAccessTime} label="Log time" value={riderLogsdata?.logdate || '—'} />
<LogChip color={BRAND} icon={MdInventory2} label="Active order" value={riderLogsdata?.orderid || 'N/A'} />
<LogChip
color={riderLogsdata?.status === 'idle' ? '#f59e0b' : '#10b981'}
icon={MdCheckCircle}
label="Status"
value={riderLogsdata?.status || 'unknown'}
/>
</Grid>
</Box>
)}
</MobileCard>
);
})}
{rows?.length !== 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more riders
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: { xs: 1100, md: 1300 } }}>
<TableHead>
<TableRow
@@ -962,6 +1268,7 @@ const Riders = () => {
)}
</TableBody>
</Table>
)}
</TableContainer>
</Paper>
</>

View File

@@ -1,4 +1,5 @@
import { Grid, TextField, Typography } from '@mui/material';
import { Grid, TextField, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useQuery } from '@tanstack/react-query';
import CircularLoader from 'components/CircularLoader';
import Loader from 'components/Loader';
@@ -6,6 +7,8 @@ import MainCard from 'components/MainCard';
import { getusers } from 'pages/api/api';
const ViewProfile = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const {
data: userData,
isLoading
@@ -23,13 +26,14 @@ const ViewProfile = () => {
)}
<MainCard
contentSX={{ p: isMobile ? 2 : 3 }}
title={
<Typography variant="h3" sx={{ m: 3 }}>
<Typography variant="h3" sx={{ m: { xs: 1.5, md: 3 } }}>
User Profile
</Typography>
}
>
<Grid container spacing={4}>
<Grid container spacing={{ xs: 2.5, md: 4 }}>
{/* ==============================|| userid ||============================== */}
<Grid item xs={12} sm={6} md={4}>
<TextField