915 lines
35 KiB
JavaScript
915 lines
35 KiB
JavaScript
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 (
|
|
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
|
|
{content}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 <div key={customer.id} className="option-item">
|
|
<input className="me-4 mt-2" type="checkbox" checked={newRouteCustomerList.find((item) => item.customer_id === customer.id)!==undefined} value={newRouteCustomerList.find((item) => item.customer_id === customer.id)!==undefined} onChange={(e) => toggleItemToRouteList(customer, e.target.value)}/>
|
|
<div>
|
|
<div>{`${customer.name}(${customer.name_cn})`}</div>
|
|
{newRouteCustomerList.find((item) => item.customer_id === customer.id) && (<div>
|
|
{addressOptions.map((address, idx) => (
|
|
<div key={`${customer.id}-address-${idx}`}>
|
|
<input className="me-4" name={`${customer.id}-address`} type="radio" onChange={(e) => setCustomerAddress(customer.id, e.currentTarget.value)} value={address} checked={newRouteCustomerList.find((item) => item.customer_id === customer.id)?.customer_address===address}/>
|
|
<small>{address}</small>
|
|
</div>
|
|
))}
|
|
</div>)}
|
|
</div>
|
|
</div>
|
|
}
|
|
)
|
|
};
|
|
|
|
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 <div key={customer.id} className="option-item">
|
|
<input className="me-4 mt-2" type="checkbox" checked={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)!==undefined} value={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)!==undefined} onChange={(e) => toggleGroupedItemToRouteList(customer, e.target.value)}/>
|
|
<div>
|
|
<div>{`${customer.name}(${customer.name_cn})`}</div>
|
|
{newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id) && (<div>
|
|
{addressOptions.map((address, idx) => (
|
|
<div key={`${customer.id}-group-address-${idx}`}>
|
|
<input className="me-4" name={`${customer.id}-address`} type="radio" onChange={(e) => setGroupedCustomerAddress(customer.id, e.currentTarget.value)} value={address} checked={newRouteGroupedCustomerList.find((item) => item.customer_id === customer.id)?.customer_address===address}/>
|
|
<small>{address}</small>
|
|
</div>
|
|
))}
|
|
</div>)}
|
|
</div>
|
|
</div>
|
|
}
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div
|
|
ref={drop}
|
|
className="customers-container mb-4"
|
|
style={{
|
|
backgroundColor: isOver ? '#f0f0f0' : 'transparent',
|
|
minHeight: isOver ? '100px' : 'auto',
|
|
border: isOver ? '2px dashed #0066B1' : '2px dashed transparent',
|
|
borderRadius: '4px',
|
|
padding: isOver ? '8px' : '0'
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<DndProvider backend={HTML5Backend}>
|
|
{ !viewMode && <h6 class="text-primary">Customers Assigned ({getCurrentAssignedNumber()})</h6>}
|
|
{ viewMode && <h6 class="text-primary">Route Assignment <button className="btn btn-sm btn-primary" onClick={() => editFun('assignment')}><Pencil size={16} className="me-2"></Pencil>Edit </button></h6>}
|
|
{!viewMode && <CustomersDropZone>
|
|
{customers.map((item, index) => {
|
|
if (item?.customers) {
|
|
return <Card key={index} index={index} moveCard={reorderItems} content={(<div className="customers-dnd-item-container">
|
|
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
|
<GripVertical className="me-4" size={20}></GripVertical>
|
|
<div className="customer-dnd-item" onClick={() => openEditAptGroupModal(index, item)}>
|
|
<span className="me-2">{item.customer_group} </span> <span>{item.customers[0]?.customer_group_address}</span>
|
|
<div className="customer-dnd-item-content">{item.customers.map(customer =>
|
|
<div key={customer.customer_id}>
|
|
|
|
<small className="me-2">{customer.customer_name}</small>
|
|
<small className="me-2">{customer.customer_address}</small>
|
|
<small className="me-2">{customer.customer_pickup_status}</small>
|
|
</div>)}
|
|
</div>
|
|
</div>
|
|
<div className="customer-delete-btn"><button className="btn btn-default" onClick={()=> deleteGroup(index)}><XSquare size={14}></XSquare></button></div>
|
|
</div>)}></Card>
|
|
} else {
|
|
return <Card key={index} index={index} moveCard={reorderItems} content={<div className="customers-dnd-item-container">
|
|
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
|
<GripVertical className="me-4" size={20}></GripVertical>
|
|
<div className="customer-dnd-item">
|
|
<span>{item.customer_name} </span>
|
|
<small className="me-2">{item.customer_address}</small>
|
|
<small className="me-2">{item.customer_pickup_status}</small>
|
|
</div>
|
|
<div className="customer-delete-btn"><button onClick={() => deleteCustomer(item.customer_id)} className="btn btn-default"><XSquare size={14}></XSquare> </button></div>
|
|
</div>}>
|
|
|
|
</Card>
|
|
}
|
|
})}
|
|
<div className="new-customers-dnd-item-container">
|
|
<div className="stop-index"><span>{`Stop ${customers?.length+1}`}</span><RecordCircleFill size={16} color={"#ccc"} className="ms-2"></RecordCircleFill> </div>
|
|
<div>
|
|
<button className="btn btn-primary btn-sm me-2 mb-2" onClick={() => openAddPersonnelModal()}> + Add Personnel </button>
|
|
<button className="btn btn-primary btn-sm me-2 mb-2" onClick={() => openAddAptGroupModal()}> + Add Apt Group </button>
|
|
</div>
|
|
|
|
</div>
|
|
</CustomersDropZone>}
|
|
{
|
|
viewMode && <div className="customers-container mb-4">
|
|
{customers.map((item, index) => {
|
|
if (item?.customers) {
|
|
return <div className="customers-dnd-item-container">
|
|
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
|
<div className="customer-dnd-item" onClick={() => openEditAptGroupModal(index, item)}>
|
|
<span className="me-2">{item.customer_group} </span> <span>{item.customers[0]?.customer_group_address}</span>
|
|
<div className="customer-dnd-item-content">{item.customers.map(customer =>
|
|
<div key={customer.customer_id}>
|
|
|
|
<small className="me-2">{customer.customer_name}</small>
|
|
<small className="me-2">{customer.customer_address}</small>
|
|
<small className="me-2">{customer.customer_pickup_status}</small>
|
|
</div>)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
} else {
|
|
return <div className="customers-dnd-item-container">
|
|
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
|
<div className="customer-dnd-item">
|
|
<span>{item.customer_name} </span>
|
|
<small className="me-2">{item.customer_address}</small>
|
|
<small className="me-2">{item.customer_pickup_status}</small>
|
|
</div>
|
|
</div>
|
|
}
|
|
})}
|
|
</div>
|
|
}
|
|
<Modal show={showAddPersonnelModal} onHide={() => closeAddPersonnelModal()}>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>Add Personnel</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="me-4">
|
|
<div className="field-label">Type in UserId OR Name OR Address to Search
|
|
</div>
|
|
<input type="text" className="mb-4" value={customerFilter} onChange={(e) => setCustomerFilter(e.target.value)}/>
|
|
</div>
|
|
|
|
</div>
|
|
<div>
|
|
<div className="app-main-content-fields-section">
|
|
{['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 <a key={item} className="me-2" onClick={() => {setLastNameFilter(item?.toLowerCase())} }>{item}</a>
|
|
})}
|
|
</div>
|
|
</div>
|
|
<a className="mb-4" onClick={() => setLastNameFilter(undefined)}>Clear All</a>
|
|
<div className="customers-container mt-4">
|
|
<Items currentItems={currentItems} />
|
|
<ReactPaginate
|
|
className="customers-pagination"
|
|
breakLabel="..."
|
|
nextLabel=">"
|
|
onPageChange={handlePageClick}
|
|
pageRangeDisplayed={5}
|
|
pageCount={pageCount}
|
|
previousLabel="<"
|
|
renderOnZeroPageCount={null}
|
|
containerClassName="pagination justify-content-center"
|
|
pageClassName="page-item"
|
|
pageLinkClassName="page-link"
|
|
previousClassName="page-item"
|
|
previousLinkClassName="page-link"
|
|
nextClassName="page-item"
|
|
nextLinkClassName="page-link"
|
|
activeClassName="active"
|
|
breakClassName="page-item"
|
|
breakLinkClassName="page-link"
|
|
/>
|
|
</div>
|
|
</>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="link" onClick={() => closeAddPersonnelModal()}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" size="sm" onClick={() => addPersonnel()}>
|
|
Add Personnel
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
|
|
<Modal show={showAddAptGroupModal} onHide={() => closeAddAptGroupModal()}>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>Add Apt Group</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="me-4">
|
|
<div className="field-label">Group Name
|
|
<span className="required">*</span>
|
|
</div>
|
|
<input type="text" value={newGroupName} onChange={(e) => setNewGroupNameAction(e.target.value)}/>
|
|
</div>
|
|
<div className="me-4">
|
|
<div className="field-label">Group Address
|
|
<span className="required">*</span>
|
|
</div>
|
|
<input type="text" value={newGroupAddress} onChange={(e) => setNewGroupAddressAction(e.target.value)}/>
|
|
</div>
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="me-4">
|
|
<div className="field-label">Type in user Id or Name to Search
|
|
</div>
|
|
<input type="text" className="mb-4" value={customerFilter} onChange={(e) => setCustomerFilter(e.target.value)}/>
|
|
</div>
|
|
|
|
</div>
|
|
<div>
|
|
<div className="mb-4">
|
|
{['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 <a key={item} className="me-2" onClick={() => {setLastNameFilter(item?.toLowerCase())} }>{item}</a>
|
|
})}
|
|
|
|
</div>
|
|
</div>
|
|
<a className="mb-4" onClick={() => setLastNameFilter(undefined)}>Clear All</a>
|
|
<div className="customers-container mt-4">
|
|
<ItemsGroup currentItems={currentItems} />
|
|
<ReactPaginate
|
|
className="customers-pagination"
|
|
breakLabel="..."
|
|
nextLabel=">"
|
|
onPageChange={handlePageClick}
|
|
pageRangeDisplayed={5}
|
|
pageCount={pageCount}
|
|
previousLabel="<"
|
|
renderOnZeroPageCount={null}
|
|
containerClassName="pagination justify-content-center"
|
|
pageClassName="page-item"
|
|
pageLinkClassName="page-link"
|
|
previousClassName="page-item"
|
|
previousLinkClassName="page-link"
|
|
nextClassName="page-item"
|
|
nextLinkClassName="page-link"
|
|
activeClassName="active"
|
|
breakClassName="page-item"
|
|
breakLinkClassName="page-link"
|
|
/>
|
|
</div>
|
|
</>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="link" size="sm" onClick={() => closeAddAptGroupModal()}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" size="sm" onClick={() => addAptGroup()}>
|
|
Add Apt Group
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
|
|
<Modal show={showEditAptGroupModal} onHide={() => closeEditAptGroupModal()}>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>Update Apt Group</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="me-4">
|
|
<div className="field-label">Group Name
|
|
<span className="required">*</span>
|
|
</div>
|
|
<input type="text" value={newGroupName} onChange={(e) => setNewGroupNameAction(e.target.value)}/>
|
|
</div>
|
|
<div className="me-4">
|
|
<div className="field-label">Group Address
|
|
<span className="required">*</span>
|
|
</div>
|
|
<input type="text" value={newGroupAddress} onChange={(e) => setNewGroupAddressAction(e.target.value)}/>
|
|
</div>
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="me-4">
|
|
<div className="field-label">Type in user Id or Name to Search
|
|
</div>
|
|
<input type="text" className="mb-4" value={customerFilter} onChange={(e) => setCustomerFilter(e.target.value)}/>
|
|
</div>
|
|
|
|
</div>
|
|
<div>
|
|
<div className="mb-4">
|
|
{['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 <a key={item} className="me-2" onClick={() => {setLastNameFilter(item?.toLowerCase())} }>{item}</a>
|
|
})}
|
|
|
|
</div>
|
|
</div>
|
|
<a className="mb-4" onClick={() => setLastNameFilter(undefined)}>Clear All</a>
|
|
<div className="customers-container mt-4">
|
|
<ItemsGroup currentItems={currentItems} />
|
|
<ReactPaginate
|
|
className="customers-pagination"
|
|
breakLabel="..."
|
|
nextLabel=">"
|
|
onPageChange={handlePageClick}
|
|
pageRangeDisplayed={5}
|
|
pageCount={pageCount}
|
|
previousLabel="<"
|
|
renderOnZeroPageCount={null}
|
|
containerClassName="pagination justify-content-center"
|
|
pageClassName="page-item"
|
|
pageLinkClassName="page-link"
|
|
previousClassName="page-item"
|
|
previousLinkClassName="page-link"
|
|
nextClassName="page-item"
|
|
nextLinkClassName="page-link"
|
|
activeClassName="active"
|
|
breakClassName="page-item"
|
|
breakLinkClassName="page-link"
|
|
/>
|
|
</div>
|
|
</>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="link" size="sm" onClick={() => closeEditAptGroupModal()}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" size="sm" onClick={() => editAptGroup()}>
|
|
Update Apt Group
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
</DndProvider>
|
|
);
|
|
};
|
|
|
|
export default RouteCustomerEditor; |