updates on the orders and assign page
This commit is contained in:
BIN
src/assets/images/aiImage.png
Normal file
BIN
src/assets/images/aiImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 759 KiB |
34
src/components/nearle_components/LoaderWithImage.js
Normal file
34
src/components/nearle_components/LoaderWithImage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
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
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { TbListDetails } from 'react-icons/tb';
|
||||
import { LiaFileInvoiceSolid } from 'react-icons/lia';
|
||||
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
|
||||
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
|
||||
|
||||
// assets
|
||||
import {
|
||||
@@ -64,6 +65,13 @@ const nearle = {
|
||||
url: '/nearle/orders',
|
||||
icon: AiOutlineDashboard
|
||||
},
|
||||
{
|
||||
id: 'deliveries',
|
||||
title: <FormattedMessage id="Deliveries" />,
|
||||
type: 'item',
|
||||
url: '/nearle/deliveries',
|
||||
icon: MopedOutlinedIcon
|
||||
},
|
||||
{
|
||||
id: 'locations',
|
||||
title: <FormattedMessage id="Locations" />,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AiOutlineBarChart } from 'react-icons/ai';
|
||||
import { AiOutlineDashboard } from 'react-icons/ai';
|
||||
import { TbListDetails } from 'react-icons/tb';
|
||||
import { LiaFileInvoiceSolid } from 'react-icons/lia';
|
||||
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
|
||||
|
||||
// assets
|
||||
import {
|
||||
@@ -60,6 +61,13 @@ const other = {
|
||||
url: 'nearle/orders',
|
||||
icon: AiOutlineDashboard
|
||||
},
|
||||
{
|
||||
id: 'deliveries',
|
||||
title: <FormattedMessage id="Deliveries" />,
|
||||
type: 'item',
|
||||
url: 'nearle/deliveries',
|
||||
icon: MopedOutlinedIcon
|
||||
},
|
||||
{
|
||||
id: 'customers',
|
||||
title: <FormattedMessage id="Customers" />,
|
||||
|
||||
@@ -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)||============================== //
|
||||
export const fetchOrderSummary = async () => {
|
||||
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 url =
|
||||
variables.selectedMode.value == 1
|
||||
variables.selectedMode?.value == 1
|
||||
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
|
||||
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
|
||||
|
||||
const body =
|
||||
variables.selectedMode.value == 1
|
||||
variables.selectedMode?.value == 1
|
||||
? { deliveries: variables.deliveries, 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
|
||||
};
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
1104
src/pages/nearle/deliveries/deliveries.js
Normal file
1104
src/pages/nearle/deliveries/deliveries.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2287,6 +2287,10 @@
|
||||
color: var(--ad-accent, var(--accent));
|
||||
}
|
||||
|
||||
.dispatch-container .adcard-m-time {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.dispatch-container .adcard-ic {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
MdInsights,
|
||||
MdRefresh
|
||||
} from 'react-icons/md';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../api/api';
|
||||
import {
|
||||
STATUS_STYLES,
|
||||
@@ -2609,15 +2610,20 @@ const Dispatch = ({
|
||||
<span />
|
||||
)}
|
||||
<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 && (
|
||||
<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>
|
||||
{formatMeters(estMeters)}
|
||||
</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>
|
||||
</div>
|
||||
@@ -3465,7 +3471,9 @@ const Dispatch = ({
|
||||
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>
|
||||
<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
|
||||
type="button"
|
||||
className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`}
|
||||
@@ -4641,6 +4649,14 @@ const Dispatch = ({
|
||||
// same `visibleRiders` set the map uses keeps the sidebar
|
||||
// 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
|
||||
.map((r) => getActiveOrder(r.orders))
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -552,62 +552,39 @@ const Preview = () => {
|
||||
<CircularLoader color="inherit" />
|
||||
</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" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Tooltip title="Back to orders" placement="top">
|
||||
<IconButton
|
||||
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>
|
||||
</Tooltip>
|
||||
<Typography variant="h3" fontWeight={600}>
|
||||
<Typography variant="h3" fontWeight={700} sx={{ color: '#0f172a' }}>
|
||||
Assign Orders
|
||||
</Typography>
|
||||
</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>
|
||||
</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 sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{tabValue === 0 && dispatchPreviewData && (
|
||||
{dispatchPreviewData && (
|
||||
<Dispatch
|
||||
// The key forces a full re-mount when the cache reference changes
|
||||
// (after Change Rider / Reconcile / Re-Assign) so Dispatch's
|
||||
@@ -620,144 +597,57 @@ const Preview = () => {
|
||||
onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)}
|
||||
/>
|
||||
)}
|
||||
{tabValue === 1 && (
|
||||
<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={{
|
||||
bgcolor: hasReconciled ? '#ecfdf5' : '#fffbeb',
|
||||
border: `1px solid ${hasReconciled ? '#a7f3d0' : '#fde68a'}`,
|
||||
color: hasReconciled ? '#065f46' : '#92400e',
|
||||
borderRadius: '10px',
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
fontSize: 13
|
||||
px: 3,
|
||||
py: 1.75,
|
||||
borderTop: '1px solid #e2e8f0',
|
||||
bgcolor: '#ffffff',
|
||||
boxShadow: '0 -4px 20px rgba(0, 0, 0, 0.03)'
|
||||
}}
|
||||
>
|
||||
{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">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
borderRadius: 999,
|
||||
px: 3,
|
||||
py: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#64748b',
|
||||
textTransform: 'none',
|
||||
fontWeight: 700,
|
||||
'&:hover': {
|
||||
borderColor: '#cbd5e1',
|
||||
bgcolor: '#f8fafc'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</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
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
768
src/pages/nearle/orders/OrdersPreview.js
Normal file
768
src/pages/nearle/orders/OrdersPreview.js
Normal 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;
|
||||
49
src/pages/nearle/orders/OrdersTableSkeleton.js
Normal file
49
src/pages/nearle/orders/OrdersTableSkeleton.js
Normal 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
@@ -5,7 +5,6 @@ import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -25,11 +24,15 @@ import {
|
||||
DialogContent,
|
||||
Tooltip,
|
||||
Skeleton,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
InputBase,
|
||||
InputAdornment
|
||||
Backdrop,
|
||||
SpeedDial,
|
||||
SpeedDialIcon,
|
||||
SpeedDialAction,
|
||||
Badge,
|
||||
TableContainer,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MdAccessTime,
|
||||
@@ -42,22 +45,28 @@ import {
|
||||
MdHourglassEmpty,
|
||||
MdInventory2,
|
||||
MdLocalShipping,
|
||||
MdLocationOn,
|
||||
MdMyLocation,
|
||||
MdNote,
|
||||
MdPlace,
|
||||
MdSearch,
|
||||
MdStraighten,
|
||||
MdCalendarMonth,
|
||||
MdReceiptLong,
|
||||
MdClear
|
||||
MdClear,
|
||||
MdNotes
|
||||
} from 'react-icons/md';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import Loader from 'components/Loader';
|
||||
import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus';
|
||||
import DateFilterDialog from 'components/nearle_components/DateFilterDialog';
|
||||
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.
|
||||
@@ -82,48 +91,14 @@ const soft = (c) => dtA(c, '18');
|
||||
const ring = (c) => dtA(c, '26');
|
||||
const edge = (c) => dtA(c, '55');
|
||||
|
||||
const dtTint = tint;
|
||||
const dtSoft = soft;
|
||||
const dtRing = ring;
|
||||
const dtEdge = edge;
|
||||
|
||||
const BRAND = '#662582';
|
||||
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:
|
||||
// green=delivered, amber=pending, blue=created/processing, red=cancelled,
|
||||
@@ -145,13 +120,9 @@ const ROW_STATUS_META = {
|
||||
|
||||
// Top-level pill tabs.
|
||||
const ORDERS_STATUS_TABS = [
|
||||
{ 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' }
|
||||
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
|
||||
];
|
||||
|
||||
// Filled status badge — high-contrast pill (white text on solid color).
|
||||
const StatusBadge = ({ status }) => {
|
||||
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
|
||||
label: status || '—',
|
||||
@@ -219,35 +190,20 @@ const MetricCell = ({ value, color, icon, isMoney = false }) => {
|
||||
};
|
||||
|
||||
const Orders = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const tid = localStorage.getItem('tenantid');
|
||||
const tenId = localStorage.getItem('tenantid');
|
||||
const loadMoreRef = useRef();
|
||||
const containerRef = useRef();
|
||||
const [page, setPage] = useState(0);
|
||||
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 [tabstatus, setTabstatus] = 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 [orderheaderid, setOrderheaderid] = useState('');
|
||||
const [locationId, setLocationId] = useState(0);
|
||||
const [locoName, setLocoName] = useState('All Locations');
|
||||
const locationId = 0;
|
||||
const locoName = 'All Locations';
|
||||
const [dateOpen, setDateOpen] = useState(false);
|
||||
const [datestatus, setDatestatus] = useState('Today');
|
||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
@@ -255,6 +211,13 @@ const Orders = () => {
|
||||
const [searchword, setSearchword] = 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(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearch(searchword);
|
||||
@@ -262,13 +225,6 @@ const Orders = () => {
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchword]);
|
||||
|
||||
const tabCounts = {
|
||||
created: createdLenght,
|
||||
pending: pendingLenght,
|
||||
delivered: deliveredlenght,
|
||||
cancelled: cancelledLenght
|
||||
};
|
||||
|
||||
const handleChangetab = (e, i) => {
|
||||
setSearchword('');
|
||||
setRowsPerPage(10);
|
||||
@@ -282,8 +238,39 @@ const Orders = () => {
|
||||
const textFieldRef = useRef(null);
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
||||
orderheaderid: orderheaderid,
|
||||
@@ -298,14 +285,13 @@ const Orders = () => {
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
refetchOrders();
|
||||
fetchorderscount();
|
||||
orderscountRefetch();
|
||||
percentagedataRefetch();
|
||||
setCancelOpen(false);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -329,7 +315,8 @@ const Orders = () => {
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId],
|
||||
queryFn: fetchOrders,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
refetchInterval: 15000
|
||||
});
|
||||
|
||||
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,
|
||||
rootMargin: '0px 0px 400px 0px',
|
||||
threshold: 0
|
||||
@@ -365,96 +350,129 @@ const Orders = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchpercentage = async () => {
|
||||
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();
|
||||
}, []);
|
||||
// ==============================|| Mutations ||============================== //
|
||||
|
||||
const fetchorderscount = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.get(
|
||||
`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}`
|
||||
)
|
||||
.then((res) => {
|
||||
setCreatedLenght(res.data.details.created);
|
||||
setPendingLenght(res.data.details.pending);
|
||||
setDeliveredlenght(res.data.details.delivered);
|
||||
setCancelledLenght(res.data.details.cancelled);
|
||||
tabvalue === 0 && setPageCount(res.data.details.created);
|
||||
tabvalue === 1 && setPageCount(res.data.details.pending);
|
||||
tabvalue === 2 && setPageCount(res.data.details.delivered);
|
||||
tabvalue === 3 && setPageCount(res.data.details.cancelled);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
const createDeliveryMutation = useMutation({
|
||||
mutationFn: createAutomationDeliveries,
|
||||
onSuccess: (data, variables) => {
|
||||
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000 });
|
||||
orderscountRefetch();
|
||||
refetchOrders();
|
||||
setCreateloader(false);
|
||||
navigate('/nearle/dispatch/preview', {
|
||||
state: {
|
||||
dispatchPreviewData: data,
|
||||
aiMode: 1,
|
||||
selectedMode: { value: 1 },
|
||||
deliveryData: variables?.deliveries || [],
|
||||
appId: locationId,
|
||||
startdate: startdate,
|
||||
tenantId: tid,
|
||||
autoRiders: autoRiders || []
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
|
||||
setCreateloader(false);
|
||||
},
|
||||
onSettled: () => {
|
||||
setCreateloader(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchorderscount();
|
||||
}, [tabvalue, locationId, startdate, enddate]);
|
||||
});
|
||||
|
||||
// ============================================= || gettenantlocations (branches) || =============================================
|
||||
const gettenantlocations = async (id) => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
|
||||
setTenantlocations(res.data.details || []);
|
||||
} catch (err) {
|
||||
console.log('gettenantlocations', err);
|
||||
const cancelMultipleOrderMutation = useMutation({
|
||||
mutationFn: cancelMultipleOrder,
|
||||
onSuccess: (data) => {
|
||||
if (data.status) {
|
||||
setMultiDeleteDialog(false);
|
||||
enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 });
|
||||
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.
|
||||
const kpiCards = [
|
||||
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: created, percentage: percentage1 },
|
||||
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: uncoveredorders, percentage: 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 }
|
||||
{ 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: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{loading && (
|
||||
{(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && (
|
||||
<>
|
||||
<Loader />
|
||||
<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) || ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
@@ -518,154 +536,10 @@ const Orders = () => {
|
||||
</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>
|
||||
</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) || ============================================= */}
|
||||
<Paper
|
||||
@@ -730,42 +604,6 @@ const Orders = () => {
|
||||
</Box>
|
||||
</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>
|
||||
</Paper>
|
||||
|
||||
@@ -807,7 +645,7 @@ const Orders = () => {
|
||||
{ORDERS_STATUS_TABS.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const active = tabvalue === t.idx;
|
||||
const count = tabCounts[t.countKey] ?? 0;
|
||||
const count = ordersCountData?.[t.countKey] ?? 0;
|
||||
return (
|
||||
<Box
|
||||
key={t.status}
|
||||
@@ -870,54 +708,6 @@ const Orders = () => {
|
||||
})}
|
||||
</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>
|
||||
</Paper>
|
||||
|
||||
@@ -939,10 +729,6 @@ const Orders = () => {
|
||||
onScroll={handleScroll}
|
||||
ref={containerRef}
|
||||
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',
|
||||
overflowY: 'visible',
|
||||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||||
@@ -987,11 +773,11 @@ const Orders = () => {
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{(isLoadingGetOrders || loading) &&
|
||||
{(isLoadingGetOrders || createloader) &&
|
||||
rows.length === 0 &&
|
||||
Array.from({ length: 10 }).map((_, 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 }}>
|
||||
<Skeleton animation="wave" height={20} />
|
||||
</TableCell>
|
||||
@@ -1001,7 +787,7 @@ const Orders = () => {
|
||||
|
||||
{!isLoadingGetOrders && rows.length === 0 && (
|
||||
<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}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
@@ -1045,12 +831,25 @@ const Orders = () => {
|
||||
</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
|
||||
key={`${row.orderheaderid}-${index}`}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||||
backgroundColor: 'transparent',
|
||||
'& td': {
|
||||
borderBottom: `1px solid ${DT.divider}`,
|
||||
py: 0.5,
|
||||
@@ -1058,7 +857,7 @@ const Orders = () => {
|
||||
verticalAlign: 'top'
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: tint(BRAND),
|
||||
backgroundColor: DT.surfaceAlt,
|
||||
boxShadow: `inset 3px 0 0 ${BRAND}`
|
||||
}
|
||||
}}
|
||||
@@ -1151,7 +950,7 @@ const Orders = () => {
|
||||
<TableCell>
|
||||
{row.ordernotes ? (
|
||||
<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
|
||||
variant="caption"
|
||||
sx={{
|
||||
@@ -1206,11 +1005,12 @@ const Orders = () => {
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{rows.length !== 0 && (
|
||||
<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
|
||||
ref={loadMoreRef}
|
||||
direction="row"
|
||||
@@ -1310,6 +1110,67 @@ const Orders = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ const Customers = Loadable(lazy(() => import('pages/nearle/clients/customers')))
|
||||
const Locations = Loadable(lazy(() => import('pages/nearle/locations/Locations')));
|
||||
|
||||
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 Accountsettings = Loadable(lazy(() => import('pages/nearle/accountsettings')));
|
||||
@@ -63,11 +65,15 @@ const MainRoutes = {
|
||||
path: 'orders',
|
||||
children: [
|
||||
{
|
||||
path: '', // /orders
|
||||
path: '',
|
||||
element: <Orders />
|
||||
},
|
||||
{
|
||||
path: 'create/grouporders', // /orders/create/grouporders
|
||||
path: 'preview',
|
||||
element: <OrdersPreview />
|
||||
},
|
||||
{
|
||||
path: 'create/grouporders',
|
||||
element: <MultipleOrders />
|
||||
},
|
||||
{
|
||||
@@ -76,6 +82,10 @@ const MainRoutes = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'deliveries',
|
||||
element: <Deliveries />
|
||||
},
|
||||
{
|
||||
path: 'customers',
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user