2024 lines
82 KiB
JavaScript
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 & 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 & 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;
|