updated the ui for the mobile view

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

View File

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