All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 30s
776 lines
37 KiB
JavaScript
776 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;
|
|
}
|
|
// Keep scheduled-absence customers in route assignment payload; only skip transient
|
|
// attendance-derived placeholder rows if any are ever injected into this list.
|
|
const filteredCustomerList = (newCustomerList || []).filter(
|
|
(customer) => customer?._attendance_based !== true
|
|
);
|
|
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)});
|
|
} else {
|
|
// Explicitly clear persisted value when user removes estimated start time.
|
|
data = Object.assign({}, data, {estimated_start_time: null});
|
|
}
|
|
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}
|
|
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; |