updates on the deliveries page and the order creation page

This commit is contained in:
2026-05-28 13:38:44 +05:30
parent c77f3c96b7
commit 8cc7cc75f9
3 changed files with 769 additions and 118 deletions

View File

@@ -13,7 +13,7 @@ import Loader from 'components/Loader';
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
import { PiMapPinLineDuotone } from 'react-icons/pi';
import { MdOutlineDateRange } from 'react-icons/md';
import { MdOutlineDateRange, MdPersonOff, MdEventBusy } from 'react-icons/md';
import { VscArchive } from 'react-icons/vsc';
import DateFilterDialog from 'components/DateFilterDialog';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
@@ -29,11 +29,7 @@ import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlin
import WarningIcon from '@mui/icons-material/Warning';
import BoltIcon from '@mui/icons-material/Bolt';
import { ArrowRightAltOutlined } from '@mui/icons-material';
import { DashboardFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi';
import { GiProfit } from 'react-icons/gi';
import { IoReload } from 'react-icons/io5';
@@ -74,7 +70,8 @@ import {
Divider,
AccordionDetails,
AccordionSummary,
Accordion
Accordion,
Paper
} from '@mui/material';
import {
@@ -152,6 +149,10 @@ const Orders = () => {
{ label: 'Auto', value: 2 }
];
const [selectedMode, setSelectedMode] = useState(null);
// Riders the operator has marked as absent for today — passed to the AI
// assign API so its allocation skips them. Optional: the AI run still
// works with an empty list.
const [absentRiders, setAbsentRiders] = useState([]);
// when orders are selected to show popup to stop unwanted reload
@@ -549,6 +550,18 @@ const Orders = () => {
deliverylocation: val.deliverysuburb
}));
console.log('deliveryData', deliveryData);
// Normalize the operator's "Absent Riders" picks into the {userid, username}
// shape the AI assign API expects. The API returns firstname/lastname, so
// we join them into a single `username` string (with a fallback to the
// raw `username` field when names are missing).
const absentRidersPayload = (absentRiders || []).map((r) => ({
userid: r.userid,
username:
`${r.firstname || ''} ${r.lastname || ''}`.trim() ||
r.username ||
`Rider ${r.userid}`
}));
if (aiModeRef.current == 0) {
// manual assign
createDeliveryMutation.mutate({
@@ -561,7 +574,8 @@ const Orders = () => {
// mode 1 -> bike , 2 -> auto
deliveries: selectedMode.value == 1 ? deliveryData : { body: deliveryData },
hypertuning_params: tune || null,
selectedMode
selectedMode,
absent_riders: absentRidersPayload
// reshuffle: 'joshi'
});
} else {
@@ -574,7 +588,8 @@ const Orders = () => {
pay_type: 'hourly', // options: "hourly", "daily", or "orders"
base_pay: 300.0,
strategy: 'multi_trip'
}
},
absent_riders: absentRidersPayload
},
selectedMode
});
@@ -634,6 +649,8 @@ const Orders = () => {
setAssignDialog(false);
setRider(null);
setPayment(null);
setSelectedMode(null);
setAbsentRiders([]);
};
const errorMessage = fetchpercentageIsError
@@ -1716,6 +1733,235 @@ const Orders = () => {
/>
)}
{/* Absent Riders multi-select — optional. Operator picks the
riders who are off today; the AI assign API receives their
{userid, username} under `absent_riders` and skips them
during allocation. Leaving it empty is valid: the AI run
proceeds against every rider in the location. */}
{aiModeRef.current == 1 && (
<Autocomplete
multiple
fullWidth={false}
options={ridersList || []}
loading={ridersListLoading}
disableCloseOnSelect
limitTags={2}
value={absentRiders}
onChange={(event, newValue) => setAbsentRiders(newValue)}
getOptionLabel={(option) => {
const name = `${option.firstname || ''} ${option.lastname || ''}`.trim();
return name || `Rider #${option.userid}`;
}}
isOptionEqualToValue={(option, value) => option.userid === value.userid}
PaperComponent={(paperProps) => (
<Paper
{...paperProps}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.18)',
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
)}
ListboxProps={{ sx: { py: 0, maxHeight: 340 } }}
noOptionsText={
<Stack alignItems="center" py={2} spacing={1}>
<MdEventBusy size={26} color="#94a3b8" />
<Typography variant="caption" color="text.secondary">
No riders to show for this location
</Typography>
</Stack>
}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
const name =
`${option.firstname || ''} ${option.lastname || ''}`.trim() ||
`Rider #${option.userid}`;
const initials =
(name.match(/\b\w/g) || []).slice(0, 2).join('').toUpperCase() || '?';
const tagProps = getTagProps({ index });
return (
<Chip
{...tagProps}
key={option.userid}
size="small"
avatar={
<Avatar
sx={{
bgcolor: '#fed7aa',
color: '#9a3412',
fontSize: 10,
fontWeight: 800
}}
>
{initials}
</Avatar>
}
label={name}
sx={{
height: 26,
bgcolor: '#fff7ed',
color: '#9a3412',
border: '1px solid',
borderColor: '#fdba74',
fontWeight: 600,
fontSize: 12,
'& .MuiChip-label': { px: 0.75 },
'& .MuiChip-deleteIcon': {
color: '#c2410c',
fontSize: 16,
'&:hover': { color: '#7c2d12' }
}
}}
/>
);
})
}
renderOption={(props, option, { selected }) => {
const name = `${option.firstname || ''} ${option.lastname || ''}`.trim();
const initials =
(name.match(/\b\w/g) || []).slice(0, 2).join('').toUpperCase() || '?';
return (
<li
{...props}
key={option.userid}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 12px',
borderBottom: '1px solid #f1f5f9',
backgroundColor: selected ? '#fff7ed' : 'transparent'
}}
>
<Checkbox
size="small"
checked={selected}
sx={{
p: 0.5,
color: '#fb923c',
'&.Mui-checked': { color: '#ea580c' }
}}
/>
<Avatar
sx={{
width: 34,
height: 34,
bgcolor: selected ? '#fb923c' : '#f1f5f9',
color: selected ? '#fff' : '#475569',
fontSize: 12,
fontWeight: 800,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{initials}
</Avatar>
<Stack direction="column" spacing={0} flex={1} minWidth={0}>
<Typography
variant="body2"
fontWeight={700}
color="#0f172a"
noWrap
>
{name || `Rider #${option.userid}`}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
ID #{option.userid}
{option.contactno ? ` · ${option.contactno}` : ''}
</Typography>
</Stack>
{selected && (
<Chip
size="small"
label="Absent"
sx={{
height: 18,
fontSize: 9,
fontWeight: 800,
letterSpacing: 0.3,
bgcolor: '#ea580c',
color: '#fff',
'& .MuiChip-label': { px: 0.75 }
}}
/>
)}
</li>
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Absent Riders"
placeholder={absentRiders.length ? '' : 'Pick riders unavailable today'}
InputLabelProps={{ shrink: true }}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack
direction="row"
alignItems="center"
spacing={0.75}
sx={{ pl: 0.5, mr: 0.25 }}
>
<MdPersonOff size={18} color="#ea580c" />
{absentRiders.length > 0 && (
<Chip
size="small"
label={absentRiders.length}
sx={{
height: 18,
minWidth: 22,
fontSize: 10,
fontWeight: 800,
bgcolor: '#ea580c',
color: '#fff',
'& .MuiChip-label': { px: 0.5 }
}}
/>
)}
{params.InputProps.startAdornment}
</Stack>
)
}}
sx={{
minWidth: 300,
maxWidth: 440,
'& .MuiOutlinedInput-root': {
borderRadius: 2,
bgcolor: '#fffbf5',
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': {
borderColor: '#fdba74',
borderWidth: 1.5
},
'&:hover fieldset': { borderColor: '#fb923c' },
'&.Mui-focused': {
boxShadow: '0 0 0 3px rgba(251, 146, 60, 0.18)'
},
'&.Mui-focused fieldset': {
borderColor: '#ea580c',
borderWidth: 2
}
},
'& .MuiInputLabel-root': {
fontWeight: 700,
fontSize: 13,
color: '#9a3412',
'&.Mui-focused': { color: '#9a3412' }
}
}}
/>
)}
sx={{
'& .MuiAutocomplete-tag': { my: 0.25 },
'& .MuiAutocomplete-endAdornment': { right: 8 }
}}
/>
)}
<Button
color={'primary'}
disabled={!selectedMode && aiModeRef.current == 1}
@@ -2108,7 +2354,7 @@ const Orders = () => {
<CircularLoader color="inherit" />
</Backdrop>
}
<DialogTitle>
<DialogTitle sx={{ py: 1.25 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{}}>
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
@@ -2190,51 +2436,13 @@ const Orders = () => {
/>
</Stack>
</Stack>
<Stack sx={{ my: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.total_orders}
primary={'Orders'}
percentage={<DashboardFilled />}
color={theme.palette.success.main}
sx={{ cursor: 'pointer' }}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.utilized_riders}
primary={'Riders'}
percentage={<MdDirectionsBike />}
color={theme.palette.warning.main}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={zoneData?.length}
primary={'Zones'}
percentage={<FaMapLocationDot />}
color={theme.palette.info.main}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.total_profit}
primary={'Profit'}
percentage={<GiProfit />}
color={theme.palette.error.main}
/>
</Grid>
</Grid>
</Stack>
</DialogTitle>
<DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
{dispatchPreviewData && <Dispatch data={dispatchPreviewData} embedded />}
</DialogContent>
<DialogActions>
<Stack display={'flex'} flexDirection={'row'} gap={2} alignItems={'center'} justifyContent={'end'} sx={{ p: 2 }}>
<DialogActions sx={{ px: 2, py: 1.25 }}>
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
<Button
sx={{}}
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
@@ -2245,7 +2453,7 @@ const Orders = () => {
>
Back
</Button>
<Button sx={{ my: 2 }} variant="contained" onClick={handleFinalCreateDelivery}>
<Button variant="contained" onClick={handleFinalCreateDelivery}>
Assign Orders
</Button>
</Stack>