updated the ui for the mobile view
This commit is contained in:
140
src/components/nearle_components/MobileCard.js
Normal file
140
src/components/nearle_components/MobileCard.js
Normal 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
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 | ============================================= */}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user