updates on the orders and assign page

This commit is contained in:
2026-06-25 20:05:09 +05:30
parent 30c0e184ce
commit 1ba28231a0
15 changed files with 3495 additions and 2071 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

View File

@@ -0,0 +1,34 @@
// LoaderWithImage.jsx
import React from 'react';
import { Box, CircularProgress } from '@mui/material';
import nelogo from '../../assets/images/logo-sm.png';
export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) {
return (
<Box position="relative" display="inline-flex" justifyContent="center" alignItems="center">
<CircularProgress size={size} />
<Box
position="absolute"
display="flex"
justifyContent="center"
alignItems="center"
sx={{
width: imgSize,
height: imgSize
}}
>
<img
src={nelogo}
alt={alt}
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</Box>
</Box>
);
}

View File

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

View File

@@ -6,6 +6,7 @@ import { TbListDetails } from 'react-icons/tb';
import { LiaFileInvoiceSolid } from 'react-icons/lia'; import { LiaFileInvoiceSolid } from 'react-icons/lia';
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined'; import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
// assets // assets
import { import {
@@ -64,6 +65,13 @@ const nearle = {
url: '/nearle/orders', url: '/nearle/orders',
icon: AiOutlineDashboard icon: AiOutlineDashboard
}, },
{
id: 'deliveries',
title: <FormattedMessage id="Deliveries" />,
type: 'item',
url: '/nearle/deliveries',
icon: MopedOutlinedIcon
},
{ {
id: 'locations', id: 'locations',
title: <FormattedMessage id="Locations" />, title: <FormattedMessage id="Locations" />,

View File

@@ -4,6 +4,7 @@ import { AiOutlineBarChart } from 'react-icons/ai';
import { AiOutlineDashboard } from 'react-icons/ai'; import { AiOutlineDashboard } from 'react-icons/ai';
import { TbListDetails } from 'react-icons/tb'; import { TbListDetails } from 'react-icons/tb';
import { LiaFileInvoiceSolid } from 'react-icons/lia'; import { LiaFileInvoiceSolid } from 'react-icons/lia';
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
// assets // assets
import { import {
@@ -60,6 +61,13 @@ const other = {
url: 'nearle/orders', url: 'nearle/orders',
icon: AiOutlineDashboard icon: AiOutlineDashboard
}, },
{
id: 'deliveries',
title: <FormattedMessage id="Deliveries" />,
type: 'item',
url: 'nearle/deliveries',
icon: MopedOutlinedIcon
},
{ {
id: 'customers', id: 'customers',
title: <FormattedMessage id="Customers" />, title: <FormattedMessage id="Customers" />,

View File

@@ -24,6 +24,34 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
}; };
}; };
// ==============================|| fetchPercentageData (orders) ||============================== //
export const fetchPercentageData = async ({ queryKey }) => {
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
const details = response.data.details;
return {
created: details.created.toString(),
uncoveredOrders: details.pending.toString(),
coveredOrders: details.delivered.toString(),
cancelled: details.cancelled.toString(),
percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(),
percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(),
percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(),
percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString()
};
};
// ==============================|| fetchorderscount (orders) ||============================== //
export const fetchorderscount = async ({ queryKey }) => {
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`;
const response = await axios.get(url);
return response.data.details;
};
// ==============================|| fetchOrderSummary (orders)||============================== // // ==============================|| fetchOrderSummary (orders)||============================== //
export const fetchOrderSummary = async () => { export const fetchOrderSummary = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getordersummary`); const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getordersummary`);
@@ -385,12 +413,12 @@ export const createAutomationDeliveries = async (variables) => {
const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : []; const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : [];
const url = const url =
variables.selectedMode.value == 1 variables.selectedMode?.value == 1
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}` ? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`; : `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
const body = const body =
variables.selectedMode.value == 1 variables.selectedMode?.value == 1
? { deliveries: variables.deliveries, absent_riders: absentRiders } ? { deliveries: variables.deliveries, absent_riders: absentRiders }
: { ...(variables.data || {}), absent_riders: absentRiders }; : { ...(variables.data || {}), absent_riders: absentRiders };
@@ -431,3 +459,37 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
}; };
}; };
// Fetch payment types.
export const fetchPaymentType = async () => {
const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
return data.details.map((val) => ({
...val,
label: val.typename
}));
};
// Fetch all riders.
export const getallriders = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`);
return res.data.details;
} catch (err) {
console.log('getallriders', err.message);
}
};
// Cancel multiple orders.
export const cancelMultipleOrder = async (orderlist) => {
const data = orderlist?.map((e) => ({
orderheaderid: e.orderheaderid,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
}));
const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updatemultipleorders`, data);
return response.data;
};

File diff suppressed because it is too large Load Diff

View File

