Files
worldshine-redesign/client/src/components/trans-routes/RouteEdit.js
Lixian Zhou fb6a996d9e
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 29s
fix
2026-03-16 16:33:42 -04:00

777 lines
37 KiB
JavaScript

import React, {useEffect, useState, useRef} from "react";
import { useSelector,useDispatch } from "react-redux";
import { useParams, useNavigate } from "react-router-dom";
import { selectAllRoutes, transRoutesSlice, selectTomorrowAllRoutes, selectAllActiveVehicles, selectHistoryRoutes } from "./../../store";
import { Breadcrumb, Tabs, Tab } from "react-bootstrap";
import RouteCustomerEditor from "./RouteCustomerEditor";
import { AuthService, TransRoutesService, CustomerService, EventsService, EmployeeService } from "../../services";
import TimePicker from 'react-time-picker';
import 'react-time-picker/dist/TimePicker.css';
import moment from 'moment';
import { Archive, GripVertical } from "react-bootstrap-icons";
import { PERSONAL_ROUTE_STATUS } from "../../shared";
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useDrag } from 'react-dnd';
const RouteEdit = () => {
const params = useParams();
const allRoutes = useSelector(selectAllRoutes);
const tomorrowRoutes = useSelector(selectTomorrowAllRoutes);
const historyRoutes = useSelector(selectHistoryRoutes);
const vehicles = useSelector(selectAllActiveVehicles);
// const currentRoute = (allRoutes.find(item => item.id === params.id)) || (tomorrowRoutes.find(item => item.id === params.id)) || (historyRoutes.find(item => item.id === params.id)) || {};
const currentVehicle = vehicles.find(item => item.id === currentRoute?.vehicle ) || [];
const navigate = useNavigate();
const dispatch = useDispatch();
const { updateRoute} = transRoutesSlice.actions;
const [routeName, setRouteName] = useState('');
const [newDriver, setNewDriver] = useState('');
const [newVehicle, setNewVehicle] = useState('');
const [newRouteType, setNewRouteType] = useState('');
const [newCustomerList, setNewCustomerList] = useState([]);
const [errorMessage, setErrorMessage] = useState(undefined);
const [estimatedStartTime, setEstimatedStartTime] = useState(undefined);
const [currentRoute, setCurrentRoute] = useState(undefined);
const [allCustomers, setAllCustomers] = useState([]);
const [driverOptions, setDriverOptions] = useState([]);
const [unassignedCustomers, setUnassignedCustomers] = useState([]);
const [addCustomerToRoute, setAddCustomerToRoute] = useState(null);
const [attendanceAbsentCustomers, setAttendanceAbsentCustomers] = useState([]); // customers with attendance notes on route date
const initialCustomerListRef = useRef(null);
const paramsQuery = new URLSearchParams(window.location.search);
const isDriverEligibleEmployee = (employee) => {
if (!employee || employee.status !== 'active') return false;
const roles = Array.isArray(employee.roles) ? employee.roles.map((item) => `${item || ''}`.toLowerCase()) : [];
const permissions = Array.isArray(employee.permissions) ? employee.permissions : [];
return roles.includes('driver') || permissions.includes('isDriver');
};
const buildExternalDriverOptions = (records = []) => {
return (records || [])
.filter((record) => Array.isArray(record?.permissions) && record.permissions.includes('isDriver'))
.map((record) => ({
id: record?.external_user_id,
name: record?.name || record?.username || '',
name_cn: '',
title: record?.title || '',
employment_status: 'external',
license_type: '',
phone: '',
email: record?.email || '',
status: 'active'
}))
.filter((record) => record.id && record.name);
};
const scheduleDate = paramsQuery.get('dateSchedule');
const editSection = paramsQuery.get('editSection')
const hasUnsavedCustomerChanges = () => {
if (!initialCustomerListRef.current || editSection !== 'assignment') return false;
const current = JSON.stringify(
(newCustomerList || []).map(c => c.customer_id).sort()
);
return current !== initialCustomerListRef.current;
};
const confirmNavigate = (navigateFn) => {
if (hasUnsavedCustomerChanges()) {
if (window.confirm('You have unsaved changes to the customer assignment. Are you sure you want to leave without saving?')) {
navigateFn();
}
} else {
navigateFn();
}
};
// Warn on browser refresh / tab close
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedCustomerChanges()) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [newCustomerList, editSection]);
const redirectToView = () => {
const go = () => {
if (scheduleDate) {
navigate(`/trans-routes/${params.id}?dateSchedule=${scheduleDate}`);
} else {
navigate(`/trans-routes/${params.id}`);
}
};
confirmNavigate(go);
}
const redirectToDashboard = () => {
confirmNavigate(() => navigate(`/trans-routes/dashboard`));
}
const softDeleteCurrentRoute = () => {
if (!window.confirm('Are you sure you want to archive this route? This action cannot be undone.')) {
return;
}
const data = Object.assign({}, currentRoute, {status: ['disabled']})
dispatch(updateRoute({ id: currentRoute?.id, data, callback: redirectToDashboard }));
// redirectToDashboard();
}
const validateRoute = () => {
const errors = [];
// Required fields validation
if (!routeName || routeName.trim() === '') {
errors.push('Route Name');
}
if (!newRouteType || newRouteType === '') {
errors.push('Route Type');
}
if (!newDriver || newDriver === '') {
errors.push('Driver');
}
if (!newVehicle || newVehicle === '') {
errors.push('Vehicle');
}
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
return false;
}
return true;
};
const normalizeAddressText = (value) => {
return (value || '')
.toString()
.toLowerCase()
.replace(/\([^)]*\)/g, ' ')
.replace(/[^a-z0-9]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
};
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 getStructuredAddresses = (customerProfile) => {
if (!customerProfile) return [];
return [
formatStructuredAddress(customerProfile.address_line_1, customerProfile.address_line_2, customerProfile.city, customerProfile.state, customerProfile.zip_code, customerProfile.address_note),
formatStructuredAddress(customerProfile.address2_line_1, customerProfile.address2_line_2, customerProfile.city2, customerProfile.state2, customerProfile.zip_code2, customerProfile.address2_note),
formatStructuredAddress(customerProfile.address3_line_1, customerProfile.address3_line_2, customerProfile.city3, customerProfile.state3, customerProfile.zip_code3, customerProfile.address3_note),
formatStructuredAddress(customerProfile.address4_line_1, customerProfile.address4_line_2, customerProfile.city4, customerProfile.state4, customerProfile.zip_code4, customerProfile.address4_note)
].filter((addr) => (addr || '').trim() !== '');
};
const updateCurrentRoute = () => {
try {
if (!validateRoute()) {
return;
}
// Do not persist scheduled-absence entries into route_customer_list.
const filteredCustomerList = (newCustomerList || []).filter(
(customer) => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
);
const existingRouteCustomers = currentRoute?.route_customer_list || [];
const existingByCustomerId = new Map(existingRouteCustomers.map((c) => [c?.customer_id, c]));
const customerProfileById = new Map((allCustomers || []).map((c) => [c?.id, c]));
const stabilizedCustomerList = filteredCustomerList.map((customerItem) => {
const existingCustomer = existingByCustomerId.get(customerItem?.customer_id);
if (!existingCustomer?.customer_address) return customerItem;
const customerProfile = customerProfileById.get(customerItem?.customer_id);
const structuredAddresses = getStructuredAddresses(customerProfile);
if (structuredAddresses.length === 0) return customerItem;
const structuredAddressSet = new Set(structuredAddresses.map(normalizeAddressText).filter(Boolean));
const existingAddressNormalized = normalizeAddressText(existingCustomer.customer_address);
if (!existingAddressNormalized) return customerItem;
// Keep the route's current address if it no longer exists in DB structured addresses.
if (!structuredAddressSet.has(existingAddressNormalized)) {
return Object.assign({}, customerItem, { customer_address: existingCustomer.customer_address });
}
return customerItem;
});
let data = Object.assign({}, currentRoute, {name: routeName, driver: newDriver, vehicle: newVehicle, type: newRouteType, route_customer_list: stabilizedCustomerList});
if (estimatedStartTime && estimatedStartTime !== '') {
data = Object.assign({}, data, {estimated_start_time: combineDateAndTime(currentRoute.schedule_date, estimatedStartTime)})
}
let payload = { id: currentRoute?.id, data };
if ((historyRoutes.find(item => item.id === params.id)) || (scheduleDate && new Date(data.schedule_date) > new Date())) {
payload = Object.assign({}, payload, {dateText: data.schedule_date});
if (scheduleDate && new Date(data.schedule_date) > new Date()) {
payload = Object.assign({}, payload, {fromSchedule: true});
}
}
initialCustomerListRef.current = JSON.stringify(
(newCustomerList || []).map(c => c.customer_id).sort()
);
payload.callback = redirectToView;
dispatch(updateRoute(payload));
// TransRoutesService.updateInProgress(data);
// setTimeout(() => {
// redirectToView();
// }, 5000);
} catch(ex) {
}
}
const combineDateAndTime = (date, time) => {
const dateObj = moment(date);
const timeObj = moment(time, 'HH:mm');
dateObj.set({
hour: timeObj.get('hour'),
minute: timeObj.get('minute'),
second: timeObj.get('second')
})
return dateObj;
}
const calculateUnassignedCustomers = (customers, routes) => {
if (!customers || !routes) return [];
// Get all customer IDs that are assigned to any route today
const assignedCustomerIds = new Set();
routes.forEach(route => {
route.route_customer_list?.forEach(customer => {
assignedCustomerIds.add(customer.customer_id);
});
});
// Filter out customers that are not assigned to any route
// Also exclude discharged customers (type is 'discharged' or name contains 'discharged')
return customers.filter(customer => {
const isDischarged = customer.type === 'discharged' ||
(customer.name && customer.name.toLowerCase().includes('discharged')) ||
(customer.status !== 'active');
return customer.status === 'active' &&
!assignedCustomerIds.has(customer.id) &&
!isDischarged;
});
}
// Helper: check if a recurring rule has an occurrence on a specific date
const recurRuleFallsOnDate = (rule, targetDate) => {
const start = new Date(rule.start_repeat_date);
const end = new Date(rule.end_repeat_date);
const target = new Date(targetDate);
// Normalize to date-only comparison
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
target.setHours(0, 0, 0, 0);
if (target < start || target > end) return false;
const freq = rule.rrule;
let current = new Date(start);
let count = 0;
while (current <= target && count < 5000) {
if (current.getTime() === target.getTime()) return true;
if (freq === 'FREQ=DAILY') {
current = new Date(current.getTime() + 24 * 60 * 60 * 1000);
} else if (freq === 'FREQ=WEEKLY') {
current = new Date(current.getTime() + 7 * 24 * 60 * 60 * 1000);
} else if (freq === 'FREQ=MONTHLY') {
const next = new Date(current);
next.setMonth(next.getMonth() + 1);
current = next;
} else if (freq === 'FREQ=YEARLY') {
const next = new Date(current);
next.setFullYear(next.getFullYear() + 1);
current = next;
} else {
break;
}
count++;
}
return false;
};
useEffect(() => {
if (!AuthService.canAddOrEditRoutes()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.')
AuthService.logout();
navigate(`/login`);
}
TransRoutesService.getRoute(params.id).then(data => {
setCurrentRoute(data?.data);
setRouteName(data?.data?.name);
setNewDriver(data?.data?.driver);
setNewVehicle(data?.data?.vehicle);
setNewRouteType(data?.data?.type);
setEstimatedStartTime(data?.data?.estimated_start_time && new Date(data?.data?.estimated_start_time));
setNewCustomerList(data?.data?.route_customer_list);
initialCustomerListRef.current = JSON.stringify(
(data?.data?.route_customer_list || []).map(c => c.customer_id).sort()
);
setErrorMessage(undefined);
})
// Fetch all customers
CustomerService.getAllCustomers().then(data => {
setAllCustomers(data?.data || []);
});
Promise.all([
EmployeeService.getAllEmployees(),
EmployeeService.getExternalUserPermissionsList(EventsService.site)
]).then(([employeeResponse, externalPermissionResponse]) => {
const employees = employeeResponse?.data || [];
const internalDrivers = employees.filter(isDriverEligibleEmployee);
const externalDrivers = buildExternalDriverOptions(externalPermissionResponse?.data || []);
const mergedById = new Map();
[...internalDrivers, ...externalDrivers].forEach((driverItem) => {
if (!driverItem?.id) return;
if (!mergedById.has(driverItem.id)) {
mergedById.set(driverItem.id, driverItem);
}
});
setDriverOptions(Array.from(mergedById.values()));
});
}, []);
// Calculate unassigned customers when allCustomers or routes change
useEffect(() => {
if (!currentRoute?.schedule_date) return;
const routeDate = currentRoute.schedule_date;
// Get routes from the same date as the current route (excluding current route which we'll handle separately)
const sameDateRoutes = [
...allRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id),
...tomorrowRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id),
...historyRoutes.filter(route => route.schedule_date === routeDate && route.id !== currentRoute.id)
];
// Add a virtual route with the current newCustomerList (to include customers being added in the editor)
const routesWithCurrentEdits = [
...sameDateRoutes,
{ route_customer_list: newCustomerList || [] }
];
// Also exclude attendance-absent customers from unassigned list
const attendanceAbsentIds = new Set(attendanceAbsentCustomers.map(c => c.customer_id));
const unassigned = calculateUnassignedCustomers(allCustomers, routesWithCurrentEdits)
.filter(customer => !attendanceAbsentIds.has(customer.id));
setUnassignedCustomers(unassigned);
}, [allCustomers, allRoutes, tomorrowRoutes, historyRoutes, currentRoute, newCustomerList, attendanceAbsentCustomers]);
// Fetch attendance notes (single + recurring) for the route date
useEffect(() => {
if (!currentRoute?.schedule_date || !allCustomers?.length) return;
const routeDate = currentRoute.schedule_date; // MM/DD/YYYY
// Convert to YYYY-MM-DD for the API
const dateParts = routeDate.split('/');
if (dateParts.length !== 3) return;
const apiDate = `${dateParts[2]}-${dateParts[0].padStart(2, '0')}-${dateParts[1].padStart(2, '0')}`;
// Parse into a Date object for recurring rule checking
const routeDateObj = new Date(parseInt(dateParts[2]), parseInt(dateParts[0]) - 1, parseInt(dateParts[1]));
routeDateObj.setHours(0, 0, 0, 0);
// Fetch single attendance notes and recurring rules in parallel
Promise.all([
EventsService.getAllEvents({ date: apiDate, type: 'incident' }),
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
const absentCustomerIds = new Set();
const attendanceReasonsByCustomer = new Map();
const addAttendanceReason = (customerId, reasonText) => {
if (!customerId || !reasonText) return;
const cleaned = `${reasonText}`.trim();
if (!cleaned) return;
const existing = attendanceReasonsByCustomer.get(customerId) || [];
if (!existing.includes(cleaned)) {
attendanceReasonsByCustomer.set(customerId, [...existing, cleaned]);
}
};
const extractAttendanceReason = (item) => {
return item?.description || item?.data?.description || item?.data?.note || item?.title || '';
};
// Single attendance notes for this date
const singleNotes = (eventsRes?.data || []).filter(
e => e.type === 'incident' && e.status === 'active' && e.target_uuid
);
singleNotes.forEach(note => {
absentCustomerIds.add(note.target_uuid);
addAttendanceReason(note.target_uuid, extractAttendanceReason(note));
});
// Recurring attendance rules that fall on this date
const recurRules = (recurRes?.data || []).filter(
r => r.type === 'incident' && r.status === 'active' && r.target_uuid && r.rrule
);
recurRules.forEach(rule => {
if (recurRuleFallsOnDate(rule, routeDateObj)) {
absentCustomerIds.add(rule.target_uuid);
addAttendanceReason(rule.target_uuid, extractAttendanceReason(rule));
}
});
// Build the list of absent customer objects
const absentList = [];
absentCustomerIds.forEach(customerId => {
const customer = allCustomers.find(c => c.id === customerId);
if (customer) {
const reasons = attendanceReasonsByCustomer.get(customer.id) || [];
absentList.push({
customer_id: customer.id,
customer_name: customer.name,
customer_address: customer.address1 || '',
customer_route_status: PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT,
customer_pickup_status: 'scheduleAbsent',
attendance_note: reasons.join('; '),
_attendance_based: true // flag to identify these are from attendance notes
});
}
});
setAttendanceAbsentCustomers(absentList);
});
}, [currentRoute?.schedule_date, allCustomers]);
// useEffect(() => {
// if (currentRoute) {
// setRouteName(currentRoute.name);
// setNewDriver(currentRoute.driver);
// setNewVehicle(currentRoute.vehicle);
// setNewRouteType(currentRoute.type);
// setEstimatedStartTime(currentRoute.estimated_start_time && new Date(currentRoute.estimated_start_time));
// setNewCustomerList(currentRoute.route_customer_list);
// }
// setErrorMessage(undefined);
// }, [currentRoute])
// Draggable component for unassigned customers
const DraggableUnassignedCustomer = ({ customer }) => {
const [{ isDragging }, drag] = useDrag({
type: 'UNASSIGNED_CUSTOMER',
item: () => customer,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0.5 : 1;
return (
<div ref={drag} style={{ opacity }} className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={20}></GripVertical>
<div className="customer-dnd-item">
<span>{customer.name} </span>
<small className="me-2">{customer.address1}</small>
<small className="me-2">{customer.type}</small>
</div>
</div>
);
};
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item href="/trans-routes/dashboard">
Transportation Routes
</Breadcrumb.Item>
<Breadcrumb.Item active>
Edit Route
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
Edit Route Information <button className="btn btn-link btn-sm" onClick={() => {redirectToView()}}>Back</button>
</h4>
</div>
</div>
<div className="app-main-content-list-container form-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="routeOverview" id="route-view-tab">
<Tab eventKey="routeOverview" title="Route Information">
{ editSection === 'info' && <div className="multi-columns-container">
<div className="column-container">
<div className="column-card">
<h6 className="text-primary">Route Details</h6>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Route Name
<span className="required">*</span>
</div>
<input type="text" value={routeName || ''} onChange={e => setRouteName(e.target.value)}/>
</div>
<div className="me-4">
<div className="field-label">Vehicle
<span className="required">*</span>
</div>
<select value={newVehicle} onChange={e => setNewVehicle(e.target.value)}>
{vehicles.map((vehicle) => (<option key={vehicle.id} value={vehicle.id}>{vehicle.vehicle_number}</option>))}
</select>
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Driver
<span className="required">*</span>
</div>
<select value={newDriver} onChange={e => setNewDriver(e.target.value)}>
{driverOptions.map((driver) => <option key={driver.id} value={driver.id}>{driver.name}</option>)}
</select>
</div>
<div className="me-4">
<div className="field-label">Route Type
<span className="required">*</span>
</div>
<select value={newRouteType} onChange={e => setNewRouteType(e.target.value)}>
<option value="inbound">Inbound</option>
<option value="outbound">Outbound</option>
</select>
</div>
</div>
{ newRouteType === 'outbound' && <div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Estimated Start Time
</div>
<TimePicker disableClock={true} format={'HH:mm'} value={estimatedStartTime} onChange={setEstimatedStartTime} />
</div>
</div>}
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Vehicle Checklist
</div>
{ vehicles.find(item => item.id === newVehicle)?.checklist?.length > 0 && (<table className="mb-4">
<tbody>
{vehicles.find(item => item.id === newVehicle)?.checklist?.map((item, index) => (<tr key={index}><td>{item}</td></tr>))}
</tbody>
</table>) }
</div>
</div>
<div className="list row mb-5">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectToView()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => updateCurrentRoute()}> Save </button>
</div>
{errorMessage && <div className="col-md-12 col-sm-12 col-xs-12 alert alert-danger mt-4">{errorMessage}</div>}
</div>
</div>
</div>
<div className="column-container">
{ newVehicle && newVehicle !== '' && <div className="column-card mb-4">
<h6 className="text-primary">Vehicle Information</h6>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Vehicle Number</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.vehicle_number}</div>
</div>
<div className="field-body">
<div className="field-label">Seating Capacity</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.capacity}</div>
</div>
<div className="field-body">
<div className="field-label">Mileage</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.mileage}</div>
</div>
<div className="field-body">
<div className="field-label">Make</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.make}</div>
</div>
<div className="field-body">
<div className="field-label">Model</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.model}</div>
</div>
</div>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">License Plate</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.tag}</div>
</div>
<div className="field-body">
<div className="field-label">Year</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.year}</div>
</div>
<div className="field-body">
<div className="field-label">GPS ID</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.gps_tag}</div>
</div>
<div className="field-body">
<div className="field-label">EZPass</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.ezpass}</div>
</div>
<div className="field-body">
<div className="field-label">Vin</div>
<div className="field-value">{vehicles.find(item => item.id === newVehicle)?.vin || ''}</div>
</div>
</div>
</div>}
{
newDriver && newDriver !== '' && <div className="column-card">
<h6 className="text-primary">Driver Information</h6>
<small className="text-primary">Personal Details</small>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Driver Name</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.name}</div>
</div>
<div className="field-body">
<div className="field-label">Preferred Name</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.name_cn}</div>
</div>
<div className="field-body">
<div className="field-label">Job Title</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.title}</div>
</div>
<div className="field-body">
<div className="field-label">Job Type</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.employment_status}</div>
</div>
<div className="field-body">
<div className="field-label">License Type</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.license_type}</div>
</div>
</div>
<div className="app-main-content-fields-section short">
<div className="field-body">
<div className="field-label">Phone Number</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.phone}</div>
</div>
<div className="field-body">
<div className="field-label">Email</div>
<div className="field-value">{driverOptions.find(item => item.id === newDriver)?.email}</div>
</div>
<div className="field-body"></div>
<div className="field-body"></div>
<div className="field-body"></div>
</div>
</div>
}
</div>
</div> }
{
editSection === 'assignment' && <DndProvider backend={HTML5Backend}>
<div className="multi-columns-container">
<div className="column-container">
<div className="column-card adjust" style={{paddingRight: '30px'}}>
<div className="col-md-12 mb-4">
{(() => {
const existingScheduledAbsentIds = (currentRoute?.route_customer_list || [])
.filter((customer) => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)
.map((customer) => customer?.customer_id)
.filter(Boolean);
const attendanceBasedAbsentIds = (attendanceAbsentCustomers || [])
.map((customer) => customer?.customer_id)
.filter(Boolean);
const mergedAbsentIds = Array.from(new Set([].concat(existingScheduledAbsentIds, attendanceBasedAbsentIds)));
return (
<RouteCustomerEditor
currentRoute={currentRoute ? {
...currentRoute,
route_customer_list: currentRoute.route_customer_list?.filter(
customer => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
) || []
} : undefined}
setNewCustomerList={setNewCustomerList}
onAddCustomer={(addFn) => setAddCustomerToRoute(() => addFn)}
scheduledAbsentCustomerIds={mergedAbsentIds}
/>
);
})()}
</div>
</div>
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Scheduled Absences ({(() => {
const existingAbsent = currentRoute?.route_customer_list?.filter(item => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) || [];
const existingIds = new Set(existingAbsent.map(a => a.customer_id));
const attendanceOnly = attendanceAbsentCustomers.filter(a => !existingIds.has(a.customer_id));
return existingAbsent.length + attendanceOnly.length;
})()})</h6>
<div className="customers-container mb-4">
{
// Existing scheduled absences from route data
currentRoute?.route_customer_list?.filter(customer => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.map((abItem) => {
return <div key={abItem.customer_id} className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={20}></GripVertical>
<div className="customer-dnd-item">
<span>{abItem.customer_name} </span>
<small className="me-2">{abItem.customer_address}</small>
<small className="me-2">{abItem.customer_pickup_status}</small>
</div>
</div>
})
}
{
// Attendance-based absences (not already in existing scheduled absences)
(() => {
const existingIds = new Set(
(currentRoute?.route_customer_list?.filter(c => c?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) || [])
.map(a => a.customer_id)
);
return attendanceAbsentCustomers
.filter(a => !existingIds.has(a.customer_id))
.map((abItem) => (
<div key={`att-${abItem.customer_id}`} className="customers-dnd-item-container-absent" style={{ opacity: 0.85 }}>
<GripVertical className="me-4" size={20}></GripVertical>
<div className="customer-dnd-item">
<span>{abItem.customer_name} </span>
<small className="me-2">{abItem.customer_address}</small>
{!!abItem.attendance_note && <small className="me-2 text-muted">({abItem.attendance_note})</small>}
</div>
</div>
));
})()
}
</div>
</div>
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Unassigned Customers ({unassignedCustomers?.length || 0})</h6>
<div className="customers-container mb-4">
{
unassignedCustomers?.map((customer) => {
return <DraggableUnassignedCustomer key={customer.id} customer={customer} />
})
}
</div>
</div>
</div>
</div>
<div className="list row mb-5 mt-4">
<div className="col-md-12 col-sm-12 col-xs-12">
<button className="btn btn-default btn-sm float-right" onClick={() => redirectToView()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => updateCurrentRoute()}> Save </button>
</div>
{errorMessage && <div className="col-md-12 col-sm-12 col-xs-12 alert alert-danger mt-4">{errorMessage}</div>}
</div>
</DndProvider>
}
</Tab>
</Tabs>
<div className="list-func-panel">
<button className="btn btn-primary" onClick={() => softDeleteCurrentRoute()}><Archive size={16} className="me-2"></Archive>Delete</button>
</div>
</div>
</div>
</>
);
};
export default RouteEdit;