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 && } t.zIndex.drawer + 1 }} open={btnLoading}> {/* Outer viewport-locked shell — page never scrolls; panes scroll internally. */} {/* Thin title bar */} Create Multiple Orders Bulk-create deliveries from CSV/Excel or saved customers. {/* ============================== 50 / 50 workspace ============================== */} {/* ============================== LEFT 50% : Input fields ============================== */} {/* Card: Setup (Location / Client / Business) */} Setup `${option.locationname}`} onChange={(event, value, reason) => { if (reason === 'clear') setAppId(0); else if (value) setAppId(value.applocationid); }} renderInput={(params) => ( {params.InputProps.startAdornment} > ) }} /> )} /> 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) => ( {params.InputProps.startAdornment} > ) }} /> )} /> {tenantLocations.length === 1 ? ( ) }} /> ) : ( 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) => ( {params.InputProps.startAdornment} > ) }} /> )} /> )} {/* Card: Schedule + Pickup display */} Schedule & Pickup { 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: ( ) }, sx: { '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', paddingLeft: '10px' } } } }} /> { 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) => ( {params.InputProps.startAdornment} > ) }} /> )} /> {/* Pickup display (compact, single row) */} {pickCust ? ( {pickCust.locationname || '—'} {pickCust.address || '—'} ) : ( Pickup auto-fills once a Business Location is selected. )} {/* Card: Notes */} Order Notes Applied to every order Special Dispatch Notes 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' } }} /> {/* Card: Summary + Submit */} Bulk Summary Live totals {(() => { const metric = ({ icon: Icon, label, value, accent, active }) => ( {label} {value} ); return ( {metric({ icon: FaRoute, label: 'Distance', value: totaldist ? `${totaldist} km` : '—', accent: '#1890ff', active: !!totaldist })} {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#16a34a', active: !!totalQty })} {metric({ icon: FaMoneyBillWave, label: 'Cash Collect', value: `₹${Number(totalCash).toFixed(2)}`, accent: '#d97706', active: !!totalCash })} {metric({ icon: FaTruck, label: 'Deliveries', value: dropCust.length, accent: '#65387a', active: dropCust.length > 0 })} ); })()} {dropCust.length > 0 && ( Total Charge ₹{Number(totalAmt).toFixed(2)} )} } onClick={createorders} sx={{ minHeight: '36px !important' }} > {btnLoading ? ( ) : ( `Dispatch ${dropCust.length || ''} ${dropCust.length === 1 ? 'Order' : 'Orders'}`.trim() )} {/* ============================== RIGHT 50% : File / Drop Preview ============================== */} {/* Preview header (sticky inside card) */} {previewMode === 'drops' ? `Drop List` : previewMode === 'preview' ? `File Preview` : `Preview`} {previewMode === 'drops' && ( )} {previewMode === 'preview' && ( } 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 ( handleHeaderPick(0)} startIcon={} 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 handleHeaderPick(1)} startIcon={} sx={{ height: 24, py: 0, textTransform: 'none', borderRadius: '6px', fontSize: 10.5, fontWeight: 600, borderColor: '#cbd5e1', color: '#475569', '&:hover': { borderColor: '#65387a', color: '#65387a' } }} > Selection ); })()} {fileName && ( {fileName} )} {previewMode === 'preview' && users.length >= 1 && ( Process & Calculate Distances Click continue to import spreadsheet rows and calculate drop charges. 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 )} {/* Scrollable preview body */} {previewMode === 'drops' && isMobile && ( {dropCust.map((customer, index) => ( {index + 1}. {customer.firstname} {customer.address} handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5, flexShrink: 0 }} > } > {uploadType === 0 ? ( {customer.quantity ?? '—'} ) : ( handleQuantityChange(customer.customerid, e.target.value)} inputProps={{ min: 0 }} fullWidth sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }} /> )} {uploadType === 0 ? ( {`₹${Number(customer.collectionamt || 0).toFixed(2)}`} ) : ( { const v = Number(e.target.value); handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); }} inputProps={{ min: 0 }} InputProps={{ startAdornment: ₹ }} fullWidth sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 34 } }} /> )} ₹{Number(customer?.totalcharge || 0).toFixed(2)} ))} ₹{Number(totalAmt).toFixed(2)} )} {previewMode === 'drops' && !isMobile && ( # Customer Address Qty Cash Km Charge {' '} {dropCust.map((customer, index) => ( {index + 1} {customer.firstname} {customer.address} {uploadType === 0 ? ( customer.quantity ) : ( handleQuantityChange(customer.customerid, e.target.value)} inputProps={{ min: 0 }} sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} /> )} {uploadType === 0 ? ( `₹${Number(customer.collectionamt || 0).toFixed(2)}` ) : ( { const v = Number(e.target.value); handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); }} inputProps={{ min: 0 }} InputProps={{ startAdornment: ₹ }} sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} /> )} {customer.distance} ₹{Number(customer?.totalcharge || 0).toFixed(2)} handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5 }} > ))} Total {totalQty} ₹{Number(totalCash).toFixed(2)} {totaldist} ₹{Number(totalAmt).toFixed(2)} )} {previewMode === 'preview' && isMobile && ( {users.map((u, i) => ( {i + 1}. {u.firstname || '—'} {u.address || '—'} } > ))} )} {previewMode === 'preview' && !isMobile && ( # Name Contact Address Qty Cash {users.map((u, i) => ( {i + 1} {u.firstname || '—'} {u.contactno || '—'} {u.address || '—'} {u.quantity ?? '—'} {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} ))} )} {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 ( 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 }} > {title} {sub} ); }; return ( Choose a Drop Source to begin Select a source from below or the left panel to import or pick your delivery customers. {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' })} ); })()} {/* ============================== Saved customers dialog ============================== */} setIsCustomerOpen(false)} fullWidth fullScreen={isMobile} sx={{ '& .MuiDialog-paper': { borderRadius: { xs: 0, sm: '16px' }, overflow: 'hidden' } }} > {`Select Drop Customers (${dropCust.length || 0})`} setSearchCustList(e.target.value)} sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} startAdornment={ } endAdornment={ setSearchCustList('')} > } autoComplete="off" /> {customerlist?.length === 0 ? ( ) : ( {customerlist?.map((customer, index) => { const checked = dropCust.some((c) => c.customerid === customer.customerid); return ( handleCheckboxChange(event, customer)} /> } label={ {customer.firstname} ({customer.contactno}) {customer.address} } /> ); })} )} setIsCustomerOpen(false)} sx={{ borderRadius: '10px', textTransform: 'none', fontWeight: 600, px: 3 }} > {dropCust.length === 0 ? 'Close' : 'Continue'} > ); }; export default MultipleOrders;