All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 32s
1031 lines
39 KiB
JavaScript
1031 lines
39 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, isOver, draggedIndex }, drop] = useDrop({
|
|
accept: ItemTypes.CARD,
|
|
collect(monitor) {
|
|
return {
|
|
handlerId: monitor.getHandlerId(),
|
|
isOver: monitor.isOver({ shallow: true }),
|
|
draggedIndex: monitor.getItem()?.index,
|
|
}
|
|
},
|
|
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
|
|
const showDropTopIndicator = isOver && draggedIndex !== undefined && draggedIndex > index;
|
|
const showDropBottomIndicator = isOver && draggedIndex !== undefined && draggedIndex < index;
|
|
const dropZoneStyle = {
|
|
opacity,
|
|
paddingTop: '10px',
|
|
paddingBottom: '10px',
|
|
borderRadius: '8px',
|
|
backgroundColor: isOver ? '#eef6ff' : 'transparent',
|
|
borderTop: showDropTopIndicator ? '4px solid #0d6efd' : '4px solid transparent',
|
|
borderBottom: showDropBottomIndicator ? '4px solid #0d6efd' : '4px solid transparent',
|
|
transition: 'background-color 0.12s ease, border-color 0.12s ease'
|
|
};
|
|
drag(drop(ref))
|
|
return (
|
|
<div ref={ref} style={dropZoneStyle} data-handler-id={handlerId}>
|
|
{content}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun = () => {}, canEdit = true, onAddCustomer = null, scheduledAbsentCustomerIds = []}) => {
|
|
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;
|
|
const hasActiveFilters = Boolean((customerFilter || '').trim()) || Boolean(lastNameFilter);
|
|
const scheduledAbsentIdsSet = new Set([
|
|
...(Array.isArray(scheduledAbsentCustomerIds) ? scheduledAbsentCustomerIds : []),
|
|
...((currentRoute?.route_customer_list || [])
|
|
.filter((item) => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)
|
|
.map((item) => item?.customer_id)
|
|
.filter(Boolean))
|
|
]);
|
|
const getAbsentNameStyle = (customerId) => (
|
|
scheduledAbsentIdsSet.has(customerId) ? { color: '#dc3545', fontWeight: 700 } : {}
|
|
);
|
|
|
|
// 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, note) => {
|
|
const cityState = [city, state].filter(Boolean).join(', ');
|
|
const mainAddress = [line1, line2, cityState, zipCode]
|
|
.filter(item => item && String(item).trim() !== '')
|
|
.join(' ')
|
|
.trim();
|
|
const addressNote = (note || '').trim();
|
|
if (!mainAddress) return '';
|
|
return addressNote ? `${mainAddress} (${addressNote})` : mainAddress;
|
|
};
|
|
|
|
const getCustomerAddressOptions = (customer) => {
|
|
if (!customer) return [];
|
|
|
|
const addresses = [
|
|
formatStructuredAddress(customer.address_line_1, customer.address_line_2, customer.city, customer.state, customer.zip_code, customer.address_note),
|
|
formatStructuredAddress(customer.address2_line_1, customer.address2_line_2, customer.city2, customer.state2, customer.zip_code2, customer.address2_note),
|
|
formatStructuredAddress(customer.address3_line_1, customer.address3_line_2, customer.city3, customer.state3, customer.zip_code3, customer.address3_note),
|
|
formatStructuredAddress(customer.address4_line_1, customer.address4_line_2, customer.city4, customer.state4, customer.zip_code4, customer.address4_note),
|
|
];
|
|
|
|
return addresses.filter(address => (address || '').trim() !== '');
|
|
};
|
|
|
|
const getCustomerSearchAddresses = (customer) => {
|
|
if (!customer) return [];
|
|
const addresses = [
|
|
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),
|
|
];
|
|
return addresses.filter(address => (address || '').trim() !== '');
|
|
};
|
|
|
|
const getCustomerSearchText = (customer) => {
|
|
const allAddressText = getCustomerSearchAddresses(customer).join(' ');
|
|
return [
|
|
customer?.name,
|
|
customer?.id,
|
|
customer?.apartment,
|
|
allAddressText,
|
|
// Keep legacy fields searchable for older records.
|
|
customer?.address1,
|
|
customer?.address2,
|
|
customer?.address3,
|
|
customer?.address4,
|
|
customer?.address5,
|
|
]
|
|
.map(item => (item || '').toString().toLowerCase())
|
|
.join(' ');
|
|
};
|
|
|
|
const matchesCustomerSearch = (customer, keyword) => {
|
|
const normalizedKeyword = (keyword || '').toLowerCase().trim();
|
|
if (!normalizedKeyword) return true;
|
|
return getCustomerSearchText(customer).includes(normalizedKeyword);
|
|
};
|
|
|
|
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));
|
|
const filteredCustomers = availableCustomers
|
|
?.filter(customer => (lastNameFilter && (customer.lastname?.toLowerCase().indexOf(lastNameFilter) === 0)) || !lastNameFilter)
|
|
?.filter((customer) => matchesCustomerSearch(customer, customerFilter)) || [];
|
|
if (hasActiveFilters) {
|
|
setCurrentItems(filteredCustomers.slice(itemOffset, endOffset));
|
|
setPageCount(Math.ceil(filteredCustomers.length / itemsPerPage));
|
|
} else {
|
|
setCurrentItems(filteredCustomers);
|
|
setPageCount(0);
|
|
if (itemOffset !== 0) {
|
|
setItemOffset(0);
|
|
}
|
|
}
|
|
}, [customerOptions, itemOffset, customerFilter, lastNameFilter, customers]);
|
|
|
|
const handlePageClick = (event) => {
|
|
const assignedIds = getAssignedCustomerIds();
|
|
const availableCustomers = customerOptions?.filter(customer => !assignedIds.has(customer.id));
|
|
const matchedCount = availableCustomers?.filter((customer) => matchesCustomerSearch(customer, customerFilter)).length || 0;
|
|
const newOffset = matchedCount > 0 ? (event.selected * itemsPerPage) % matchedCount : 0;
|
|
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.notes_for_driver || '',
|
|
customer_special_needs: customer.notes_for_driver || '',
|
|
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.notes_for_driver || '',
|
|
customer_special_needs: customer.notes_for_driver || '',
|
|
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 removeGroupedCustomerFromSelection = (id) => {
|
|
setNewRouteGroupedCustomerList((prevList) => prevList.filter((item) => item.customer_id !== id));
|
|
}
|
|
|
|
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 merged = [...customers];
|
|
newRouteCustomerList.forEach((newCustomer) => {
|
|
const existingIndex = merged.findIndex((item) => item?.customer_id === newCustomer?.customer_id);
|
|
if (existingIndex >= 0) {
|
|
// If the customer already exists, overwrite with latest selection (including address).
|
|
merged[existingIndex] = {
|
|
...merged[existingIndex],
|
|
...newCustomer,
|
|
};
|
|
} else {
|
|
merged.push(newCustomer);
|
|
}
|
|
});
|
|
setCustomers(merged);
|
|
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.notes_for_driver || '',
|
|
customer_special_needs: customerData.notes_for_driver || '',
|
|
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);
|
|
const customerDisplayName = customer?.name || '';
|
|
const customerChineseName = customer?.name_cn || '';
|
|
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>{`${customerDisplayName}${customerChineseName ? `(${customerChineseName})` : ''}`}</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) => matchesCustomerSearch(customer, customerFilter)).map(
|
|
(customer) => {
|
|
const addressOptions = getCustomerAddressOptions(customer);
|
|
const customerDisplayName = customer?.name || '';
|
|
const customerChineseName = customer?.name_cn || '';
|
|
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>{`${customerDisplayName}${customerChineseName ? `(${customerChineseName})` : ''}`}</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 {canEdit && <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" style={getAbsentNameStyle(customer.customer_id)}>{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 style={getAbsentNameStyle(item.customer_id)}>{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={() => canEdit && openEditAptGroupModal(index, item)} style={{ cursor: canEdit ? 'pointer' : 'default' }}>
|
|
<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" style={getAbsentNameStyle(customer.customer_id)}>{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 style={getAbsentNameStyle(item.customer_id)}>{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">
|
|
<div style={{ maxHeight: '420px', overflowY: 'auto' }}>
|
|
<Items currentItems={currentItems} />
|
|
</div>
|
|
{hasActiveFilters && pageCount > 1 && <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">
|
|
<div style={{ maxHeight: '420px', overflowY: 'auto' }}>
|
|
<ItemsGroup currentItems={currentItems} />
|
|
</div>
|
|
{hasActiveFilters && pageCount > 1 && <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" style={{ width: '100%' }}>
|
|
<div className="field-label">Selected Customers ({newRouteGroupedCustomerList.length})</div>
|
|
{newRouteGroupedCustomerList.length === 0 ? (
|
|
<small className="text-muted">No customer selected yet.</small>
|
|
) : (
|
|
<div className="customers-container" style={{ maxHeight: '180px', overflowY: 'auto', padding: '8px' }}>
|
|
{newRouteGroupedCustomerList.map((customer) => (
|
|
<div
|
|
key={`selected-group-customer-${customer.customer_id}`}
|
|
className="d-flex align-items-center justify-content-between mb-2"
|
|
style={{ gap: '12px' }}
|
|
>
|
|
<div>
|
|
<div><small>{customer.customer_name}</small></div>
|
|
<div><small className="text-muted">{customer.customer_address}</small></div>
|
|
</div>
|
|
<Button
|
|
variant="outline-danger"
|
|
size="sm"
|
|
onClick={() => removeGroupedCustomerFromSelection(customer.customer_id)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</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">
|
|
<div style={{ maxHeight: '420px', overflowY: 'auto' }}>
|
|
<ItemsGroup currentItems={currentItems} />
|
|
</div>
|
|
{hasActiveFilters && pageCount > 1 && <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; |