@@ -2287,6 +2287,10 @@
color: var(--ad-accent, var(--accent)); color: var(--ad-accent, var(--accent));
} }
.dispatch-container .adcard-m-time {
color: #10b981;
}
.dispatch-container .adcard-ic { .dispatch-container .adcard-ic {
flex-shrink: 0; flex-shrink: 0;
display: inline-flex; display: inline-flex;

View File

@@ -50,6 +50,7 @@ import {
MdInsights, MdInsights,
MdRefresh MdRefresh
} from 'react-icons/md'; } from 'react-icons/md';
import { CircularProgress } from '@mui/material';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../api/api'; import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../api/api';
import { import {
STATUS_STYLES, STATUS_STYLES,
@@ -2609,15 +2610,20 @@ const Dispatch = ({
<span /> <span />
)} )}
<span className="adcard-metrics"> <span className="adcard-metrics">
<span className="adcard-m adcard-m-km" title="Trip distance">
<span className="adcard-ic"><MdStraighten /></span>
{parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km
</span>
{estMeters !== null && ( {estMeters !== null && (
<span className="adcard-m adcard-m-eta" title="Estimated distance to drop location"> <>
<span className="adcard-m adcard-m-eta" title="Distance to drop">
<span className="adcard-ic"><MdMyLocation /></span> <span className="adcard-ic"><MdMyLocation /></span>
{formatMeters(estMeters)} {formatMeters(estMeters)}
</span> </span>
<span className="adcard-m adcard-m-time" title="Estimated time to drop">
<span className="adcard-ic"><MdAccessTime /></span>
{(() => {
const etaMin = estMeters / 1000 / 20 * 60;
return etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`;
})()}
</span>
</>
)} )}
</span> </span>
</div> </div>
@@ -3465,7 +3471,9 @@ const Dispatch = ({
onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }} onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
><span className="sbt-icon"><MdMap /></span> By Zone</button> ><span className="sbt-icon"><MdMap /></span> By Zone</button>
<button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button> <button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
{!embedded && (
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Active Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span>Active</button> <button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Active Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span>Active</button>
)}
<button <button
type="button" type="button"
className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`} className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`}
@@ -4641,6 +4649,14 @@ const Dispatch = ({
// same `visibleRiders` set the map uses keeps the sidebar // same `visibleRiders` set the map uses keeps the sidebar
// and the map in lock-step (same count, same deliveries). // and the map in lock-step (same count, same deliveries).
(() => { (() => {
if (shouldFetchLive && liveIsFetching && visibleRiders.length === 0) {
return (
<div className="empty-slot" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: '200px' }}>
<CircularProgress size={30} style={{ color: '#7b1fa2', marginBottom: '16px' }} />
<div className="empty-slot-title">Loading active deliveries...</div>
</div>
);
}
const activeDeliveries = visibleRiders const activeDeliveries = visibleRiders
.map((r) => getActiveOrder(r.orders)) .map((r) => getActiveOrder(r.orders))
.filter(Boolean) .filter(Boolean)

View File

@@ -552,62 +552,39 @@ const Preview = () => {
<CircularLoader color="inherit" /> <CircularLoader color="inherit" />
</Backdrop> </Backdrop>
<Box sx={{ py: 1.25, px: 2, borderBottom: '1px solid #eef2f6' }}> <Box
sx={{
py: 1.5,
px: 2.5,
borderBottom: '1px solid #e2e8f0',
background: 'linear-gradient(135deg, rgba(102,37,130,0.06) 0%, rgba(146,85,171,0.06) 100%)'
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between"> <Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1.5}>
<Tooltip title="Back to orders" placement="top"> <Tooltip title="Back to orders" placement="top">
<IconButton <IconButton
onClick={() => navigate('/nearle/orders')} onClick={() => navigate('/nearle/orders')}
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }} sx={{
bgcolor: '#ffffff',
border: '1px solid #e2e8f0',
color: '#662582',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
'&:hover': { bgcolor: '#f8fafc', borderColor: '#662582' }
}}
> >
<HiOutlineArrowLeft size={20} /> <HiOutlineArrowLeft size={18} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Typography variant="h3" fontWeight={600}> <Typography variant="h3" fontWeight={700} sx={{ color: '#0f172a' }}>
Assign Orders Assign Orders
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Autocomplete
options={tuningTypes || []}
getOptionLabel={(option) => option.type}
sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
renderInput={(params) => <TextField {...params} label="Hyper Tuning" />}
onChange={(e, val, reason) => {
if (reason === 'clear') handleCreateDelivery(null);
else handleCreateDelivery(val.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<IoReload />}
onClick={() => {
setIsLoading(true);
handleCreateDelivery('reshuffle');
}}
>
Re-Assign
</Button>
<CSVExport
data={csvExportData}
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
label=" CSV"
style={{ m: 1 }}
/>
</Stack> </Stack>
</Stack>
</Box>
<Box sx={{ px: 2, borderBottom: '1px solid #eef2f6' }}>
<Tabs value={tabValue} onChange={(e, v) => setTabValue(v)} sx={{ minHeight: 40 }}>
<Tab label="Dispatch" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
<Tab label="Reconcile" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
</Tabs>
</Box> </Box>
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{tabValue === 0 && dispatchPreviewData && ( {dispatchPreviewData && (
<Dispatch <Dispatch
// The key forces a full re-mount when the cache reference changes // The key forces a full re-mount when the cache reference changes
// (after Change Rider / Reconcile / Re-Assign) so Dispatch's // (after Change Rider / Reconcile / Re-Assign) so Dispatch's
@@ -620,144 +597,57 @@ const Preview = () => {
onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)} onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)}
/> />
)} )}
{tabValue === 1 && ( </Box>
<Box sx={{ flex: 1, overflow: 'auto', p: 2, bgcolor: '#f8fafc' }}>
{reconcileRiders.length === 0 ? (
<Typography sx={{ color: '#94a3b8', textAlign: 'center', mt: 4 }}>
No rider data available to reconcile.
</Typography>
) : (
<Stack spacing={1.75}>
<Box <Box
sx={{ sx={{
bgcolor: hasReconciled ? '#ecfdf5' : '#fffbeb', px: 3,
border: `1px solid ${hasReconciled ? '#a7f3d0' : '#fde68a'}`, py: 1.75,
color: hasReconciled ? '#065f46' : '#92400e', borderTop: '1px solid #e2e8f0',
borderRadius: '10px', bgcolor: '#ffffff',
px: 1.5, boxShadow: '0 -4px 20px rgba(0, 0, 0, 0.03)'
py: 1,
fontSize: 13
}} }}
> >
{hasReconciled
? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.'
: 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'}
</Box>
{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 }}>
<Stack direction="row" alignItems="center" gap={1.25}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
bgcolor: '#eef2ff',
color: '#4f46e5',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<MdTwoWheeler size={18} />
</Box>
<Box>
<Typography sx={{ fontWeight: 700, fontSize: 14, color: '#1e293b' }}>
{r.rider_name}
</Typography>
<Typography sx={{ fontSize: 11.5, color: '#64748b' }}>
ID: {r.rider_id}
</Typography>
</Box>
</Stack>
<Stack direction="row" gap={1}>
<Chip size="small" label={`${r.orders.length} stops`} sx={{ fontWeight: 600 }} />
<Chip size="small" label={`${totalKms.toFixed(1)} km`} variant="outlined" />
</Stack>
</Stack>
<Stack direction="row" gap={1.25} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
{r.orders.map((o, idx) => {
const stepNum = o.step ?? idx + 1;
const color = stepColor(Number(stepNum) - 1);
return (
<Tooltip
key={`${o.orderid}-${idx}`}
title={
<Box>
<div>Order #{o.orderid}</div>
<div>{o.deliveryaddress || o.deliverysuburb || ''}</div>
<div style={{ marginTop: 4, opacity: 0.8 }}>Click to change rider</div>
</Box>
}
>
<Box
onClick={() => openChangeRider(r, o)}
sx={{
width: 36,
height: 36,
borderRadius: '50%',
bgcolor: color,
color: '#fff',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 800,
fontSize: 14,
cursor: 'pointer',
boxShadow:
'0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)',
transition: 'transform 0.15s',
'&:hover': { transform: 'scale(1.08)' }
}}
>
{stepNum}
</Box>
</Tooltip>
);
})}
</Stack>
</Card>
);
})}
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 1.5, pb: 2 }}>
<Button
variant="contained"
color="primary"
size="large"
startIcon={<MdSwapHoriz />}
onClick={handleReconcile}
disabled={reconcileLoading || dirtyRiderIds.size === 0}
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
>
{reconcileLoading
? 'Reconciling...'
: dirtyRiderIds.size === 0
? 'Reconcile'
: `Reconcile (${dirtyRiderIds.size})`}
</Button>
</Box>
</Stack>
)}
</Box>
)}
</Box>
<Box sx={{ px: 2, py: 1.25, borderTop: '1px solid #eef2f6' }}>
<Stack direction="row" gap={2} alignItems="center" justifyContent="end"> <Stack direction="row" gap={2} alignItems="center" justifyContent="end">
<Button <Button
variant="contained" variant="outlined"
color="secondary"
startIcon={<ArrowBackIcon />}
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
sx={{
borderRadius: 999,
px: 3,
py: 1,
borderColor: '#e2e8f0',
color: '#64748b',
textTransform: 'none',
fontWeight: 700,
'&:hover': {
borderColor: '#cbd5e1',
bgcolor: '#f8fafc'
}
}}
> >
Back Back
</Button> </Button>
<Button variant="contained" onClick={handleFinalCreateDelivery}> <Button
variant="contained"
onClick={handleFinalCreateDelivery}
sx={{
borderRadius: 999,
px: 4,
py: 1,
bgcolor: '#662582',
color: '#ffffff',
textTransform: 'none',
fontWeight: 800,
boxShadow: '0 4px 14px rgba(102, 37, 130, 0.25)',
'&:hover': {
bgcolor: '#9255ab',
transform: 'translateY(-1px)',
boxShadow: '0 6px 20px rgba(102, 37, 130, 0.35)'
},
transition: 'all 0.2s'
}}
>
Assign Orders Assign Orders
</Button> </Button>
</Stack> </Stack>

View File

@@ -0,0 +1,768 @@
import {
Autocomplete,
Button,
Chip,
Divider,
Grid,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
Backdrop,
IconButton
} 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';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../api/api';
import { OpenToast } from 'components/nearle_components/OpenToast';
import { useMutation, useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import CircularLoader from 'components/nearle_components/CircularLoader';
import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { DashboardFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
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);
const [rider, setRider] = useState(null);
const [payment, setPayment] = useState(null);
const [finaldeliveryList, setFinalDeliveryList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const deliverylist = location.state?.deliverylist;
const zoneData = location.state?.zoneData;
const metaData = location.state?.metaData;
const riderToken = location.state?.riderToken;
const appId = location.state?.appId;
const aiMode = location.state?.aiMode;
const reassignOrders = location.state?.reassignOrders;
useEffect(() => {
console.log('aiMode', aiMode);
console.log('riderToken', riderToken);
console.log('zoneData', zoneData);
console.log('metaData', metaData);
console.log('reassignOrders', reassignOrders);
}, []);
useEffect(() => {
if (!deliverylist?.length) return;
const updateDeliveryAmtList = deliverylist.map((list) => {
const cumulativeKms = Number(list.cumulativekms || 0);
const minKm = Number(list.minkm || 0);
const basePrice = Number(list.baseprice || 0);
const pricePerKm = Number(list.priceperkm || 0);
if (cumulativeKms <= minKm) {
return {
...list,
deliveryamt: basePrice
};
}
return {
...list,
deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice
};
});
setFinalDeliveryList(updateDeliveryAmtList);
console.log('finaldeliveryList', updateDeliveryAmtList);
}, [deliverylist]);
// ==============================|| fetchPaymentType ||============================== //
const {
data: paymentModes = [],
isLoading: paymentModesLoading
} = useQuery({
queryKey: ['paymentmodes'],
queryFn: fetchPaymentType
});
// ==============================|| fetchRidersList ||============================== //
const {
data: ridersList = [],
isLoading: ridersListLoading
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList,
enabled: appId !== 0 // Ensures query runs only when appId is valid
});
const getRiderName = async (userid) => {
await ridersList.map((rider) => {
if (rider.userid == userid) {
return rider.firstname;
}
});
};
// ======================================================= || notifyRiderMutation || =======================================================
const notifyRiderMutation = useMutation({
mutationFn: notifyRider, // Using the separate function
onSuccess: () => {
OpenToast('Notification sent Successfully', 'success', 2000);
},
onError: (error) => {
OpenToast(error.message, 'error', 2000);
}
});
const createNormalDeliveryMutation = useMutation({
mutationFn: finalCreatedeliveries, // for optimised delivery create
onSuccess: (data, variables) => {
console.log('data', data);
console.log('varialbles', variables);
notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success
if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000);
}
setTimeout(() => {
setIsLoading(false);
navigate('/nearle/orders');
}, 2000);
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
}
});
const handleManualCreateDelivery = async () => {
setIsLoading(true);
createNormalDeliveryMutation.mutate({
deliveries: finaldeliveryList
});
};
return (
<MainCard
content={false}
title={
<Stack direction="row" alignItems="center" spacing={1} sx={{ ml: 1 }}>
<Tooltip title="Back to orders" placement="top">
<IconButton
onClick={() => navigate('/nearle/orders')}
sx={{
backgroundColor: 'action.hover',
color: 'text.primary',
'&:hover': {
backgroundColor: 'action.selected'
}
}}
>
<HiOutlineArrowLeft size={22} />
</IconButton>{' '}
</Tooltip>
<Typography sx={{ m: 2 }} variant="h3">
Assign Orders
</Typography>
</Stack>
}
secondary={
<Button sx={{ m: 2 }} color="primary" variant="contained" startIcon={<ArrowBackIcon />}>
Re-Assign
</Button>
}
>
{(paymentModesLoading || ridersListLoading || isLoading) && (
<>
<Loader />
<CircularLoader />
</>
)}
{
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
open={paymentModesLoading || ridersListLoading || isLoading} // when loader = true, backdrop covers the page
>
<CircularLoader color="inherit" />
</Backdrop>
}
{aiMode == 1 && (
<Stack sx={{ m: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={3}>
<HoverSocialCard
secondary={metaData?.total_orders}
primary={'Orders'}
percentage={<DashboardFilled />}
color={theme.palette.success.main}
sx={{ cursor: 'pointer' }}
/>
</Grid>
<Grid item xs={12} sm={3}>
<HoverSocialCard
secondary={metaData?.total_riders}
primary={'Riders'}
percentage={<MdDirectionsBike />}
color={theme.palette.warning.main}
/>
</Grid>
<Grid item xs={12} sm={3}>
<HoverSocialCard
secondary={zoneData?.length}
primary={'Zones'}
percentage={<FaMapLocationDot />}
color={theme.palette.info.main}
/>
</Grid>
<Grid item xs={12} sm={3}>
<HoverSocialCard
secondary={zoneData?.length}
primary={'Kilometer'}
percentage={<FaMapLocationDot />}
color={theme.palette.error.main}
/>
</Grid>
</Grid>
</Stack>
)}
{isMobile ? (
<MobileOrdersList list={finaldeliveryList} aiMode={aiMode} />
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 250px)',
overflow: 'auto',
'&::-webkit-scrollbar': {
width: '12px', // scroll bar width
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main, // thumb color
borderRadius: '8px',
cursor: 'pointer'
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme.palette.primary.dark, // hover color
cursor: 'pointer'
},
'&::-webkit-scrollbar-track': {
backgroundColor: theme.palette.primary.lighter,
cursor: 'pointer'
}
}}
>
<Table stickyHeader>
<TableHead>
<TableRow sx={{ backgroundColor: 'red' }}>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>#</TableCell>
{aiMode == 1 && (
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Zone </TableCell>
)}
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Tenant </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>order Location</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Pickup </TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Delivery</TableCell>
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Notes</TableCell>
{aiMode == 1 && (
<TableCell sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>Rider</TableCell>
)}
<TableCell align="center" sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>
Type
</TableCell>
<TableCell align="center" sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>
Profit
</TableCell>
<TableCell align="center" sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>
Charges
</TableCell>
<TableCell align="center" sx={{ position: 'sticky !important', backgroundColor: theme.palette.secondary.light }}>
KMS
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{finaldeliveryList?.length == 0 && (
<TableRow>
<TableCell colSpan={10}>
<Empty />
</TableCell>
</TableRow>
)}
{finaldeliveryList && aiMode == 1 // ai mode , ai automation
? finaldeliveryList?.map((val, index) => {
return (
<Fragment key={index}>
<TableRow sx={{}}>
<TableCell>
<Typography> {index + 1}</Typography>
</TableCell>
{aiMode == 1 && (
<TableCell>
<Chip color="primary" label={val.zone_name} />
</TableCell>
)}
<TableCell>
<Tooltip title={val.tenantaddress}>
<Typography variant="body1" noWrap>
{val.tenantname}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{val.tenantsuburb}
<br />
</Typography>
<Typography noWrap variant="body2">
{val.applocation}
</Typography>
</Tooltip>
</TableCell>
<TableCell align="left">
<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">
<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>
</Tooltip>
-
<Tooltip title="Delivery date">
<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>
</Tooltip>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<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>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<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>
</Stack>
</TableCell>
<TableCell align="left">{val.ordernotes}</TableCell>
{aiMode == 1 && (
<TableCell align="left">
<Typography sx={{ whiteSpace: 'nowrap' }}>{val.username}</Typography>
<Typography>ID : {val.userid}</Typography>
</TableCell>
)}
<TableCell align="center">
<Chip
size="small"
label={val.ordertype}
color={val.ordertype == 'Economy' ? 'success' : val.ordertype == 'Risky' ? 'error' : 'primary'}
/>
</TableCell>
<TableCell align="center">
<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>
</TableCell>
<TableCell align="center">
<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>
</TableCell>
</TableRow>
</Fragment>
);
})
: // normal optimisation
finaldeliveryList?.map((val, index) => {
return (
<Fragment key={index}>
<TableRow sx={{}}>
<TableCell>
<Typography> {index + 1}</Typography>
</TableCell>
<TableCell>
<Tooltip title={val.tenantaddress}>
<Typography variant="body1" noWrap>
{val.tenantname}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{val.tenantsuburb}
<br />
</Typography>
<Typography noWrap variant="body2">
{val.applocation}
</Typography>
</Tooltip>
</TableCell>
<TableCell align="left">
<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">
<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>
</Tooltip>
-
<Tooltip title="Delivery date">
<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>
</Tooltip>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<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>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<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>
</Stack>
</TableCell>
<TableCell align="left">{val.ordernotes}</TableCell>
<TableCell align="center">
<Chip
size="small"
label={val.ordertype}
color={val.ordertype == 'Economy' ? 'success' : val.ordertype == 'Risky' ? 'error' : 'primary'}
/>
</TableCell>
<TableCell align="center">
<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>
</TableCell>
<TableCell align="center">
<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>
</TableCell>
</TableRow>
</Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
<Divider />
{aiMode == 0 && (
<Grid container spacing={2} sx={{ p: 2 }}>
<Grid item xs={12} sm={6}>
<Autocomplete
id="free-solo-demo"
options={paymentModes}
renderInput={(params) => <TextField {...params} label="Choose Payment" />}
onChange={(event, newValue, reason) => {
if (reason === 'clear') {
setPayment(null);
return;
}
if (newValue) {
console.log('Selected:', newValue);
setPayment(newValue);
const newList = finaldeliveryList?.map((list) => ({
...list,
paymenttype: newValue.apptypeid // merge selected rider into each list item
}));
setFinalDeliveryList(newList);
}
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Autocomplete
id="free-solo-demo"
options={ridersList}
renderInput={(params) => <TextField {...params} label="Choose Rider" />}
onChange={(event, newValue, reason) => {
if (reason === 'clear') {
setRider(null);
return;
}
if (newValue) {
setRider(newValue);
console.log('Selected:', newValue);
const newList = finaldeliveryList?.map((list) => ({
...list,
userid: newValue.userid,
userfcmtoken: newValue.userfcmtoken
}));
setFinalDeliveryList(newList);
}
}}
/>
</Grid>
</Grid>
)}
<Divider />
<Stack
display={'flex'}
flexDirection={{ xs: 'column', sm: 'row' }}
gap={2}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent={'end'}
sx={{ p: 2 }}
>
<Button
fullWidth={isMobile}
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
onClick={() => {
navigate('/nearle/orders');
}}
>
Back
</Button>
<Button
fullWidth={isMobile}
sx={{ my: { xs: 0, sm: 2 } }}
variant="contained"
disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleManualCreateDelivery}
>
Finalise
</Button>
</Stack>
</MainCard>
);
};
export default OrdersPreview;

View File

@@ -0,0 +1,49 @@
import { TableRow, TableCell, Skeleton, Stack } from '@mui/material';
export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => {
return (
<>
{Array.from(new Array(rowsPerPage)).map((_, index) => (
<TableRow key={index}>
{/* Checkbox */}
<TableCell>
<Skeleton variant="circular" width={24} height={24} />
</TableCell>
{/* Serial Number */}
<TableCell>
<Skeleton variant="text" width={30} />
</TableCell>
{/* Delivery Info */}
{Array.from({ length: col }).map((_, index) => (
<TableCell key={index}>
<Stack spacing={0.5}>
<Skeleton variant="text" width={100} />
<Skeleton variant="text" width={80} />
</Stack>
</TableCell>
))}
{/* Notes */}
<TableCell>
<Skeleton variant="text" width={150} />
</TableCell>
{/* Order Status */}
<TableCell>
<Skeleton variant="rounded" width={60} height={24} />
</TableCell>
{/* Actions */}
<TableCell align="center">
<Stack direction="row" spacing={1} justifyContent="flex-end">
<Skeleton variant="circular" width={28} height={28} />
<Skeleton variant="circular" width={28} height={28} />
</Stack>
</TableCell>
</TableRow>
))}
</>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc'); var utc = require('dayjs/plugin/utc');
dayjs.extend(utc); dayjs.extend(utc);
import axios from 'axios'; import axios from 'axios';
import { useTheme } from '@mui/material/styles';
import { import {
Avatar, Avatar,
@@ -25,11 +24,15 @@ import {
DialogContent, DialogContent,
Tooltip, Tooltip,
Skeleton, Skeleton,
Autocomplete,
TextField,
CircularProgress, CircularProgress,
InputBase, InputBase,
InputAdornment Backdrop,
SpeedDial,
SpeedDialIcon,
SpeedDialAction,
Badge,
TableContainer,
Checkbox
} from '@mui/material'; } from '@mui/material';
import { import {
MdAccessTime, MdAccessTime,
@@ -42,22 +45,28 @@ import {
MdHourglassEmpty, MdHourglassEmpty,
MdInventory2, MdInventory2,
MdLocalShipping, MdLocalShipping,
MdLocationOn,
MdMyLocation,
MdNote,
MdPlace,
MdSearch, MdSearch,
MdStraighten, MdStraighten,
MdCalendarMonth, MdCalendarMonth,
MdReceiptLong, MdReceiptLong,
MdClear MdClear,
MdNotes
} from 'react-icons/md'; } from 'react-icons/md';
import TableContainer from '@mui/material/TableContainer'; import { DeleteOutlined } from '@ant-design/icons';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus'; import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus';
import DateFilterDialog from 'components/nearle_components/DateFilterDialog'; import DateFilterDialog from 'components/nearle_components/DateFilterDialog';
import CircularLoader from 'components/nearle_components/CircularLoader'; import CircularLoader from 'components/nearle_components/CircularLoader';
import { useInfiniteQuery } from '@tanstack/react-query'; import AiImage from '../../../assets/images/aiImage.png';
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { useNavigate, useLocation } from 'react-router-dom';
import {
fetchPercentageData,
createAutomationDeliveries,
cancelMultipleOrder,
getallriders,
fetchorderscount
} from '../api/api';
// ============================================================================ // ============================================================================
// Design tokens — shared with the rest of the redesigned operator pages. // Design tokens — shared with the rest of the redesigned operator pages.
@@ -82,48 +91,14 @@ const soft = (c) => dtA(c, '18');
const ring = (c) => dtA(c, '26'); const ring = (c) => dtA(c, '26');
const edge = (c) => dtA(c, '55'); const edge = (c) => dtA(c, '55');
const dtTint = tint;
const dtSoft = soft;
const dtRing = ring;
const dtEdge = edge;
const BRAND = '#662582'; const BRAND = '#662582';
const BRAND_LIGHT = '#9255AB'; const BRAND_LIGHT = '#9255AB';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
const pillFieldSx = (color) => ({
'& .MuiOutlinedInput-root': {
borderRadius: DT.radiusPill + 'px',
bgcolor: tint(color),
fontWeight: 600,
'& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: color },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
}
});
// Semantic per-row status palette — colors per brand standard: // Semantic per-row status palette — colors per brand standard:
// green=delivered, amber=pending, blue=created/processing, red=cancelled, // green=delivered, amber=pending, blue=created/processing, red=cancelled,
@@ -145,13 +120,9 @@ const ROW_STATUS_META = {
// Top-level pill tabs. // Top-level pill tabs.
const ORDERS_STATUS_TABS = [ const ORDERS_STATUS_TABS = [
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }, { idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
{ idx: 1, status: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' },
{ idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, countKey: 'delivered' },
{ idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' }
]; ];
// Filled status badge — high-contrast pill (white text on solid color).
const StatusBadge = ({ status }) => { const StatusBadge = ({ status }) => {
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || { const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
label: status || '—', label: status || '—',
@@ -219,35 +190,20 @@ const MetricCell = ({ value, color, icon, isMoney = false }) => {
}; };
const Orders = () => { const Orders = () => {
const navigate = useNavigate();
const location = useLocation();
const tid = localStorage.getItem('tenantid'); const tid = localStorage.getItem('tenantid');
const tenId = localStorage.getItem('tenantid');
const loadMoreRef = useRef(); const loadMoreRef = useRef();
const containerRef = useRef(); const containerRef = useRef();
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [pageCount, setPageCount] = useState(0);
const [percentage1, setPercentage1] = useState('0');
const [percentage2, setPercentage2] = useState('0');
const [percentage3, setPercentage3] = useState('0');
const [percentage4, setPercentage4] = useState('0');
const [tenantLocations, setTenantlocations] = useState([]);
const [coveredorders, setCoveredorders] = useState('');
const [uncoveredorders, setUncoveredorders] = useState('');
const [cancelled, setCancelled] = useState('');
const [created, setCreated] = useState('');
const [loading, setLoading] = useState(false);
const theme = useTheme();
const [tabvalue, setTabvalue] = useState(0); const [tabvalue, setTabvalue] = useState(0);
const [tabstatus, setTabstatus] = useState('Created'); const [tabstatus, setTabstatus] = useState('Created');
const [currentStatus, setCurrentStatus] = useState('created'); const [currentStatus, setCurrentStatus] = useState('created');
const [createdLenght, setCreatedLenght] = useState();
const [pendingLenght, setPendingLenght] = useState();
const [deliveredlenght, setDeliveredlenght] = useState();
const [cancelledLenght, setCancelledLenght] = useState();
const [cancelOpen, setCancelOpen] = useState(false); const [cancelOpen, setCancelOpen] = useState(false);
const [orderheaderid, setOrderheaderid] = useState(''); const [orderheaderid, setOrderheaderid] = useState('');
const [locationId, setLocationId] = useState(0); const locationId = 0;
const [locoName, setLocoName] = useState('All Locations'); const locoName = 'All Locations';
const [dateOpen, setDateOpen] = useState(false); const [dateOpen, setDateOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today'); const [datestatus, setDatestatus] = useState('Today');
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
@@ -255,6 +211,13 @@ const Orders = () => {
const [searchword, setSearchword] = useState(''); const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState('');
// Floating button and Dialog states
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const [multiDeleteDialog, setMultiDeleteDialog] = useState(false);
const [createloader, setCreateloader] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [deliverylist, setDeliverylist] = useState([]);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedSearch(searchword); setDebouncedSearch(searchword);
@@ -262,13 +225,6 @@ const Orders = () => {
return () => clearTimeout(handler); return () => clearTimeout(handler);
}, [searchword]); }, [searchword]);
const tabCounts = {
created: createdLenght,
pending: pendingLenght,
delivered: deliveredlenght,
cancelled: cancelledLenght
};
const handleChangetab = (e, i) => { const handleChangetab = (e, i) => {
setSearchword(''); setSearchword('');
setRowsPerPage(10); setRowsPerPage(10);
@@ -282,8 +238,39 @@ const Orders = () => {
const textFieldRef = useRef(null); const textFieldRef = useRef(null);
useHotkeyFocus(textFieldRef, 'k'); useHotkeyFocus(textFieldRef, 'k');
// React Queries
const {
data: percentageData,
isLoading: fetchpercentageIsLoading,
refetch: percentagedataRefetch
} = useQuery({
queryKey: ['percentageData', locationId, startdate, enddate, tid, locationId],
queryFn: fetchPercentageData,
enabled: true,
refetchInterval: 15000
});
const {
data: ordersCountData,
refetch: orderscountRefetch
} = useQuery({
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
queryFn: fetchorderscount,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 15000
});
const {
data: autoRiders
} = useQuery({
queryKey: ['getallriders'],
queryFn: getallriders,
refetchOnMount: true,
refetchOnWindowFocus: true
});
const cancelorder = async () => { const cancelorder = async () => {
setLoading(true);
await axios await axios
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, { .put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
orderheaderid: orderheaderid, orderheaderid: orderheaderid,
@@ -298,14 +285,13 @@ const Orders = () => {
autoHideDuration: 2000 autoHideDuration: 2000
}); });
refetchOrders(); refetchOrders();
fetchorderscount(); orderscountRefetch();
percentagedataRefetch();
setCancelOpen(false); setCancelOpen(false);
} }
setLoading(false);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
setLoading(false);
}); });
}; };
@@ -329,7 +315,8 @@ const Orders = () => {
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId], queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId],
queryFn: fetchOrders, queryFn: fetchOrders,
getNextPageParam: (lastPage) => lastPage.nextPage getNextPageParam: (lastPage) => lastPage.nextPage,
refetchInterval: 15000
}); });
const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : []; const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : [];
@@ -343,8 +330,6 @@ const Orders = () => {
} }
}, },
{ {
// The page (viewport) is now the scroll container, not the table.
// Prefetch the next page ~400px before the sentinel reaches the bottom.
root: null, root: null,
rootMargin: '0px 0px 400px 0px', rootMargin: '0px 0px 400px 0px',
threshold: 0 threshold: 0
@@ -365,96 +350,129 @@ const Orders = () => {
} }
}; };
const fetchpercentage = async () => { // ==============================|| Mutations ||============================== //
setLoading(true);
try {
await axios
.get(`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}`)
.then((res) => {
setCoveredorders(res.data.details.delivered.toString());
setCancelled(res.data.details.cancelled.toString());
setUncoveredorders(res.data.details.pending.toString());
setCreated(res.data.details.created.toString());
setPercentage1((Math.round((res.data.details.created / res.data.details.total) * 100) || 0).toString());
setPercentage3((Math.round((res.data.details.delivered / res.data.details.total) * 100) || 0).toString());
setPercentage4((Math.round((res.data.details.cancelled / res.data.details.total) * 100) || 0).toString());
setPercentage2((Math.round((res.data.details.pending / res.data.details.total) * 100) || 0).toString());
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} catch (err) {
console.log(err);
setLoading(false);
}
};
useEffect(() => {
fetchpercentage();
}, []);
const fetchorderscount = async () => { const createDeliveryMutation = useMutation({
setLoading(true); mutationFn: createAutomationDeliveries,
try { onSuccess: (data, variables) => {
await axios enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000 });
.get( orderscountRefetch();
`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}` refetchOrders();
) setCreateloader(false);
.then((res) => { navigate('/nearle/dispatch/preview', {
setCreatedLenght(res.data.details.created); state: {
setPendingLenght(res.data.details.pending); dispatchPreviewData: data,
setDeliveredlenght(res.data.details.delivered); aiMode: 1,
setCancelledLenght(res.data.details.cancelled); selectedMode: { value: 1 },
tabvalue === 0 && setPageCount(res.data.details.created); deliveryData: variables?.deliveries || [],
tabvalue === 1 && setPageCount(res.data.details.pending); appId: locationId,
tabvalue === 2 && setPageCount(res.data.details.delivered); startdate: startdate,
tabvalue === 3 && setPageCount(res.data.details.cancelled); tenantId: tid,
setLoading(false); autoRiders: autoRiders || []
}) }
.catch((err) => {
console.log(err);
setLoading(false);
}); });
} catch (err) { },
console.log(err); onError: (error) => {
setLoading(false); enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
setCreateloader(false);
},
onSettled: () => {
setCreateloader(false);
setIsLoading(false);
} }
}; });
useEffect(() => {
fetchorderscount();
}, [tabvalue, locationId, startdate, enddate]);
// ============================================= || gettenantlocations (branches) || ============================================= const cancelMultipleOrderMutation = useMutation({
const gettenantlocations = async (id) => { mutationFn: cancelMultipleOrder,
try { onSuccess: (data) => {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`); if (data.status) {
setTenantlocations(res.data.details || []); setMultiDeleteDialog(false);
} catch (err) { enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 });
console.log('gettenantlocations', err); refetchOrders();
orderscountRefetch();
percentagedataRefetch();
setDeliverylist([]);
} }
},
onError: (error) => {
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
}
});
const handleCreateDelivery = async () => {
if (rows.length === 0) return;
setIsLoading(true);
setCreateloader(true);
const deliveryData = rows.map((val) => ({
...val,
deliveryid: 0,
deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'),
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderstatus: 'pending',
orderamount: val.deliverycharge,
droplat: val.deliverylat,
droplon: val.deliverylong,
pickuplat: val.pickuplat,
pickuplon: val.pickuplong,
ordernotes: val.ordernotes,
deliverycharges: val.deliverycharge,
pickuplocation: val.pickupsuburb,
deliverylocation: val.deliverysuburb
}));
createDeliveryMutation.mutate({
deliveries: deliveryData,
selectedMode: { value: 1 },
hypertuning_params: 'balanced',
absent_riders: []
});
}; };
useEffect(() => {
gettenantlocations(tenId);
}, []);
// KPI tile definitions. // KPI tile definitions.
const kpiCards = [ const kpiCards = [
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: created, percentage: percentage1 }, { key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 },
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: uncoveredorders, percentage: percentage2 }, { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdCheckCircle, value: coveredorders, percentage: percentage3 },
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdCancel, value: cancelled, percentage: percentage4 }
]; ];
return ( return (
<Fragment> <Fragment>
{loading && ( {(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && (
<> <>
<Loader /> <Loader />
<CircularLoader /> <CircularLoader />
</> </>
)} )}
{rows.length > 0 && currentStatus === 'created' && (
<Button
variant="contained"
onClick={handleCreateDelivery}
sx={{
position: 'fixed',
bottom: 32,
right: 24,
bgcolor: BRAND,
color: '#fff',
px: 3,
py: 1.5,
borderRadius: 999,
fontWeight: 850,
boxShadow: `0 10px 30px ${ring(BRAND)}`,
zIndex: 1000,
textTransform: 'none',
fontSize: '14px',
gap: 1,
'&:hover': {
bgcolor: BRAND_LIGHT,
transform: 'translateY(-2px)'
}
}}
startIcon={<MdLocalShipping size={18} />}
>
Assign All Orders ({rows.length})
</Button>
)}
{/* ============================================= || Header (compact) || ============================================= */} {/* ============================================= || Header (compact) || ============================================= */}
<Paper <Paper
elevation={0} elevation={0}
@@ -518,154 +536,10 @@ const Orders = () => {
</Stack> </Stack>
</Stack> </Stack>
{/* Location picker */}
{tenantLocations.length === 1 ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.5,
py: 0.75,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1.5px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 13
}}
>
<MdMyLocation size={14} /> {tenantLocations[0].locationname}
</Box>
) : (
<Autocomplete
options={tenantLocations || []}
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
setLocationId(value.locationid);
setLocoName(value.locationname);
}
if (reason === 'clear') {
setLocationId(0);
setLocoName('All Locations');
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Location"
size="small"
sx={{ ...pillFieldSx(BRAND), width: { xs: '100%', sm: 260 } }}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color={BRAND} size={22} selected>
<MdMyLocation size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
sx={{ width: { xs: '100%', sm: 280 } }}
/>
)}
</Stack> </Stack>
</Paper> </Paper>
{/* ============================================= || KPI Cards (compact) || ============================================= */}
<Grid container spacing={{ xs: 1, sm: 1.25, md: 1.5 }}>
{kpiCards.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<Paper
elevation={0}
sx={{
position: 'relative',
overflow: 'hidden',
px: { xs: 1.25, sm: 1.5 },
py: { xs: 0.875, sm: 1.125 },
borderRadius: 2,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s',
'&:hover': {
transform: 'translateY(-1px)',
boxShadow: DT.shadowMd,
borderColor: edge(item.color)
}
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
width: 3,
background: item.color
}}
/>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1} sx={{ pl: 0.5 }}>
<Stack spacing={0.125} sx={{ minWidth: 0, flex: 1 }}>
<Typography
sx={{
color: DT.textSecondary,
fontWeight: 700,
letterSpacing: 0.4,
textTransform: 'uppercase',
fontSize: 10.5,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: 1.2
}}
>
{item.label}
</Typography>
<Stack direction="row" alignItems="baseline" spacing={0.75}>
<Typography
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.15,
fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' }
}}
noWrap
>
{item.value === '' ? <Skeleton sx={{ width: 40 }} animation="wave" /> : item.value}
</Typography>
{item.percentage != null && item.value !== '' && (
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 700 }}>
{item.percentage}%
</Typography>
)}
</Stack>
</Stack>
<Avatar
variant="rounded"
sx={{
width: 30,
height: 30,
bgcolor: soft(item.color),
color: item.color,
borderRadius: 1.25,
flexShrink: 0
}}
>
<Icon size={15} />
</Avatar>
</Stack>
</Paper>
</Grid>
);
})}
</Grid>
{/* ============================================= || Filter Bar (compact) || ============================================= */} {/* ============================================= || Filter Bar (compact) || ============================================= */}
<Paper <Paper
@@ -730,42 +604,6 @@ const Orders = () => {
</Box> </Box>
</Stack> </Stack>
{tenantLocations.length > 1 && (
<Autocomplete
options={tenantLocations || []}
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
setLocationId(value.locationid);
setLocoName(value.locationname);
}
if (reason === 'clear') {
setLocationId(0);
setLocoName('All Locations');
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Location"
size="small"
sx={pillFieldSx('#10b981')}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color="#10b981" size={22} selected>
<MdPlace size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
sx={{ width: { xs: '100%', md: 320 } }}
/>
)}
</Stack> </Stack>
</Paper> </Paper>
@@ -807,7 +645,7 @@ const Orders = () => {
{ORDERS_STATUS_TABS.map((t) => { {ORDERS_STATUS_TABS.map((t) => {
const Icon = t.icon; const Icon = t.icon;
const active = tabvalue === t.idx; const active = tabvalue === t.idx;
const count = tabCounts[t.countKey] ?? 0; const count = ordersCountData?.[t.countKey] ?? 0;
return ( return (
<Box <Box
key={t.status} key={t.status}
@@ -870,54 +708,6 @@ const Orders = () => {
})} })}
</Stack> </Stack>
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.5,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1.5px solid ${edge(BRAND)}`,
transition: 'all 0.18s',
'&:focus-within': {
borderColor: BRAND,
boxShadow: `0 0 0 3px ${ring(BRAND)}`
}
}}
>
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
<InputBase
inputRef={textFieldRef}
placeholder="Search (ctrl+k)"
value={searchword}
onChange={(e) => setSearchword(e.target.value)}
autoComplete="off"
sx={{
flex: 1,
fontSize: 13,
fontWeight: 600,
color: DT.textPrimary,
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
}}
/>
{searchword && (
<IconButton
size="small"
onClick={() => {
setSearchword('');
refetchOrders();
fetchorderscount();
}}
sx={{ p: 0.25, color: BRAND }}
>
<MdClear size={14} />
</IconButton>
)}
</Box>
</Box>
</Stack> </Stack>
</Paper> </Paper>
@@ -939,10 +729,6 @@ const Orders = () => {
onScroll={handleScroll} onScroll={handleScroll}
ref={containerRef} ref={containerRef}
sx={{ sx={{
// Single page scroll: the table is NOT height-capped, so it renders at its
// full height and the whole page scrolls as one. Scrolling down moves the
// KPI cards + header + filter bar off-screen and reveals the full table.
// Only horizontal overflow scrolls inside the container (for wide column sets).
overflowX: 'auto', overflowX: 'auto',
overflowY: 'visible', overflowY: 'visible',
'&::-webkit-scrollbar': { width: 10, height: 10 }, '&::-webkit-scrollbar': { width: 10, height: 10 },
@@ -987,11 +773,11 @@ const Orders = () => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{(isLoadingGetOrders || loading) && {(isLoadingGetOrders || createloader) &&
rows.length === 0 && rows.length === 0 &&
Array.from({ length: 10 }).map((_, idx) => ( Array.from({ length: 10 }).map((_, idx) => (
<TableRow key={`sk-${idx}`}> <TableRow key={`sk-${idx}`}>
{Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => ( {Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => (
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}> <TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
<Skeleton animation="wave" height={20} /> <Skeleton animation="wave" height={20} />
</TableCell> </TableCell>
@@ -1001,7 +787,7 @@ const Orders = () => {
{!isLoadingGetOrders && rows.length === 0 && ( {!isLoadingGetOrders && rows.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={currentStatus === 'created' ? 11 : 10} sx={{ py: 7, borderBottom: 'none' }}> <TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ py: 7, borderBottom: 'none' }}>
<Stack alignItems="center" spacing={1.25}> <Stack alignItems="center" spacing={1.25}>
<Avatar <Avatar
variant="rounded" variant="rounded"
@@ -1045,12 +831,25 @@ const Orders = () => {
</TableRow> </TableRow>
)} )}
{rows.map((row, index) => ( {rows.map((row, index) => {
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
const handleCheckbox = (e) => {
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 }))
);
}
};
return (
<TableRow <TableRow
key={`${row.orderheaderid}-${index}`} key={`${row.orderheaderid}-${index}`}
sx={{ sx={{
cursor: 'pointer', cursor: 'pointer',
transition: 'background-color 0.12s, box-shadow 0.12s', transition: 'background-color 0.12s, box-shadow 0.12s',
backgroundColor: 'transparent',
'& td': { '& td': {
borderBottom: `1px solid ${DT.divider}`, borderBottom: `1px solid ${DT.divider}`,
py: 0.5, py: 0.5,
@@ -1058,7 +857,7 @@ const Orders = () => {
verticalAlign: 'top' verticalAlign: 'top'
}, },
'&:hover': { '&:hover': {
backgroundColor: tint(BRAND), backgroundColor: DT.surfaceAlt,
boxShadow: `inset 3px 0 0 ${BRAND}` boxShadow: `inset 3px 0 0 ${BRAND}`
} }
}} }}
@@ -1151,7 +950,7 @@ const Orders = () => {
<TableCell> <TableCell>
{row.ordernotes ? ( {row.ordernotes ? (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}> <Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
<MdNote size={12} style={{ color: DT.textMuted, flexShrink: 0 }} /> <MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
<Typography <Typography
variant="caption" variant="caption"
sx={{ sx={{
@@ -1206,11 +1005,12 @@ const Orders = () => {
</TableCell> </TableCell>
)} )}
</TableRow> </TableRow>
))} );
})}
{rows.length !== 0 && ( {rows.length !== 0 && (
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}> <TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
<TableCell colSpan={currentStatus === 'created' ? 11 : 10} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}> <TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
<Stack <Stack
ref={loadMoreRef} ref={loadMoreRef}
direction="row" direction="row"
@@ -1310,6 +1110,67 @@ const Orders = () => {
setDatestatus(label); setDatestatus(label);
}} }}
/> />
{/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */}
<Dialog open={multiDeleteDialog} onClose={() => setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}>
<Box
sx={{
p: 2.5,
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
borderBottom: `1px solid ${DT.borderSubtle}`
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
<MdDeleteOutline size={20} />
</Avatar>
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Cancel Selected Orders
</Typography>
</Stack>
</Box>
<DialogContent sx={{ pt: 3 }}>
<Stack alignItems="center" spacing={3}>
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone.
</Typography>
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
<Button
fullWidth
onClick={() => setMultiDeleteDialog(false)}
variant="outlined"
sx={{
borderRadius: 999,
py: 1,
borderColor: DT.borderSubtle,
color: DT.textSecondary,
fontWeight: 700,
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
}}
>
No
</Button>
<Button
fullWidth
variant="contained"
onClick={() => cancelMultipleOrderMutation.mutate(deliverylist)}
autoFocus
sx={{
borderRadius: 999,
py: 1,
bgcolor: '#ef4444',
fontWeight: 700,
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
'&:hover': { bgcolor: '#dc2626' }
}}
>
Yes, Cancel
</Button>
</Stack>
</Stack>
</DialogContent>
</Dialog>
</Fragment> </Fragment>
); );
}; };

View File

@@ -22,6 +22,8 @@ const Customers = Loadable(lazy(() => import('pages/nearle/clients/customers')))
const Locations = Loadable(lazy(() => import('pages/nearle/locations/Locations'))); const Locations = Loadable(lazy(() => import('pages/nearle/locations/Locations')));
const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders'))); const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders')));
const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries')));
const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview')));
const Details = Loadable(lazy(() => import('pages/nearle/orders/details'))); const Details = Loadable(lazy(() => import('pages/nearle/orders/details')));
const Accountsettings = Loadable(lazy(() => import('pages/nearle/accountsettings'))); const Accountsettings = Loadable(lazy(() => import('pages/nearle/accountsettings')));
@@ -63,11 +65,15 @@ const MainRoutes = {
path: 'orders', path: 'orders',
children: [ children: [
{ {
path: '', // /orders path: '',
element: <Orders /> element: <Orders />
}, },
{ {
path: 'create/grouporders', // /orders/create/grouporders path: 'preview',
element: <OrdersPreview />
},
{
path: 'create/grouporders',
element: <MultipleOrders /> element: <MultipleOrders />
}, },
{ {
@@ -76,6 +82,10 @@ const MainRoutes = {
} }
] ]
}, },
{
path: 'deliveries',
element: <Deliveries />
},
{ {
path: 'customers', path: 'customers',
children: [ children: [