import React, {useEffect, useRef, useState, useCallback} from 'react'; import { useDrag, useDrop, DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import update from 'immutability-helper'; import { Modal, Button } from "react-bootstrap"; import { CustomerService } from '../../services'; import { PERSONAL_ROUTE_STATUS } from '../../shared'; import ReactPaginate from 'react-paginate'; import { GripVertical, Pencil, RecordCircleFill, XSquare } from 'react-bootstrap-icons'; const ItemTypes = { CARD: 'card', UNASSIGNED_CUSTOMER: 'unassigned_customer', }; const Card = ({ content, index, moveCard }) => { const ref = useRef(null); const [{ handlerId }, drop] = useDrop({ accept: ItemTypes.CARD, collect(monitor) { return { handlerId: monitor.getHandlerId(), } }, drop(item, monitor) { if (!ref.current) { return } const dragIndex = item.index const hoverIndex = index // Don't replace items with themselves if (dragIndex === hoverIndex) { return } // Determine rectangle on screen const hoverBoundingRect = ref.current?.getBoundingClientRect() // Get vertical middle const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 // Determine mouse position const clientOffset = monitor.getClientOffset() // Get pixels to the top const hoverClientY = clientOffset.y - hoverBoundingRect.top // Only perform the move when the mouse has crossed half of the items height // When dragging downwards, only move when the cursor is below 50% // When dragging upwards, only move when the cursor is above 50% // Dragging downwards if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { return } // Dragging upwards if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { return } // Time to actually perform the action moveCard(dragIndex, hoverIndex) // Note: we're mutating the monitor item here! // Generally it's better to avoid mutations, // but it's good here for the sake of performance // to avoid expensive index searches. item.index = hoverIndex }, }) const [{ isDragging }, drag] = useDrag({ type: ItemTypes.CARD, item: () => { return { index } }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }) const opacity = isDragging ? 0 : 1 drag(drop(ref)) return (
{content}
) } const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun, onAddCustomer = null}) => { const [customers, setCustomers] = useState([]); const [initializedRouteId, setInitializedRouteId] = useState(null); const [showAddPersonnelModal, setShowAddPersonnelModal] = useState(false); const [showAddAptGroupModal, setShowAddAptGroupModal] = useState(false); const [showEditAptGroupModal, setShowEditAptGroupModal] = useState(false); const [editGroupIndex, setEditGroupIndex] = useState(-1); const [customerOptions, setCustomerOptions] = useState([]); const [customerFilter, setCustomerFilter] = useState(''); const [lastNameFilter, setLastNameFilter] = useState(undefined); const [newRouteCustomerList, setNewRouteCustomerList] = useState([]); const [newRouteGroupedCustomerList, setNewRouteGroupedCustomerList] = useState([]); const [newGroupName, setNewGroupName] = useState(''); const [newGroupAddress, setNewGroupAddress] = useState(''); // We start with an empty list of items. const [currentItems, setCurrentItems] = useState(null); // Here we use item offsets; we could also use page offsets // following the API or data you're working with. const [itemOffset, setItemOffset] = useState(0); const [pageCount, setPageCount] = useState(0); const itemsPerPage = 10; // Helper function to get all customer IDs already in the route const getAssignedCustomerIds = () => { const assignedIds = new Set(); customers.forEach(item => { if (item.customer_id) { assignedIds.add(item.customer_id); } if (item.customers) { item.customers.forEach(c => { if (c.customer_id) { assignedIds.add(c.customer_id); } }); } }); return assignedIds; }; const formatStructuredAddress = (line1, line2, city, state, zipCode) => { const cityState = [city, state].filter(Boolean).join(', '); return [line1, line2, cityState, zipCode] .filter(item => item && String(item).trim() !== '') .join(' ') .trim(); }; const getCustomerAddressOptions = (customer) => { if (!customer) return []; const structuredAddresses = [ formatStructuredAddress(customer.address_line_1, customer.address_line_2, customer.city, customer.state, customer.zip_code), formatStructuredAddress(customer.address2_line_1, customer.address2_line_2, customer.city2, customer.state2, customer.zip_code2), formatStructuredAddress(customer.address3_line_1, customer.address3_line_2, customer.city3, customer.state3, customer.zip_code3), formatStructuredAddress(customer.address4_line_1, customer.address4_line_2, customer.city4, customer.state4, customer.zip_code4), formatStructuredAddress(customer.address5_line_1, customer.address5_line_2, customer.city5, customer.state5, customer.zip_code5), ]; const legacyAddresses = [ customer.address1, customer.address2, customer.address3, customer.address4, customer.address5, ]; return Array.from( new Set( [...structuredAddresses, ...legacyAddresses] .map(item => (item || '').trim()) .filter(item => item !== '') ) ); }; useEffect(() => { // Fetch items from another resources. const endOffset = itemOffset + itemsPerPage; const assignedIds = getAssignedCustomerIds(); // Filter out customers already in the route const availableCustomers = customerOptions?.filter(customer => !assignedIds.has(customer.id)); setCurrentItems(availableCustomers?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).slice(itemOffset, endOffset)); setPageCount(Math.ceil(availableCustomers?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter).filter((customer) => customer.name.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length / itemsPerPage)); }, [customerOptions, itemOffset, customerFilter, lastNameFilter, customers]); const handlePageClick = (event) => { const assignedIds = getAssignedCustomerIds(); const availableCustomers = customerOptions?.filter(customer => !assignedIds.has(customer.id)); const newOffset = (event.selected * itemsPerPage) % availableCustomers?.filter((customer) => customer.name?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.id?.toLowerCase().includes(customerFilter?.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).length; console.log( `User requested page number ${event.selected}, which is offset ${newOffset}` ); setItemOffset(newOffset); }; const closeAddPersonnelModal = () => { setShowAddPersonnelModal(false); setNewRouteCustomerList([]); } const openAddPersonnelModal = () => { setItemOffset(0); setPageCount(0); setLastNameFilter(undefined); if (customerOptions.length === 0) { CustomerService.getAllActiveCustomers().then((data) => { // Filter out discharged customers const filtered = (data.data || []).filter(customer => { const isDischarged = customer.type === 'discharged' || (customer.name && customer.name.toLowerCase().includes('discharged')) || customer.status !== 'active'; return !isDischarged; }); setCustomerOptions(filtered); }) } setShowAddPersonnelModal(true); } const closeAddAptGroupModal = () => { setShowAddAptGroupModal(false); } const openAddAptGroupModal = () => { setItemOffset(0); setPageCount(0); setLastNameFilter(undefined); if (customerOptions.length === 0) { CustomerService.getAllActiveCustomers().then((data) => { // Filter out discharged customers const filtered = (data.data || []).filter(customer => { const isDischarged = customer.type === 'discharged' || (customer.name && customer.name.toLowerCase().includes('discharged')) || customer.status !== 'active'; return !isDischarged; }); setCustomerOptions(filtered); }) } setShowAddAptGroupModal(true); } const closeEditAptGroupModal = () => { setShowEditAptGroupModal(false); setNewGroupAddress(''); setNewGroupName(''); setNewRouteGroupedCustomerList([]); setEditGroupIndex(-1); } const openEditAptGroupModal = (index, group) => { setItemOffset(0); setPageCount(0); setLastNameFilter(undefined); if (customerOptions.length === 0) { CustomerService.getAllActiveCustomers().then((data) => { // Filter out discharged customers const filtered = (data.data || []).filter(customer => { const isDischarged = customer.type === 'discharged' || (customer.name && customer.name.toLowerCase().includes('discharged')) || customer.status !== 'active'; return !isDischarged; }); setCustomerOptions(filtered); }) } setNewGroupAddress(group.customers[0].customer_group_address); setNewGroupName(group.customer_group); setNewRouteGroupedCustomerList(group.customers); setEditGroupIndex(index); setShowEditAptGroupModal(true); } const toggleItemToRouteList = (customer, value) => { if (value === 'false') { const customerAddresses = getCustomerAddressOptions(customer); setNewRouteCustomerList([].concat(newRouteCustomerList).concat([{ customer_id: customer.id, customer_name: `${customer.name} ${customer.name_cn?.length > 0 ? `(${customer.name_cn})` : ``}`, customer_address: customerAddresses[0] || '', customer_avatar: customer.avatar, customer_type: customer.type, customer_pickup_status: customer.pickup_status, customer_note: customer.note, customer_special_needs: customer.special_needs, customer_phone: customer.phone || customer.mobile_phone || customer.home_phone, customer_route_status: PERSONAL_ROUTE_STATUS.NO_STATUS, customer_pickup_order: customers.length + newRouteCustomerList.length + 1, customer_table_id: customer.table_id, customer_language: customer.language }])); } else { setNewRouteCustomerList([].concat(newRouteCustomerList.filter((item) => item.customer_id !== customer.id))); } } const toggleGroupedItemToRouteList = (customer, value) => { if (value === 'false') { const customerAddresses = getCustomerAddressOptions(customer); setNewRouteGroupedCustomerList([].concat(newRouteGroupedCustomerList).concat([{ customer_id: customer.id, customer_name: `${customer.name} ${customer.name_cn?.length > 0 ? `(${customer.name_cn})` : ``}`, customer_address: customerAddresses[0] || '', customer_avatar: customer.avatar, customer_group: newGroupName, customer_group_address: newGroupAddress, customer_type: customer.type, customer_pickup_status: customer.pickup_status, customer_note: customer.note, customer_special_needs: customer.special_needs, customer_phone: customer.phone || customer.mobile_phone || customer.home_phone, customer_route_status: PERSONAL_ROUTE_STATUS.NO_STATUS, customer_pickup_order: customers.length + 1, customer_table_id: customer.table_id, customer_language: customer.language }])); } else { setNewRouteGroupedCustomerList([].concat(newRouteGroupedCustomerList.filter((item) => item.customer_id !== customer.id))); } } const setCustomerAddress = (id, value) => { setNewRouteCustomerList(newRouteCustomerList.map((item) => { if (item.customer_id === id) { return { ...item, customer_address: value } } else { return item; } })) } const setGroupedCustomerAddress = (id, value) => { setNewRouteGroupedCustomerList(newRouteGroupedCustomerList.map((item) => { if (item.customer_id === id) { return { ...item, customer_address: value } } else { return item; } })) } const setNewGroupNameAction = (value) => { setNewGroupName(value); for (const item of newRouteGroupedCustomerList) { item.customer_group = value; } } const setNewGroupAddressAction = (value) => { setNewGroupAddress(value); for (const item of newRouteGroupedCustomerList) { item.customer_group_address = value; } } const checkGroupRequiredField = () => { if ((!newGroupName || newGroupName.replace(' ', '') === '') || (!newGroupAddress || newGroupAddress.replace(' ', '') === '')) { window.alert('Group Name and Group Address is Required') return false; } return true; } const addPersonnel = () => { const result = [].concat(customers).concat(newRouteCustomerList); setCustomers(result.filter((item, pos) => result.indexOf(item) === pos)); setShowAddPersonnelModal(false); setNewRouteCustomerList([]); } // Function to add a customer from external drop (like unassigned customers) const addCustomerFromDrop = useCallback((customerData, dropIndex = null) => { if (!customerData || !customerData.id) return; // Check if customer already exists const customerExists = customers.some(item => { if (item.customer_id) { return item.customer_id === customerData.id; } if (item.customers) { return item.customers.some(c => c.customer_id === customerData.id); } return false; }); if (customerExists) return; const newCustomer = { customer_id: customerData.id, customer_name: `${customerData.name} ${customerData.name_cn?.length > 0 ? `(${customerData.name_cn})` : ``}`, customer_address: getCustomerAddressOptions(customerData)[0] || '', customer_avatar: customerData.avatar, customer_type: customerData.type, customer_pickup_status: customerData.pickup_status, customer_note: customerData.note, customer_special_needs: customerData.special_needs, customer_phone: customerData.phone || customerData.mobile_phone || customerData.home_phone, customer_route_status: PERSONAL_ROUTE_STATUS.NO_STATUS, customer_pickup_order: customers.length + 1, customer_table_id: customerData.table_id, customer_language: customerData.language }; setCustomers(prevCustomers => { if (dropIndex !== null && dropIndex >= 0 && dropIndex <= prevCustomers.length) { // Insert at specific position const newList = [...prevCustomers]; newList.splice(dropIndex, 0, newCustomer); return newList; } else { // Add to end return [...prevCustomers, newCustomer]; } }); }, [customers]); const addAptGroup = () => { if (checkGroupRequiredField()) { const result = [].concat(customers).concat([{ customers: newRouteGroupedCustomerList, customer_pickup_order: customers.length + 1, customer_group: newGroupName, }]); setCustomers(result.filter((item, pos) => result.indexOf(item) === pos)); setShowAddAptGroupModal(false); setNewRouteGroupedCustomerList([]); setNewGroupAddress(''); setNewGroupName(''); setEditGroupIndex(-1); } } const editAptGroup = () => { if (checkGroupRequiredField()) { const result = [].concat(customers); result[editGroupIndex] = { ...result[editGroupIndex], customers: newRouteGroupedCustomerList, customer_group: newGroupName, } setCustomers(result.filter((item, pos) => result.indexOf(item) === pos)); setShowEditAptGroupModal(false); setNewGroupAddress(''); setNewGroupName(''); setNewRouteGroupedCustomerList([]); setEditGroupIndex(-1); } } // Only initialize customers from currentRoute when the route ID changes // Don't reset when currentRoute object reference changes (which happens on every render) // Note: template sub-document routes use _id instead of id const currentRouteId = currentRoute?.id || currentRoute?._id; useEffect(() => { if (currentRouteId && currentRouteId !== initializedRouteId) { setCustomers(getRouteCustomersWithGroups()); setInitializedRouteId(currentRouteId); } }, [currentRouteId, initializedRouteId]) const getRouteCustomersWithGroups = () => { const customerList = currentRoute?.route_customer_list?.map(item => Object.assign({}, item, {routeType: currentRoute.type, routeId: currentRoute.id || currentRoute._id})); const result = {}; if (customerList) { for (const customer of customerList) { if (customer.customer_group) { if (result[customer.customer_group]) { result[customer.customer_group].push(customer); } else { result[customer.customer_group] = []; result[customer.customer_group].push(customer); } } else { if (result.no_group) { result.no_group.push(customer); } else { result.no_group = []; result.no_group.push(customer); } } } } let finalResult = []; for (const key of Object.keys(result)) { if (key === 'no_group') { finalResult = finalResult.concat(result[key]); } else { finalResult.push({ customer_pickup_order: result[key][0].customer_pickup_order, customer_group: key, customers: result[key] }) } } return finalResult.sort((a, b) => a.customer_pickup_order - b.customer_pickup_order); } const deleteCustomer = (id) => { if (!window.confirm('Are you sure you want to remove this customer from the route?')) { return; } setCustomers(customers.filter((customer) => customer.customer_id !== id)); } const deleteGroup = (index) => { if (!window.confirm('Are you sure you want to remove this group from the route?')) { return; } const arr = [].concat(customers); arr.splice(index, 1); setCustomers(arr); } const reorderItems = useCallback((dragIndex, hoverIndex) => { setCustomers((prevCards) => { return update(prevCards, { $splice: [ [dragIndex, 1], [hoverIndex, 0, prevCards[dragIndex]] ] }) }); }, []); const Items = ({ currentItems }) => { return currentItems?.map( (customer) => { const addressOptions = getCustomerAddressOptions(customer); return
item.customer_id === customer.id)!==undefined} value={newRouteCustomerList.find((item) => item.customer_id === customer.id)!==undefined} onChange={(e) => toggleItemToRouteList(customer, e.target.value)}/>
{`${customer.name}(${customer.name_cn})`}
{newRouteCustomerList.find((item) => item.customer_id === customer.id) && (
{addressOptions.map((address, idx) => (
setCustomerAddress(customer.id, e.currentTarget.value)} value={address} checked={newRouteCustomerList.find((item) => item.customer_id === customer.id)?.customer_address===address}/> {address}
))}
)}
} ) }; const ItemsGroup = ({ currentItems }) => { const assignedIds = getAssignedCustomerIds(); return currentItems?.filter(customer => !assignedIds.has(customer.id)).filter((customer) => customer.name.toLowerCase().includes(customerFilter.toLowerCase()) || customer.id.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address1?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address2?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address3?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address4?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.address5?.toLowerCase().includes(customerFilter.toLowerCase()) || customer.apartment?.toLowerCase().includes(customerFilter.toLocaleLowerCase()) ).map( (customer) => { const addressOptions = getCustomerAddressOptions(customer); return
item.customer_id === customer.id)!==undefined} value={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)!==undefined} onChange={(e) => toggleGroupedItemToRouteList(customer, e.target.value)}/>
{`${customer.name}(${customer.name_cn})`}
{newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id) && (
{addressOptions.map((address, idx) => (
setGroupedCustomerAddress(customer.id, e.currentTarget.value)} value={address} checked={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)?.customer_address===address}/> {address}
))}
)}
} ) } const getCurrentAssignedNumber = () => { let count = 0; for (const item of customers) { if (item.customers) { for (const customer of item.customers) { count++; } } else { count++; } } return count; } // Expose addCustomerFromDrop via callback useEffect(() => { if (onAddCustomer && typeof onAddCustomer === 'function') { onAddCustomer(addCustomerFromDrop); } }, [addCustomerFromDrop, onAddCustomer]); useEffect(() => { const result = []; for (const item of customers) { if (item.customer_group) { for (const customer of item.customers) { customer.customer_pickup_order = customers.indexOf(item); result.push(customer); } } else { item.customer_pickup_order = customers.indexOf(item); result.push(item); } } setNewCustomerList(result); }, [customers]) // Create a drop zone for unassigned customers const CustomersDropZone = ({ children }) => { const [{ isOver }, drop] = useDrop({ accept: 'UNASSIGNED_CUSTOMER', drop: (item, monitor) => { if (addCustomerFromDrop && item && item.id) { addCustomerFromDrop(item, null); // Add to end } return { dropped: true }; }, collect: (monitor) => ({ isOver: monitor.isOver(), }), }); return (
{children}
); }; return ( { !viewMode &&
Customers Assigned ({getCurrentAssignedNumber()})
} { viewMode &&
Route Assignment
} {!viewMode && {customers.map((item, index) => { if (item?.customers) { return
{`Stop ${index+1}`}
openEditAptGroupModal(index, item)}> {item.customer_group} {item.customers[0]?.customer_group_address}
{item.customers.map(customer =>
{customer.customer_name} {customer.customer_address} {customer.customer_pickup_status}
)}
)}>
} else { return
{`Stop ${index+1}`}
{item.customer_name} {item.customer_address} {item.customer_pickup_status}
}>
} })}
{`Stop ${customers?.length+1}`}
} { viewMode &&
{customers.map((item, index) => { if (item?.customers) { return
{`Stop ${index+1}`}
openEditAptGroupModal(index, item)}> {item.customer_group} {item.customers[0]?.customer_group_address}
{item.customers.map(customer =>
{customer.customer_name} {customer.customer_address} {customer.customer_pickup_status}
)}
} else { return
{`Stop ${index+1}`}
{item.customer_name} {item.customer_address} {item.customer_pickup_status}
} })}
} closeAddPersonnelModal()}> Add Personnel <>
Type in UserId OR Name OR Address to Search
setCustomerFilter(e.target.value)}/>
{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'].map(item => { return {setLastNameFilter(item?.toLowerCase())} }>{item} })}
setLastNameFilter(undefined)}>Clear All
closeAddAptGroupModal()}> Add Apt Group <>
Group Name *
setNewGroupNameAction(e.target.value)}/>
Group Address *
setNewGroupAddressAction(e.target.value)}/>
Type in user Id or Name to Search
setCustomerFilter(e.target.value)}/>
{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'].map(item => { return {setLastNameFilter(item?.toLowerCase())} }>{item} })}
setLastNameFilter(undefined)}>Clear All
closeEditAptGroupModal()}> Update Apt Group <>
Group Name *
setNewGroupNameAction(e.target.value)}/>
Group Address *
setNewGroupAddressAction(e.target.value)}/>
Type in user Id or Name to Search
setCustomerFilter(e.target.value)}/>
{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'].map(item => { return {setLastNameFilter(item?.toLowerCase())} }>{item} })}
setLastNameFilter(undefined)}>Clear All
); }; export default RouteCustomerEditor;