Files
nearle_console/src/pages/nearle/orders/multipleOrders.js

2024 lines
82 KiB
JavaScript

import React, { useEffect, useState, useRef, Fragment } from 'react';
import axios from 'axios';
import Papa from 'papaparse';
import * as XLSX from 'xlsx';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { enqueueSnackbar } from 'notistack';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import {
FormControl,
InputAdornment,
Grid,
Typography,
Stack,
Box,
Card,
Button,
TextField,
Autocomplete,
Divider,
Dialog,
DialogTitle,
DialogContent,
Checkbox,
DialogActions,
CircularProgress,
IconButton,
OutlinedInput,
FormGroup,
FormControlLabel,
Table,
TableContainer,
TableCell,
TableBody,
TableRow,
Paper,
TableHead,
Backdrop,
Chip,
Tooltip
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import ClearIcon from '@mui/icons-material/Clear';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import {
SearchOutlined,
CloseOutlined,
ExclamationCircleOutlined,
FileAddOutlined,
CalendarOutlined,
ClockCircleOutlined,
FileTextOutlined,
InboxOutlined,
LockOutlined,
CheckCircleFilled
} from '@ant-design/icons';
import { Empty } from 'antd';
import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa';
import { FaLocationDot } from 'react-icons/fa6';
import { MdOutlineCloudUpload } from 'react-icons/md';
import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import AnimateButton from 'components/@extended/AnimateButton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import './OrdersRedesign.css';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
// ============================== small style tokens ==============================
const cellHeaderSx = {
fontSize: 11.5,
fontWeight: 700,
color: '#475569',
py: 0.75,
px: 1
};
const cellBodySx = {
fontSize: 12,
py: 0.6,
px: 1
};
const MultipleOrders = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const locationRef = useRef(null);
const tenantRef = useRef(null);
const userid = localStorage.getItem('userid');
// ============================== state ==============================
const [locations, setLocations] = useState([]);
const [tenantlist, setTenantlist] = useState([]);
const [tenantLocations, setTenantlocations] = useState([]);
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [appId, setAppId] = useState(0);
const [tenantid, setTenantid] = useState(0);
const [locationid, setLocationid] = useState(0);
const [tenantValue, setTenantValue] = useState(null);
const [locationValue, setLocationValue] = useState(null);
const [basePrice, setBasePrice] = useState(0);
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState([]);
const [customerlist, setCustomerlist] = useState([]);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
const [selectedtime, setSelectedtime] = useState('');
const [pickupSlotsList, setPickupSlotsList] = useState(null);
const [pickupSlot, setPickupSlot] = useState(null);
const [otherinstructions, setOtherinstructions] = useState('');
const [admintoken, setAdmintoken] = useState();
const [totaldist, settotaldist] = useState(0);
const [totalAmt, settotalAmt] = useState(0);
const [totalQty, settotalQty] = useState(0);
const [totalCash, settotalCash] = useState(0);
const [uploadType, setUploadType] = useState(null);
const [users, setUsers] = useState([]);
const [fileName, setFileName] = useState('');
// Stable dedup cache for OpenToast. Was a `let` inside the component body,
// which got recreated on every render and broke the dedup. A ref persists
// across renders without triggering re-renders itself.
const toastCacheRef = useRef({});
// ============================== toast ==============================
const opentoast = (message, variant, time) => {
enqueueSnackbar(message, {
variant: variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 1500
});
};
const OpenToast = (message, type = 'info', timeout = 3000) => {
const key = `${type}-${message}`;
if (toastCacheRef.current[key]) return;
opentoast(message, type, timeout);
toastCacheRef.current[key] = true;
setTimeout(() => delete toastCacheRef.current[key], 3000);
};
// ============================== effects: reset chains ==============================
useEffect(() => {
// appId change → clear downstream selections (tenant, location, customers)
setTenantid(0);
setTenantValue(null);
setLocationid(0);
setLocationValue(null);
setTenantlocations([]);
setPickCust(null);
setDropCust([]);
setUsers([]);
setUploadType(null);
setFileName('');
setPickupSlotsList(null);
setPickupSlot(null);
setSelectedtime('');
}, [appId]);
useEffect(() => {
// tenantid change → clear location/customers (keep appId)
setLocationid(0);
setLocationValue(null);
setDropCust([]);
setUsers([]);
setUploadType(null);
setFileName('');
}, [tenantid]);
// ============================== fetchAppLocations ==============================
const fetchAppLocations = async () => {
setLoading(true);
try {
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
setLocations(locationRes.data.details || []);
} catch (err) {
OpenToast(err.message, 'error', 5000);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAppLocations();
}, []);
// ============================== fetchtenantinfolist ==============================
const fetchtenantinfolist = async () => {
setLoading(true);
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
if (res.data.status) {
const arr = (res.data.details || []).map((val) => ({ ...val, label: `${val.tenantname}` }));
setTenantlist(arr);
}
} catch (err) {
OpenToast('Failed to load clients', 'warning', 3000);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (appId) fetchtenantinfolist();
}, [appId]);
// ============================== fetchTenantPricing ==============================
const fetchTenantPricing = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
const d = res.data.details || {};
setBasePrice(d.baseprice || 0);
setPricePerKm(d.priceperkm || 0);
setMinKm(d.minkm || 0);
} catch (error) {
console.log('fetchTenantPricing error', error);
}
};
// ============================== gettenantlocations ==============================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
const details = res.data.details || [];
if (details.length === 1) {
setTenantlocations(details);
setPickCust(details[0]);
setLocationid(details[0].locationid);
setLocationValue(details[0]);
setPickupSlotsList(details[0].slots);
} else {
setTenantlocations(details);
}
} catch (err) {
console.log('gettenantlocations', err);
}
};
// ============================== clientdetails ==============================
const clientdetails = async () => {
try {
const url =
searchCustList === ''
? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30`
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`;
const res = await axios.get(url);
if (res.data.status) setCustomerlist(res.data.details || []);
} catch (err) {
console.log(err);
opentoast('server error', 'warning');
}
};
useEffect(() => {
if (!tenantid) return;
// Light debounce so we don't fire on every keystroke.
const t = setTimeout(() => {
if (searchCustList === '' || searchCustList.length > 2) clientdetails();
}, 250);
return () => clearTimeout(t);
}, [searchCustList, tenantid]);
// ============================== totals ==============================
useEffect(() => {
let a1 = 0;
let a2 = 0;
let a3 = 0;
let a4 = 0;
dropCust.forEach((c) => {
a1 += Number(c.distance) || 0;
a2 += Number(c.totalcharge) || 0;
a3 += Number(c.quantity) || 0;
a4 += Number(c.collectionamt) || 0;
});
settotaldist(a1);
settotalAmt(a2);
settotalQty(a3);
settotalCash(a4);
}, [dropCust]);
// ============================== distance (OSRM/Haversine) ==============================
const calculateDistance = async (customer) => {
try {
if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.');
if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.');
const roundedDistance = await calculateDrivingDistance(pickCust, customer);
const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
return { roundedDistance, totalcharge };
} catch (error) {
if (error.message.includes('Origin') || error.message.includes('customer') || error.message.includes('coordinates')) {
OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000);
} else {
OpenToast('Unexpected error during distance calculation.', 'error', 3000);
}
throw error;
}
};
// ============================== handleCheckboxChange (dialog: add/remove on tick) ==============================
const handleCheckboxChange = async (event, customer) => {
setLoading(true);
try {
if (event.target.checked) {
const { roundedDistance, totalcharge } = await calculateDistance(customer);
setDropCust((prev) => [...prev, { ...customer, distance: roundedDistance, totalcharge }]);
} else {
setDropCust((prev) => prev.filter((c) => c.customerid !== customer.customerid));
}
} catch (err) {
console.error('Failed to calculate distance:', err);
} finally {
setLoading(false);
}
};
// Toggle handler used by:
// 1. CSV "Continue" bulk add (matches by firstname since uploaded rows
// don't carry a stable customerid).
// 2. Per-row remove (CloseOutlined) in the drop table.
// Uses a single functional updater + locally-captured already-selected flag
// so we don't read stale `dropCust` after the state change.
const handleCheckboxChange1 = async (customer) => {
// Compute "already selected" from the latest state synchronously.
let wasSelected = false;
setDropCust((prev) => {
wasSelected = prev.some((c) => c.firstname === customer.firstname);
if (wasSelected) {
return prev.filter((c) => c.firstname !== customer.firstname);
}
return prev;
});
if (wasSelected) return; // remove path is done
// Add path — compute distance, then append.
setLoading(true);
try {
const { roundedDistance, totalcharge } = await calculateDistance(customer);
setDropCust((prev) => {
// Guard against parallel adds for the same row.
if (prev.some((c) => c.firstname === customer.firstname)) return prev;
return [...prev, { ...customer, distance: roundedDistance, totalcharge }];
});
} catch (err) {
console.error('Failed to calculate distance:', err);
} finally {
setLoading(false);
}
};
// ============================== fetchAppAdminTokens ==============================
const fetchAppAdminTokens = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`);
if (res.data.status) {
const tokens = res.data.details.applocationadmins.map((a) => a.userfcmtokem);
setAdmintoken(tokens);
}
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (appId) fetchAppAdminTokens();
}, [appId]);
// ============================== sendnotifications ==============================
const sendnotifications = async () => {
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
priority: 'high',
registration_ids: admintoken,
data: { accessid: process.env.REACT_APP_RIDER_ACCESS_ID },
notification: {
title: 'Nearle Merchant',
body: 'An Order has been placed successfully, kindly process the same',
sound: 'ring'
}
});
if (res.data.message === 'Success') {
opentoast('Notification sent Successfully', 'success', 1000);
}
} catch (err) {
opentoast(err.message, 'error', 1000);
}
};
// ============================== CSV / XLSX upload ==============================
const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name);
const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, '');
const headerMap = {
'pickupdate(yyyy-mmm-dd)': 'date',
'sendername*': 'locationname',
'senderphone*': 'locationcontact',
'senderaddress*': 'locationaddress',
'receivername*': 'firstname',
receiverphone: 'contactno',
'receiveralternatephone*': 'altcontactno',
receiverfulladdress: 'address',
receiverlatitude: 'latitude',
receiverlongitude: 'longitude',
'itemdescription*': 'description',
Quantity: 'quantity',
' Collect Cash': 'collectionamt',
customerDeliveryTime: 'customerdeliverytime',
kitchenPickupTime: 'kitchenpickuptime'
};
const handleFileDirectUpload = (event) => {
try {
const file = event.target.files?.[0];
if (!file) {
opentoast('No file selected.', 'warning');
return;
}
const fileNameLower = file.name.toLowerCase();
const isCSV = fileNameLower.endsWith('.csv');
const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx');
if (!isCSV && !isExcel) {
opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning');
return;
}
const processData = (data, headers) => {
const normalizedMap = {};
for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key];
const mappedData = data.map((row) => {
const newRow = {};
for (const key in row) {
const cleanKey = normalizeHeader(key);
const newKey = normalizedMap[cleanKey] || cleanKey;
let value = row[key];
if (newKey === 'firstname') value = cleanReceiverName(value);
newRow[newKey] = value;
}
return newRow;
});
const requiredCols = Object.keys(headerMap).filter((k) => k.trim().endsWith('*'));
const missingRequired = requiredCols.filter(
(clientCol) => !headers.includes(normalizeHeader(clientCol))
);
if (missingRequired.length > 0) {
opentoast(`Missing required columns: ${missingRequired.join(', ')}`, 'warning', 3000);
setUsers([]);
return;
}
setUsers(mappedData);
opentoast('File uploaded successfully', 'success', 2000);
opentoast('Press Continue to add as drop customers', 'info', 2500);
};
if (isCSV) {
Papa.parse(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: (results) => {
if (!results.data?.length) {
opentoast('CSV file is empty or has no valid rows.', 'warning');
setUsers([]);
return;
}
const headers = results.meta.fields.map(normalizeHeader);
processData(results.data, headers);
},
error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning')
});
}
if (isExcel) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target.result;
const workbook = XLSX.read(data, { type: 'binary' });
const firstSheet = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheet];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' });
if (!jsonData?.length) {
opentoast('Excel file is empty or invalid.', 'warning');
setUsers([]);
return;
}
const headers = Object.keys(jsonData[0]).map(normalizeHeader);
processData(jsonData, headers);
} catch (err) {
opentoast(`Error reading Excel: ${err.message}`, 'warning');
}
};
reader.readAsBinaryString(file);
}
} catch (err) {
opentoast(`Unexpected error: ${err.message}`, 'warning');
}
};
const removeFileExtension = (n) => n.replace(/\.[^/.]+$/, '');
const onFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
const cleanedName = removeFileExtension(file.name);
setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName));
handleFileDirectUpload(event);
};
// ============================== row editors ==============================
const handleQuantityChange = (customerid, value) => {
setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, quantity: Number(value) || 0 } : c)));
};
const handleCollectionAmtChange = (customerid, value) => {
setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, collectionamt: Number(value) || 0 } : c)));
};
// ============================== createorders ==============================
const buildDeliveryTime = () => {
// Prefer the parsed pickupSlot (already merged date + slot's time).
// Fall back to startdate + selectedtime when present.
if (pickupSlot) {
const parsed = dayjs(pickupSlot, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']);
if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss');
}
if (startdate && selectedtime) {
const parsed = dayjs(`${dayjs(startdate).format('YYYY-MM-DD')} ${selectedtime}`, [
'YYYY-MM-DD hh:mm A',
'YYYY-MM-DD HH:mm:ss'
]);
if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss');
}
return dayjs().format('YYYY-MM-DD HH:mm:ss');
};
const createorders = async () => {
if (!tenantid) {
opentoast('Choose Client', 'warning');
return;
}
if (!pickCust) {
opentoast('Pickup location required', 'warning');
return;
}
if (!pickupSlot) {
opentoast('Select a pickup slot', 'warning');
return;
}
if (!dropCust.length) {
opentoast('Add at least one drop customer', 'warning');
return;
}
const deliverytime = buildDeliveryTime();
const arr = dropCust.map((customer) => ({
applocationid: pickCust.applocationid,
configid: 9,
partnerid: pickCust.partnerid,
partneruserid: +userid,
paymentstatus: 1,
paymenttype: 42,
pickupaddress: pickCust.address || '',
pickupcity: pickCust.city || '',
pickupcontactno: pickCust.contactno || '',
pickupcustomer: pickCust.locationname || '',
pickuplandmark: pickCust.landmark || '',
pickuplat: pickCust.latitude,
pickuplocation: pickCust.suburb || '',
pickuplocationid: pickCust.locationid || 0,
pickuplong: pickCust.longitude,
tenantid: pickCust.tenantid,
customerid: +customer?.customerid,
deliveryaddress: customer.address || '',
deliverycharge: +customer.totalcharge || 0,
deliverycity: customer.city || '',
deliverycontactno: customer.contactno?.toString() || '',
deliverycustomer: customer.firstname || '',
deliveryid: +customer.customerid,
deliverylandmark: customer.landmark || '',
deliverylat: customer.latitude?.toString() || '',
deliverylocation: customer.suburb || '',
deliverylocationid: customer.deliverylocationid || 0,
deliverylong: customer.longitude?.toString() || '',
deliverytime,
deliverytype: 'B',
itemcount: 1,
quantity: customer.quantity,
collectionamt: customer.collectionamt,
kms: customer.distance?.toString() || '0',
locationid: +pickCust.locationid,
moduleid: +pickCust.moduleid,
orderamount: +customer.totalcharge || 0,
ordercharges: 0.0,
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
ordernotes: otherinstructions,
orderstatus: 'created',
ordervalue: +customer.totalcharge || 0,
pickupSlot
}));
setLoading(true);
setBtnLoading(true);
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr);
if (res.data.status) {
opentoast('Orders Created Successfully', 'success', 2000);
if (admintoken) sendnotifications();
navigate('/nearle/orders');
} else {
opentoast(res?.data?.message || 'Order creation failed. Please try again.', 'warning', 3000);
}
} catch (err) {
let toastMessage = 'Something went wrong. Please try again.';
if (err.response) {
toastMessage = err.response.data?.message || `Server error (${err.response.status})`;
} else if (err.request) {
toastMessage = 'Network error. Check your internet connection.';
}
opentoast(toastMessage, 'error', 3000);
} finally {
setLoading(false);
setBtnLoading(false);
}
};
// ============================== derived ==============================
const stepsComplete = {
location: !!appId,
client: !!tenantid,
business: !!locationid,
schedule: !!pickupSlot,
drops: dropCust.length > 0
};
const canSubmit =
stepsComplete.location && stepsComplete.client && stepsComplete.business && stepsComplete.schedule && stepsComplete.drops;
// ============================== preview helpers ==============================
// The right pane shows three exclusive views:
// 1. dropCust.length > 0 → "Drop List" with calculated charges + remove
// 2. users.length > 0 → raw "File Preview" of parsed rows, awaiting Continue
// 3. else → empty state
const previewMode = dropCust.length > 0 ? 'drops' : users.length > 0 ? 'preview' : 'empty';
// ============================== render ==============================
return (
<>
{loading && <Loader />}
<Backdrop sx={{ color: '#fff', zIndex: (t) => t.zIndex.drawer + 1 }} open={btnLoading}>
<CircularLoader color="inherit" />
</Backdrop>
{/* Outer viewport-locked shell — page never scrolls; panes scroll internally. */}
<Box
className="orders-workspace-bg"
sx={{
height: { xs: 'auto', md: 'calc(100vh - 64px)' },
minHeight: { xs: 'calc(100vh - 64px)', md: 0 },
display: 'flex',
flexDirection: 'column',
overflow: { xs: 'visible', md: 'hidden' },
p: { xs: 0.75, sm: 1.25 },
gap: 1
}}
>
{/* Thin title bar */}
<Stack
direction="row"
alignItems="center"
gap={1}
flexWrap="wrap"
sx={{ flexShrink: 0, px: 0.5 }}
>
<Typography sx={{ fontWeight: 700, fontSize: { xs: 16, sm: 17 }, color: '#1e293b', lineHeight: 1.2 }}>
Create Multiple Orders
</Typography>
<Chip
size="small"
label={`${dropCust.length} drop${dropCust.length === 1 ? '' : 's'}`}
sx={{
height: 20,
fontSize: 10.5,
fontWeight: 700,
bgcolor: dropCust.length ? 'rgba(24,144,255,0.10)' : '#f1f5f9',
color: dropCust.length ? '#1890ff' : '#94a3b8',
border: `1px solid ${dropCust.length ? 'rgba(24,144,255,0.25)' : '#e2e8f0'}`
}}
/>
<Box sx={{ flex: 1 }} />
<Typography sx={{ fontSize: 11, color: '#94a3b8', display: { xs: 'none', md: 'block' } }}>
Bulk-create deliveries from CSV/Excel or saved customers.
</Typography>
</Stack>
{/* ============================== 50 / 50 workspace ============================== */}
<Grid
container
spacing={1.25}
sx={{ flex: 1, minHeight: 0, overflow: { xs: 'visible', md: 'hidden' } }}
>
{/* ============================== LEFT 50% : Input fields ============================== */}
<Grid
item
xs={12}
md={6}
sx={{
height: { xs: 'auto', md: '100%' },
minHeight: 0,
display: 'flex',
flexDirection: 'column',
gap: { xs: 1.25, md: 2.25 },
overflowY: { xs: 'visible', md: 'auto' },
pr: { xs: 0, md: 0.75 }
}}
>
{/* Card: Setup (Location / Client / Business) */}
<Card className="orders-card" sx={{ p: 2, flexShrink: 0 }}>
<Box className="section-title-bar" sx={{ mb: 1.5 }}>
<Typography sx={{ fontWeight: 700, color: '#1e293b', fontSize: 13.5, letterSpacing: '-0.01em' }}>
Setup
</Typography>
</Box>
<Grid container spacing={1.75}>
<Grid item xs={12} sm={4}>
<Autocomplete
fullWidth
size="small"
ref={locationRef}
className="header-compact-input"
options={locations || []}
getOptionLabel={(option) => `${option.locationname}`}
onChange={(event, value, reason) => {
if (reason === 'clear') setAppId(0);
else if (value) setAppId(value.applocationid);
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Choose Location"
label="Location"
InputLabelProps={{ shrink: true }}
className="header-compact-tf"
InputProps={{
...params.InputProps,
startAdornment: (
<>
<FaLocationDot style={{ color: '#94a3b8', fontSize: 12, marginRight: 6, flexShrink: 0 }} />
{params.InputProps.startAdornment}
</>
)
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={4}>
<Autocomplete
fullWidth
size="small"
className="header-compact-input"
options={tenantlist || []}
value={tenantValue}
getOptionLabel={(option) => option?.tenantname || ''}
isOptionEqualToValue={(option, value) => option.tenantid === value.tenantid}
onOpen={(event) => {
if (!appId) {
event.preventDefault();
OpenToast('Please select Location first!', 'warning', 3000);
setTimeout(() => locationRef.current?.focus(), 0);
}
}}
onChange={(e, val, reason) => {
if (reason === 'clear') {
setTenantid(0);
setTenantValue(null);
} else if (val) {
setTenantid(val.tenantid);
setTenantValue(val);
fetchTenantPricing(val.tenantid);
gettenantlocations(val.tenantid);
}
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Choose Client"
label="Client"
inputRef={tenantRef}
InputLabelProps={{ shrink: true }}
className="header-compact-tf"
InputProps={{
...params.InputProps,
startAdornment: (
<>
<FaUser style={{ color: '#94a3b8', fontSize: 11, marginRight: 6, flexShrink: 0 }} />
{params.InputProps.startAdornment}
</>
)
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={4}>
{tenantLocations.length === 1 ? (
<TextField
variant="outlined"
fullWidth
size="small"
label="Business Location"
value={tenantLocations[0].locationname}
InputLabelProps={{ shrink: true }}
className="header-compact-tf"
InputProps={{
style: { color: theme.palette.primary.main },
startAdornment: (
<MyLocationIcon style={{ color: '#94a3b8', fontSize: 14, marginRight: 6, flexShrink: 0 }} />
)
}}
/>
) : (
<Autocomplete
fullWidth
size="small"
className="header-compact-input"
value={locationValue}
options={tenantLocations || []}
getOptionLabel={(option) =>
option?.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
}
onOpen={(event) => {
if (!appId && !tenantid) {
event.preventDefault();
OpenToast('Please select Location and Client first!', 'warning', 3000);
setTimeout(() => locationRef.current?.focus(), 0);
} else if (!tenantid) {
event.preventDefault();
OpenToast('Please select Client first!', 'warning', 3000);
setTimeout(() => tenantRef.current?.focus(), 0);
}
}}
onChange={(event, value, reason) => {
if (reason === 'clear' || !value) {
setLocationid(0);
setLocationValue(null);
setPickCust(null);
setPickupSlotsList(null);
} else {
setLocationid(value.locationid || 0);
setLocationValue(value);
setPickCust(value);
setPickupSlotsList(value?.slots);
}
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Business Location"
label="Business Location"
InputLabelProps={{ shrink: true }}
className="header-compact-tf"
InputProps={{
...params.InputProps,
startAdornment: (
<>
<MyLocationIcon
style={{ color: '#94a3b8', fontSize: 14, marginRight: 6, flexShrink: 0 }}
/>
{params.InputProps.startAdornment}
</>
)
}}
/>
)}
/>
)}
</Grid>
</Grid>
</Card>
{/* Card: Schedule + Pickup display */}
<Card className="orders-card" sx={{ p: 2, flexShrink: 0 }}>
<Box className="section-title-bar section-title-bar--accent" sx={{ mb: 1.5 }}>
<Typography sx={{ fontWeight: 700, color: '#1e293b', fontSize: 13.5, letterSpacing: '-0.01em' }}>
Schedule &amp; Pickup
</Typography>
</Box>
<Grid container spacing={1.75}>
<Grid item xs={12} sm={6}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Pickup Date"
format="DD-MM-YYYY"
value={startdate ? dayjs(startdate) : null}
onChange={(e) => {
if (!e || !dayjs(e).isValid()) {
setStartdate(dayjs().format('MM-DD-YYYY'));
return;
}
const diffDays = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
if (diffDays <= 0) {
setStartdate(dayjs(e).format('MM-DD-YYYY'));
setSelectedtime('');
setPickupSlot(null);
} else {
opentoast('Choose an upcoming date', 'warning');
setStartdate(dayjs().format('MM-DD-YYYY'));
}
}}
disablePast
slotProps={{
textField: {
size: 'small',
fullWidth: true,
InputLabelProps: { shrink: true },
InputProps: {
startAdornment: (
<InputAdornment position="start">
<CalendarOutlined style={{ color: '#94a3b8', fontSize: '13px' }} />
</InputAdornment>
)
},
sx: {
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
height: '36px',
paddingLeft: '10px'
}
}
}
}}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12} sm={6}>
<Autocomplete
size="small"
fullWidth
options={pickupSlotsList || []}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } }}
onChange={(e, newValue, reason) => {
if (reason === 'clear' || !newValue) {
setSelectedtime(null);
setPickupSlot(null);
return;
}
if (!newValue.time) {
OpenToast('This slot has no time configured.', 'warning', 3000);
return;
}
const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A');
setSelectedtime(formattedTime);
const finalDateTime = dayjs(
`${startdate} ${formattedTime}`,
'MM-DD-YYYY hh:mm A'
).format('YYYY-MM-DD hh:mm A');
setPickupSlot(finalDateTime);
}}
getOptionLabel={(option) =>
option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : ''
}
renderInput={(params) => (
<TextField
{...params}
label="Pickup Slot"
placeholder="Select Pickup Slot"
fullWidth
InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } }}
InputProps={{
...params.InputProps,
startAdornment: (
<>
<ClockCircleOutlined
style={{
color: '#94a3b8',
fontSize: '13px',
marginRight: '6px',
marginLeft: '2px',
flexShrink: 0
}}
/>
{params.InputProps.startAdornment}
</>
)
}}
/>
)}
/>
</Grid>
{/* Pickup display (compact, single row) */}
<Grid item xs={12}>
{pickCust ? (
<Stack
direction="row"
spacing={1.25}
alignItems="center"
sx={{
border: '1px solid #eef2f6',
borderLeft: '3px solid #1890ff',
borderRadius: '10px',
px: 1.25,
py: 0.75,
bgcolor: '#fbfcff'
}}
>
<Box
sx={{
width: 28,
height: 28,
borderRadius: '8px',
background: 'linear-gradient(135deg,#1890ff,#096dd9)',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
fontSize: 12,
boxShadow: '0 4px 10px rgba(24,144,255,0.30)'
}}
>
<FaLocationDot />
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: '#1e293b', lineHeight: 1.2 }} noWrap>
{pickCust.locationname || '—'}
</Typography>
<Typography sx={{ fontSize: 11, color: '#64748b', lineHeight: 1.3 }} noWrap>
{pickCust.address || '—'}
</Typography>
</Box>
</Stack>
) : (
<Typography sx={{ color: '#94a3b8', fontSize: 12 }}>
Pickup auto-fills once a Business Location is selected.
</Typography>
)}
</Grid>
</Grid>
</Card>
{/* Card: Notes */}
<Card className="orders-card delivery-prefs-card" sx={{ p: 2, flexShrink: 0 }}>
<Box className="delivery-prefs-header" sx={{ mb: 1 }}>
<Typography className="delivery-prefs-title">Order Notes</Typography>
<Typography className="delivery-prefs-sub">Applied to every order</Typography>
</Box>
<Box className="delivery-prefs-row">
<Box className="delivery-prefs-field" sx={{ width: '100%' }}>
<label className="delivery-prefs-label" htmlFor="bulk-dispatch-notes-input">
<FileTextOutlined style={{ fontSize: 11, color: '#65387a' }} />
Special Dispatch Notes
</label>
<TextField
id="bulk-dispatch-notes-input"
size="small"
fullWidth
placeholder="Gate codes, call instructions, special cargo care…"
value={otherinstructions}
onChange={(e) => setOtherinstructions(e.target.value)}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '10px',
padding: '0 10px',
alignItems: 'center',
fontSize: '12px',
background: '#ffffff',
height: '32px'
},
'& .MuiOutlinedInput-input': {
padding: '0 !important',
fontSize: '12px !important',
lineHeight: '32px'
}
}}
/>
</Box>
</Box>
</Card>
{/* Card: Summary + Submit */}
<Card className="orders-card pricing-summary-card" sx={{ flexShrink: 0, p: '16px 18px !important' }}>
<Box className="pricing-header" sx={{ mb: 1.5, pb: 1 }}>
<Typography className="pricing-title" sx={{ fontSize: '13px !important' }}>
Bulk Summary
</Typography>
<Typography className="pricing-subtitle" sx={{ fontSize: '10px !important' }}>
Live totals
</Typography>
</Box>
{(() => {
const metric = ({ icon: Icon, label, value, accent, active }) => (
<Box
sx={{
bgcolor: '#fff',
border: '1px solid #eef2f6',
borderRadius: '10px',
px: 1,
py: 0.75,
display: 'flex',
alignItems: 'center',
gap: 0.85,
minWidth: 0,
transition: 'border-color 0.2s, box-shadow 0.2s',
...(active && {
borderColor: `${accent}55`,
boxShadow: `0 2px 6px -2px ${accent}33`
})
}}
>
<Box
sx={{
width: 26,
height: 26,
borderRadius: '8px',
bgcolor: `${accent}18`,
color: accent,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
flexShrink: 0
}}
>
<Icon />
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography sx={{ fontSize: 9.5, fontWeight: 600, color: '#94a3b8', letterSpacing: 0.4, textTransform: 'uppercase', lineHeight: 1 }}>
{label}
</Typography>
<Typography
sx={{
fontSize: 13.5,
fontWeight: 800,
color: active ? '#1e293b' : '#94a3b8',
lineHeight: 1.2,
mt: 0.25
}}
noWrap
>
{value}
</Typography>
</Box>
</Box>
);
return (
<Grid container spacing={1.25}>
<Grid item xs={6}>
{metric({
icon: FaRoute,
label: 'Distance',
value: totaldist ? `${totaldist} km` : '—',
accent: '#1890ff',
active: !!totaldist
})}
</Grid>
<Grid item xs={6}>
{metric({
icon: FaBoxes,
label: 'Quantity',
value: totalQty || 0,
accent: '#16a34a',
active: !!totalQty
})}
</Grid>
<Grid item xs={6}>
{metric({
icon: FaMoneyBillWave,
label: 'Cash Collect',
value: `${Number(totalCash).toFixed(2)}`,
accent: '#d97706',
active: !!totalCash
})}
</Grid>
<Grid item xs={6}>
{metric({
icon: FaTruck,
label: 'Deliveries',
value: dropCust.length,
accent: '#65387a',
active: dropCust.length > 0
})}
</Grid>
</Grid>
);
})()}
{dropCust.length > 0 && (
<div className="total-charge-badge" style={{ marginTop: 14, padding: '10px 14px' }}>
<div className="total-charge-left">
<FaReceipt className="total-charge-icon" />
<div className="total-charge-label">Total Charge</div>
</div>
<div className="total-charge-val" style={{ fontSize: 17 }}>
{Number(totalAmt).toFixed(2)}
</div>
</div>
)}
<Box sx={{ mt: 1.75 }}>
<Button
fullWidth
className="gradient-btn-create"
disabled={!canSubmit || btnLoading}
startIcon={!btnLoading && <FaPaperPlane style={{ fontSize: 11 }} />}
onClick={createorders}
sx={{ minHeight: '36px !important' }}
>
{btnLoading ? (
<CircularProgress color="inherit" size={16} thickness={5} />
) : (
`Dispatch ${dropCust.length || ''} ${dropCust.length === 1 ? 'Order' : 'Orders'}`.trim()
)}
</Button>
</Box>
</Card>
</Grid>
{/* ============================== RIGHT 50% : File / Drop Preview ============================== */}
<Grid
item
xs={12}
md={6}
sx={{ height: { xs: 'auto', md: '100%' }, minHeight: 0, display: 'flex', flexDirection: 'column' }}
>
<Card
className="orders-card"
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
p: 1.25,
maxHeight: { xs: 'none', md: 685 }
}}
>
{/* Preview header (sticky inside card) */}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={1}
sx={{ mb: 0.75, flexShrink: 0 }}
>
<Box className="section-title-bar" sx={{ mb: 0 }}>
<Typography sx={{ fontWeight: 700, color: '#1e293b', fontSize: 13.5, letterSpacing: '-0.01em' }}>
{previewMode === 'drops'
? `Drop List`
: previewMode === 'preview'
? `File Preview`
: `Preview`}
</Typography>
<Chip
size="small"
label={previewMode === 'drops' ? dropCust.length : previewMode === 'preview' ? users.length : 0}
sx={{
height: 20,
fontSize: 10.5,
fontWeight: 700,
ml: 1,
bgcolor: 'rgba(168,85,247,0.10)',
color: '#65387a',
border: '1px solid rgba(168,85,247,0.25)'
}}
/>
</Box>
{previewMode === 'drops' && (
<Chip
size="small"
label={`${Number(totalAmt).toFixed(2)} total`}
sx={{
height: 22,
fontWeight: 700,
fontSize: 11,
bgcolor: 'rgba(101,56,122,0.10)',
color: '#65387a',
border: '1px solid rgba(101,56,122,0.25)'
}}
/>
)}
{previewMode === 'preview' && (
<Chip
size="small"
label="Awaiting Continue"
icon={<ExclamationCircleOutlined style={{ fontSize: 11 }} />}
sx={{
height: 22,
fontWeight: 700,
fontSize: 11,
bgcolor: 'rgba(245,158,11,0.12)',
color: '#b45309',
border: '1px solid rgba(245,158,11,0.30)',
'& .MuiChip-icon': { color: '#b45309' }
}}
/>
)}
{previewMode !== 'empty' && (() => {
const prereqOk = appId && tenantid && locationid;
const handleHeaderPick = (val) => {
if (!prereqOk) {
OpenToast('Please select Location, Client, and Business Location first.', 'warning', 3000);
return;
}
setUploadType(val);
if (val === 0) {
document.getElementById('upload-file')?.click();
} else if (val === 1) {
setIsCustomerOpen(true);
setSearchCustList('');
}
};
return (
<Stack direction="row" gap={0.75} alignItems="center">
<Button
size="small"
variant="outlined"
onClick={() => handleHeaderPick(0)}
startIcon={<MdOutlineCloudUpload />}
sx={{
height: 24,
py: 0,
textTransform: 'none',
borderRadius: '6px',
fontSize: 10.5,
fontWeight: 600,
borderColor: '#cbd5e1',
color: '#475569',
'&:hover': { borderColor: '#1890ff', color: '#1890ff' }
}}
>
Excel / CSV
</Button>
<Button
size="small"
variant="outlined"
onClick={() => handleHeaderPick(1)}
startIcon={<FaUsers />}
sx={{
height: 24,
py: 0,
textTransform: 'none',
borderRadius: '6px',
fontSize: 10.5,
fontWeight: 600,
borderColor: '#cbd5e1',
color: '#475569',
'&:hover': { borderColor: '#65387a', color: '#65387a' }
}}
>
Selection
</Button>
</Stack>
);
})()}
</Stack>
{fileName && (
<Stack
direction="row"
gap={0.75}
alignItems="center"
sx={{ mb: 0.75, color: '#64748b', flexShrink: 0 }}
>
<FileAddOutlined style={{ fontSize: 12 }} />
<Typography sx={{ fontSize: 11.5 }} noWrap>
{fileName}
</Typography>
</Stack>
)}
{previewMode === 'preview' && users.length >= 1 && (
<Box
sx={{
mb: 1,
p: 1.25,
bgcolor: 'rgba(245,158,11,0.06)',
border: '1.5px solid rgba(245,158,11,0.25)',
borderRadius: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
flexShrink: 0,
animation: 'blink 1.5s infinite',
'@keyframes blink': {
'0%': { borderColor: 'rgba(245,158,11,0.25)' },
'50%': { borderColor: 'rgba(245,158,11,0.6)' },
'100%': { borderColor: 'rgba(245,158,11,0.25)' }
}
}}
>
<Box sx={{ textAlign: 'left' }}>
<Typography sx={{ fontWeight: 700, fontSize: 12, color: '#b45309', lineHeight: 1.2 }}>
Process &amp; Calculate Distances
</Typography>
<Typography sx={{ color: '#b45309', opacity: 0.8, fontSize: 10.5, lineHeight: 1.25 }}>
Click continue to import spreadsheet rows and calculate drop charges.
</Typography>
</Box>
<Button
variant="contained"
size="small"
color="warning"
onClick={() => users.forEach((customer) => handleCheckboxChange1(customer))}
sx={{
textTransform: 'none',
borderRadius: '8px',
fontWeight: 700,
fontSize: 11,
height: 28,
px: 1.5,
bgcolor: '#d97706',
boxShadow: 'none',
'&:hover': { bgcolor: '#b45309', boxShadow: 'none' }
}}
>
Continue
</Button>
</Box>
)}
{/* Scrollable preview body */}
<Box sx={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
{previewMode === 'drops' && isMobile && (
<MobileCardList sx={{ p: 0 }}>
{dropCust.map((customer, index) => (
<MobileCard
key={customer.customerid || customer.firstname || index}
accent="#65387a"
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#1e293b', lineHeight: 1.2 }}>
{index + 1}. {customer.firstname}
</Typography>
<Typography sx={{ fontSize: 12, color: '#64748b', mt: 0.25 }}>
{customer.address}
</Typography>
</Box>
<Tooltip title="Remove">
<IconButton
size="small"
onClick={() => handleCheckboxChange1(customer)}
sx={{ color: '#ef4444', p: 0.5, flexShrink: 0 }}
>
<CloseOutlined style={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Qty">
{uploadType === 0 ? (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: '#0f172a' }}>
{customer.quantity ?? '—'}
</Typography>
) : (
<TextField
size="small"
type="number"
value={customer.quantity || ''}
onChange={(e) => handleQuantityChange(customer.customerid, e.target.value)}
inputProps={{ min: 0 }}
fullWidth
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }}
/>
)}
</MobileField>
<MobileField label="Cash">
{uploadType === 0 ? (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: '#0f172a' }}>
{`${Number(customer.collectionamt || 0).toFixed(2)}`}
</Typography>
) : (
<TextField
size="small"
type="number"
value={customer.collectionamt ? customer.collectionamt : ''}
placeholder="0"
onChange={(e) => {
const v = Number(e.target.value);
handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0);
}}
inputProps={{ min: 0 }}
InputProps={{
startAdornment: <InputAdornment position="start"></InputAdornment>
}}
fullWidth
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }}
/>
)}
</MobileField>
<MobileField label="Km" value={customer.distance} />
<MobileField label="Charge" align="right">
<Typography sx={{ fontSize: 13, fontWeight: 700, color: '#1e293b' }}>
{Number(customer?.totalcharge || 0).toFixed(2)}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
<MobileCard accent="#65387a" sx={{ bgcolor: '#fafbfc' }}>
<MobileFieldGrid columns={2}>
<MobileField label="Total Qty" value={totalQty} />
<MobileField label="Total Cash" value={`${Number(totalCash).toFixed(2)}`} />
<MobileField label="Total Km" value={totaldist} />
<MobileField label="Total Charge" align="right">
<Typography sx={{ fontSize: 14, fontWeight: 800, color: '#65387a' }}>
{Number(totalAmt).toFixed(2)}
</Typography>
</MobileField>
</MobileFieldGrid>
</MobileCard>
</MobileCardList>
)}
{previewMode === 'drops' && !isMobile && (
<TableContainer
component={Paper}
sx={{ borderRadius: '10px', border: '1px solid #eef2f6', boxShadow: 'none' }}
>
<Table size="small" stickyHeader>
<TableHead sx={{ bgcolor: '#f8fafc' }}>
<TableRow>
<TableCell sx={cellHeaderSx}>#</TableCell>
<TableCell sx={cellHeaderSx}>Customer</TableCell>
<TableCell sx={cellHeaderSx}>Address</TableCell>
<TableCell sx={cellHeaderSx} align="center">
Qty
</TableCell>
<TableCell sx={cellHeaderSx} align="center">
Cash
</TableCell>
<TableCell sx={cellHeaderSx}>Km</TableCell>
<TableCell sx={cellHeaderSx} align="right">
Charge
</TableCell>
<TableCell sx={cellHeaderSx} align="center">
{' '}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{dropCust.map((customer, index) => (
<TableRow key={customer.customerid || customer.firstname || index} hover>
<TableCell sx={cellBodySx}>{index + 1}</TableCell>
<TableCell sx={{ ...cellBodySx, whiteSpace: 'nowrap', color: '#1e293b', fontWeight: 600 }}>
{customer.firstname}
</TableCell>
<TableCell sx={{ ...cellBodySx, color: '#64748b', maxWidth: 220 }}>
<Tooltip title={customer.address || ''}>
<span
style={{
display: 'inline-block',
maxWidth: 220,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
verticalAlign: 'middle'
}}
>
{customer.address}
</span>
</Tooltip>
</TableCell>
<TableCell align="center" sx={cellBodySx}>
{uploadType === 0 ? (
customer.quantity
) : (
<TextField
size="small"
type="number"
value={customer.quantity || ''}
onChange={(e) => handleQuantityChange(customer.customerid, e.target.value)}
inputProps={{ min: 0 }}
sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }}
/>
)}
</TableCell>
<TableCell align="center" sx={cellBodySx}>
{uploadType === 0 ? (
`${Number(customer.collectionamt || 0).toFixed(2)}`
) : (
<TextField
size="small"
type="number"
value={customer.collectionamt ? customer.collectionamt : ''}
placeholder="0"
onChange={(e) => {
const v = Number(e.target.value);
handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0);
}}
inputProps={{ min: 0 }}
InputProps={{
startAdornment: <InputAdornment position="start"></InputAdornment>
}}
sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }}
/>
)}
</TableCell>
<TableCell sx={cellBodySx}>{customer.distance}</TableCell>
<TableCell align="right" sx={{ ...cellBodySx, fontWeight: 700, color: '#1e293b' }}>
{Number(customer?.totalcharge || 0).toFixed(2)}
</TableCell>
<TableCell align="center" sx={cellBodySx}>
<Tooltip title="Remove">
<IconButton
size="small"
onClick={() => handleCheckboxChange1(customer)}
sx={{ color: '#ef4444', p: 0.5 }}
>
<CloseOutlined style={{ fontSize: 13 }} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
<TableRow sx={{ bgcolor: '#fafbfc' }}>
<TableCell sx={{ ...cellBodySx, fontWeight: 800 }}>Total</TableCell>
<TableCell colSpan={2} sx={cellBodySx} />
<TableCell align="center" sx={{ ...cellBodySx, fontWeight: 800 }}>
{totalQty}
</TableCell>
<TableCell align="center" sx={{ ...cellBodySx, fontWeight: 800 }}>
{Number(totalCash).toFixed(2)}
</TableCell>
<TableCell sx={{ ...cellBodySx, fontWeight: 800 }}>{totaldist}</TableCell>
<TableCell align="right" sx={{ ...cellBodySx, fontWeight: 800, color: '#65387a' }}>
{Number(totalAmt).toFixed(2)}
</TableCell>
<TableCell sx={cellBodySx} />
</TableRow>
</TableBody>
</Table>
</TableContainer>
)}
{previewMode === 'preview' && isMobile && (
<MobileCardList sx={{ p: 0 }}>
{users.map((u, i) => (
<MobileCard
key={i}
accent="#d97706"
header={
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 14, fontWeight: 700, color: '#1e293b', lineHeight: 1.2 }}>
{i + 1}. {u.firstname || '—'}
</Typography>
<Typography sx={{ fontSize: 12, color: '#64748b', mt: 0.25 }}>
{u.address || '—'}
</Typography>
</Box>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Contact" value={u.contactno || '—'} />
<MobileField label="Qty" value={u.quantity ?? '—'} />
<MobileField
label="Cash"
value={u.collectionamt != null ? `${Number(u.collectionamt).toFixed(2)}` : '—'}
/>
</MobileFieldGrid>
</MobileCard>
))}
</MobileCardList>
)}
{previewMode === 'preview' && !isMobile && (
<TableContainer
component={Paper}
sx={{ borderRadius: '10px', border: '1px solid #eef2f6', boxShadow: 'none' }}
>
<Table size="small" stickyHeader>
<TableHead sx={{ bgcolor: '#f8fafc' }}>
<TableRow>
<TableCell sx={cellHeaderSx}>#</TableCell>
<TableCell sx={cellHeaderSx}>Name</TableCell>
<TableCell sx={cellHeaderSx}>Contact</TableCell>
<TableCell sx={cellHeaderSx}>Address</TableCell>
<TableCell sx={cellHeaderSx} align="center">
Qty
</TableCell>
<TableCell sx={cellHeaderSx} align="center">
Cash
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((u, i) => (
<TableRow key={i} hover>
<TableCell sx={cellBodySx}>{i + 1}</TableCell>
<TableCell sx={{ ...cellBodySx, whiteSpace: 'nowrap', color: '#1e293b', fontWeight: 600 }}>
{u.firstname || '—'}
</TableCell>
<TableCell sx={cellBodySx}>{u.contactno || '—'}</TableCell>
<TableCell sx={{ ...cellBodySx, color: '#64748b', maxWidth: 240 }}>
<Tooltip title={u.address || ''}>
<span
style={{
display: 'inline-block',
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
verticalAlign: 'middle'
}}
>
{u.address || '—'}
</span>
</Tooltip>
</TableCell>
<TableCell align="center" sx={cellBodySx}>
{u.quantity ?? '—'}
</TableCell>
<TableCell align="center" sx={cellBodySx}>
{u.collectionamt != null ? `${Number(u.collectionamt).toFixed(2)}` : '—'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{previewMode === 'empty' && (() => {
const prereqOk = appId && tenantid && locationid;
const handleEmptyPick = (val) => {
if (!prereqOk) {
OpenToast('Please select Location, Client, and Business Location first.', 'warning', 3000);
return;
}
setUploadType(val);
setDropCust([]);
setUsers([]);
setFileName('');
if (val === 0) {
document.getElementById('upload-file')?.click();
} else if (val === 1) {
setIsCustomerOpen(true);
setSearchCustList('');
}
};
const tile = ({ value, icon: Icon, title, sub, accent }) => {
return (
<Box
role="button"
tabIndex={0}
onClick={() => handleEmptyPick(value)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleEmptyPick(value);
}
}}
sx={{
flex: 1,
minWidth: 0,
display: 'flex',
alignItems: 'center',
textAlign: 'left',
gap: 1.25,
px: 1.5,
py: 1.25,
borderRadius: '10px',
border: `1.5px solid #eef2f6`,
bgcolor: '#fff',
opacity: prereqOk ? 1 : 0.6,
cursor: prereqOk ? 'pointer' : 'not-allowed',
transition: 'all 0.18s ease',
'&:hover': prereqOk
? {
borderColor: accent,
boxShadow: `0 4px 12px -4px ${accent}40`,
transform: 'translateY(-1px)'
}
: undefined
}}
>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
background: `${accent}15`,
color: accent,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 15,
flexShrink: 0
}}
>
<Icon />
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography sx={{ fontWeight: 700, fontSize: 13, color: '#1e293b', lineHeight: 1.15 }}>
{title}
</Typography>
<Typography sx={{ fontSize: 11, color: '#94a3b8', lineHeight: 1.25 }} noWrap>
{sub}
</Typography>
</Box>
</Box>
);
};
return (
<Stack
alignItems="center"
justifyContent="center"
spacing={1.5}
sx={{
height: '100%',
minHeight: 240,
bgcolor: 'rgba(24,144,255,0.03)',
border: '1px dashed rgba(24,144,255,0.25)',
borderRadius: '12px',
p: 3,
textAlign: 'center'
}}
>
<Box
sx={{
width: 56,
height: 56,
borderRadius: '14px',
bgcolor: 'rgba(24,144,255,0.08)',
color: '#1890ff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 28
}}
>
<InboxOutlined />
</Box>
<Typography sx={{ fontWeight: 700, color: '#1e293b', fontSize: 14 }}>
Choose a Drop Source to begin
</Typography>
<Typography sx={{ color: '#64748b', fontSize: 12, maxWidth: 360, mb: 1 }}>
Select a source from below or the left panel to import or pick your delivery customers.
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} gap={1.5} sx={{ width: '100%', maxWidth: 480, mt: 1 }}>
{tile({
value: 0,
icon: MdOutlineCloudUpload,
title: 'Excel / CSV',
sub: 'Bulk upload a sheet',
accent: '#1890ff'
})}
{tile({
value: 1,
icon: FaUsers,
title: 'Selection',
sub: 'Pick saved customers',
accent: '#65387a'
})}
</Stack>
</Stack>
);
})()}
</Box>
</Card>
</Grid>
</Grid>
</Box>
<input
accept=".csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
id="upload-file"
type="file"
hidden
onChange={onFileChange}
/>
{/* ============================== Saved customers dialog ============================== */}
<Dialog
open={isCustomerOpen}
onClose={() => setIsCustomerOpen(false)}
fullWidth
fullScreen={isMobile}
sx={{
'& .MuiDialog-paper': {
borderRadius: { xs: 0, sm: '16px' },
overflow: 'hidden'
}
}}
>
<DialogTitle sx={{ bgcolor: theme.palette.primary.main, color: 'white', py: 2.5 }}>
<Stack spacing={1.5}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'white' }}>
{`Select Drop Customers (${dropCust.length || 0})`}
</Typography>
<FormControl sx={{ width: '100%' }}>
<OutlinedInput
autoFocus
fullWidth
id="input-search-header"
placeholder="Search by name, contact, or address…"
value={searchCustList}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{
bgcolor: 'white',
borderRadius: '10px',
'& .MuiOutlinedInput-input': { p: '10px 14px' }
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small', color: '#64748b' }} />
</InputAdornment>
}
endAdornment={
<IconButton
sx={{ visibility: searchCustList ? 'visible' : 'hidden', p: 0.5 }}
onClick={() => setSearchCustList('')}
>
<ClearIcon style={{ fontSize: '18px' }} />
</IconButton>
}
autoComplete="off"
/>
</FormControl>
</Stack>
</DialogTitle>
<Divider />
<DialogContent sx={{ p: 2.5, bgcolor: '#fafbfc', minHeight: 400, maxHeight: { xs: 'none', sm: 600 } }}>
{customerlist?.length === 0 ? (
<Stack alignItems="center" justifyContent="center" sx={{ minHeight: 300 }}>
<Empty description="No saved customers found for this client" />
</Stack>
) : (
<Stack spacing={0}>
{customerlist?.map((customer, index) => {
const checked = dropCust.some((c) => c.customerid === customer.customerid);
return (
<FormGroup key={customer.customerid || index}>
<FormControlLabel
sx={{
m: 0,
py: 1,
px: 1.25,
borderRadius: '10px',
'&:hover': { bgcolor: 'rgba(24,144,255,0.04)' }
}}
control={
<Checkbox
checked={checked}
onChange={(event) => handleCheckboxChange(event, customer)}
/>
}
label={
<Box sx={{ width: '100%' }}>
<Typography sx={{ fontWeight: 600, color: '#1e293b', fontSize: 13.5 }}>
{customer.firstname} ({customer.contactno})
</Typography>
<Typography sx={{ color: '#64748b', fontSize: 12, mt: 0.25 }}>
{customer.address}
</Typography>
</Box>
}
/>
</FormGroup>
);
})}
</Stack>
)}
</DialogContent>
<Divider />
<DialogActions sx={{ p: 2, bgcolor: '#fafbfc' }}>
<Button
variant="outlined"
color={dropCust.length === 0 ? 'error' : 'primary'}
onClick={() => setIsCustomerOpen(false)}
sx={{
borderRadius: '10px',
textTransform: 'none',
fontWeight: 600,
px: 3
}}
>
{dropCust.length === 0 ? 'Close' : 'Continue'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default MultipleOrders;