Files
worldshine-redesign/client/src/components/trans-routes/RoutesDashboard.js
Lixian Zhou 39e00c7765
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 52s
fix
2026-03-11 13:35:05 -04:00

2263 lines
101 KiB
JavaScript

import React, { useState, useEffect, useRef, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate, useSearchParams } from "react-router-dom";
import { selectInboundRoutes, selectTomorrowAllRoutes, selectTomorrowInboundRoutes, selectTomorrowOutboundRoutes, selectHistoryInboundRoutes, selectHistoryRoutes, selectHistoryOutboundRoutes, selectOutboundRoutes, selectAllRoutes, selectAllActiveVehicles, selectAllActiveDrivers, transRoutesSlice } from "./../../store";
import RoutesSection from "./RoutesSection";
import PersonnelSection from "./PersonnelSection";
import { AuthService, CustomerService, TransRoutesService, DriverService, EventsService, DailyRoutesTemplateService } from "../../services";
import { PERSONAL_ROUTE_STATUS, ROUTE_STATUS, reportRootUrl, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE } from "../../shared";
import moment from 'moment';
import DatePicker from "react-datepicker";
import { CalendarWeek, ClockHistory, Copy, Download, Eraser, Plus, Clock, Filter, CalendarCheck, Check } from "react-bootstrap-icons";
import { Breadcrumb, Tabs, Tab, Dropdown, Spinner, Modal, Button, ProgressBar } from "react-bootstrap";
import RouteCustomerTable from "./RouteCustomerTable";
const RoutesDashboard = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { fetchAllRoutes, createRoute, fetchAllHistoryRoutes, fetchAllTomorrowRoutes, updateRoute } = transRoutesSlice.actions;
const inboundRoutes = useSelector(selectInboundRoutes);
const outboundRoutes = useSelector(selectOutboundRoutes);
const tmrInboundRoutes = useSelector(selectTomorrowInboundRoutes);
const tmrOutboundRoutes = useSelector(selectTomorrowOutboundRoutes);
const historyInboundRoutes = useSelector(selectHistoryInboundRoutes);
const historyOutboundRoutes = useSelector(selectHistoryOutboundRoutes);
const allRoutes = useSelector(selectAllRoutes);
const allHistoryRoutes = useSelector(selectHistoryRoutes);
const allTomorrowRoutes = useSelector(selectTomorrowAllRoutes);
const drivers = useSelector(selectAllActiveDrivers);
const vehicles = useSelector(selectAllActiveVehicles);
const [showSyncCustomersLoading, setShowSyncCustomersLoading] = useState(false);
const [keyword, setKeyword] = useState('');
const [directorSignature, setDirectorSignature] = useState(undefined);
const [selectedFile, setSelectedFile] = useState();
const [selectedDriver, setSelectedDriver] = useState(undefined);
const [driverList, setDriverList] = useState([]);
const [customers, setCustomers] = useState([]);
const [dateSelected, setDateSelected] = useState(new Date());
const [routesForSignature, setRoutesForSignature] = useState(allRoutes);
const [currentTab, setCurrentTab] = useState('allRoutesOverview');
const [sorting, setSorting] = useState({key: '', order: ''});
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
const [showDateDropdown, setShowDateDropdown] = useState(false);
const [routesForShowing, setRoutesForShowing] = useState(allRoutes);
const [routesInboundForShowing, setRoutesInboundForShowing] = useState(inboundRoutes);
const [routesOutboundForShowing, setRoutesOutboundForShowing] = useState(outboundRoutes);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(undefined);
const [successMessage, setSuccessMessage] = useState(undefined);
const [showCopyTodayLoading, setShowCopyTodayLoading] = useState(false);
const [showCopyDateLoading, setShowCopyDateLoading] = useState(false);
const [showCopyDateTargetLoading, setShowCopyDateTargetLoading] = useState(false);
const [copyDisabled, setCopyDisabled] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteRouteType, setDeleteRouteType] = useState('');
const [targetedDateSelected, setTargetedDateSelected] = useState(undefined);
const [originDateSelected, setOriginDateSelected] = useState(undefined);
const [showOriginDateDropdown, setShowOriginDateDropdown] = useState(false);
const [showTargetDateDropdown, setShowTargetDateDropdown] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [showFilterReportDropdown, setShowFilterReportDropdown] = useState(false);
const [statusFilter, setStatusFilter] = useState('');
const [customerTypeFilter, setCustomerTypeFilter] = useState('');
const [customerNameFilter, setCustomerNameFilter] = useState('');
const [customerTableId, setCustomerTableId] = useState('');
const [routeTypeFilter, setRouteTypeFilter] = useState('');
// Save template modal state
const [showSaveTemplateModal, setShowSaveTemplateModal] = useState(false);
const [templateName, setTemplateName] = useState('');
const [savingTemplate, setSavingTemplate] = useState(false);
// Apply template state
const [templates, setTemplates] = useState([]);
const [selectedTemplateId, setSelectedTemplateId] = useState('');
const [applyingTemplate, setApplyingTemplate] = useState(false);
const [pageLoading, setPageLoading] = useState(false);
const [showImportDateDropdown, setShowImportDateDropdown] = useState(false);
const [showImportTemplateDropdown, setShowImportTemplateDropdown] = useState(false);
const [importSourceDate, setImportSourceDate] = useState(undefined);
const [importTemplateId, setImportTemplateId] = useState('');
const [isScheduleImporting, setIsScheduleImporting] = useState(false);
const [scheduleImportProgress, setScheduleImportProgress] = useState(0);
const [scheduleImportLabel, setScheduleImportLabel] = useState('');
const [showCheckRoutesModal, setShowCheckRoutesModal] = useState(false);
const [checkRoutesResult, setCheckRoutesResult] = useState({ inbound: [], outbound: [], attendance: [], addressMismatch: [], customerTypeMismatch: [], customerSpecialNeedsMismatch: [], customerNoteMismatch: [], dischargedOnRoute: [] });
const [customerTypeFixing, setCustomerTypeFixing] = useState({});
const [customerSpecialNeedsFixing, setCustomerSpecialNeedsFixing] = useState({});
const [customerNoteFixing, setCustomerNoteFixing] = useState({});
const scheduleImportProgressTimerRef = useRef(null);
const params = new URLSearchParams(window.location.search);
const scheduleDate = params.get('dateSchedule');
const getDateString = (date) => {
return ((date.getMonth() > 8) ? (date.getMonth() + 1) : ('0' + (date.getMonth() + 1))) + '/' + ((date.getDate() > 9) ? date.getDate() : ('0' + date.getDate())) + '/' + date.getFullYear()
}
const normalizeName = (name) => (name || '').toString().trim().toLowerCase();
const normalizeAddress = (address) => {
return (address || '')
.toString()
.toLowerCase()
.replace(/\([^)]*\)/g, ' ')
.replace(/[^a-z0-9]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
};
// Match route addresses against stored structured addresses without state/zip sensitivity.
const normalizeAddressForMismatchCheck = (address) => {
return normalizeAddress(address)
.replace(/\b\d{5}(?:\s*\d{4})?\b/g, ' ')
.replace(/\b(maryland|md|virginia|va)\b/g, ' ')
// Normalize unit identifiers like "Apt C-1" / "Apt C 1" to "Apt C1".
.replace(/\b(apt|apartment|unit|suite|ste)\s+([a-z])\s+(\d+)\b/g, '$1 $2$3')
.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 getStoredCustomerAddresses = (customerProfile) => {
if (!customerProfile) return [];
const structured = [
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)
];
return structured.filter((item) => (item || '').toString().trim() !== '');
};
const joinRouteNames = (routeNames = []) => {
if (routeNames.length <= 1) return routeNames[0] || '';
if (routeNames.length === 2) return `${routeNames[0]} and ${routeNames[1]}`;
return `${routeNames.slice(0, -1).join(', ')}, and ${routeNames[routeNames.length - 1]}`;
};
const formatRouteAddressList = (routes = []) => {
const parts = (routes || []).map((route) => `Route ${route.routeName}(address: ${route.customerAddress || 'N/A'})`);
if (parts.length <= 1) return parts[0] || '';
if (parts.length === 2) return `${parts[0]}, ${parts[1]}`;
return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
};
const getCustomerTypeLabel = (type) => CUSTOMER_TYPE_TEXT[type] || type || 'N/A';
const getCustomerKey = (customer) => {
return customer?.customer_id || customer?.id || normalizeName(customer?.customer_name || customer?.name);
};
const getCustomerTimeKey = (route, customer) => {
const candidates = [
customer?.customer_estimated_pickup_time,
customer?.customer_pickup_time,
customer?.customer_estimated_dropoff_time,
customer?.customer_dropoff_time,
route?.estimated_start_time,
route?.start_time
];
for (const value of candidates) {
if (!value) continue;
const parsed = moment(value, [
moment.ISO_8601,
'MM/DD/YYYY HH:mm',
'MM/DD/YYYY hh:mm A',
'HH:mm',
'hh:mm A'
], true);
if (parsed.isValid()) {
return parsed.format('HH:mm');
}
const fallback = moment(value);
if (fallback.isValid()) {
return fallback.format('HH:mm');
}
}
return null;
};
const buildRouteConflictsByDirection = (routes = []) => {
const byCustomer = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customer) => {
const customerKey = customer?.customer_id || normalizeName(customer?.customer_name);
if (!customerKey) return;
const record = {
customerKey,
customerName: customer?.customer_name || 'Unknown',
customerAddress: customer?.customer_address_override || customer?.customer_address || '',
routeId: route?.id || route?._id || '',
routeName: route?.name || 'Unnamed Route'
};
const existing = byCustomer.get(customerKey) || [];
byCustomer.set(customerKey, [...existing, record]);
});
});
const issues = [];
byCustomer.forEach((records) => {
const uniqueRoutes = Array.from(
new Map(
records.map((item) => [item.routeId, { routeId: item.routeId, routeName: item.routeName, customerAddress: item.customerAddress || '' }])
).values()
);
if (uniqueRoutes.length < 2) return;
issues.push({
customerName: records[0]?.customerName || 'Unknown',
routes: uniqueRoutes
});
});
return issues;
};
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);
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 += 1;
}
return false;
};
const getRoutesByCustomerForCheck = (routes = []) => {
const map = new Map();
routes.forEach((route) => {
(route?.route_customer_list || []).forEach((customer) => {
const key = getCustomerKey(customer);
if (!key) return;
const existing = map.get(key) || {
customerName: customer?.customer_name || customer?.name || 'Unknown',
routeNames: new Set()
};
existing.routeNames.add(route?.name || 'Unnamed Route');
map.set(key, existing);
});
});
return map;
};
const buildAttendanceNoteRouteIssues = async () => {
const dateText = moment(dateSelected).format('YYYY-MM-DD');
const noteDateLabel = moment(dateSelected).format('MM/DD/YYYY');
const [eventsRes, recurRes] = await Promise.all([
EventsService.getAllEvents({ date: dateText, type: 'incident' }),
EventsService.getAllEventRecurrences()
]);
const notes = [];
const singleNotes = (eventsRes?.data || []).filter(
(event) => event?.type === 'incident' && event?.status === 'active' && event?.target_uuid
);
singleNotes.forEach((note) => {
notes.push({
customerKey: note.target_uuid,
noteDate: note?.start_time ? moment(note.start_time).format('MM/DD/YYYY') : noteDateLabel
});
});
const recurringNotes = (recurRes?.data || []).filter(
(rule) => rule?.type === 'incident' && rule?.status === 'active' && rule?.target_uuid && rule?.rrule
);
recurringNotes.forEach((rule) => {
if (recurRuleFallsOnDate(rule, dateSelected)) {
notes.push({
customerKey: rule.target_uuid,
noteDate: noteDateLabel
});
}
});
const allFutureRoutes = [...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])];
const routesByCustomer = getRoutesByCustomerForCheck(allFutureRoutes);
const dedupe = new Set();
const issues = [];
notes.forEach((note) => {
const routeInfo = routesByCustomer.get(note.customerKey);
if (!routeInfo || routeInfo.routeNames.size === 0) return;
const dedupeKey = `${note.customerKey}|${note.noteDate}|${Array.from(routeInfo.routeNames).sort().join('|')}`;
if (dedupe.has(dedupeKey)) return;
dedupe.add(dedupeKey);
issues.push({
customerName: routeInfo.customerName,
noteDate: note.noteDate,
routeNames: Array.from(routeInfo.routeNames)
});
});
return issues;
};
const buildAddressMismatchIssues = (routes = []) => {
const customerProfileById = new Map((customers || []).map((customer) => [customer.id, customer]));
const mismatchMap = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customerInRoute) => {
const customerId = customerInRoute?.customer_id;
if (!customerId) return;
const customerProfile = customerProfileById.get(customerId);
if (!customerProfile) return;
const routeAddress = customerInRoute?.customer_address_override || customerInRoute?.customer_address || '';
const normalizedRouteAddress = normalizeAddressForMismatchCheck(routeAddress);
if (!normalizedRouteAddress) return;
const storedAddressSet = new Set(
getStoredCustomerAddresses(customerProfile)
.map((address) => normalizeAddressForMismatchCheck(address))
.filter(Boolean)
);
if (storedAddressSet.size === 0) return;
if (storedAddressSet.has(normalizedRouteAddress)) return;
const key = `${customerId}|${normalizedRouteAddress}`;
const existing = mismatchMap.get(key) || {
customerName: customerInRoute?.customer_name || customerProfile?.name || 'Unknown',
routeAddress,
routeNames: new Set()
};
existing.routeNames.add(route?.name || 'Unnamed Route');
mismatchMap.set(key, existing);
});
});
return Array.from(mismatchMap.values()).map((item) => ({
customerName: item.customerName,
routeAddress: item.routeAddress,
routeNames: Array.from(item.routeNames)
}));
};
const buildCustomerTypeMismatchIssues = (routes = []) => {
const customerProfileById = new Map((customers || []).map((customer) => [customer.id, customer]));
const mismatchMap = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customerInRoute) => {
const customerId = customerInRoute?.customer_id;
if (!customerId) return;
const customerProfile = customerProfileById.get(customerId);
if (!customerProfile) return;
const routeType = `${customerInRoute?.customer_type || ''}`.trim();
const dbType = `${customerProfile?.type || ''}`.trim();
if (!dbType) return;
if (routeType.toLowerCase() === dbType.toLowerCase()) return;
const existing = mismatchMap.get(customerId) || {
customerId,
customerName: customerInRoute?.customer_name || customerProfile?.name || 'Unknown',
dbType,
mismatchedRoutes: []
};
existing.mismatchedRoutes.push({
routeId: route?.id,
routeName: route?.name || 'Unnamed Route',
routeType: routeType || 'N/A'
});
mismatchMap.set(customerId, existing);
});
});
return Array.from(mismatchMap.values()).map((item) => ({
...item,
mismatchedRoutes: item.mismatchedRoutes.filter((route, idx, arr) => arr.findIndex((r) => r.routeId === route.routeId) === idx)
}));
};
const buildDischargedCustomerRouteIssues = (routes = []) => {
const customerProfileById = new Map((customers || []).map((customer) => [customer.id, customer]));
const issuesMap = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customerInRoute) => {
const customerId = customerInRoute?.customer_id;
if (!customerId) return;
const customerProfile = customerProfileById.get(customerId);
if (!customerProfile) return;
if (`${customerProfile?.type || ''}` !== 'discharged') return;
const existing = issuesMap.get(customerId) || {
customerName: customerInRoute?.customer_name || customerProfile?.name || 'Unknown',
routeNames: new Set()
};
existing.routeNames.add(route?.name || 'Unnamed Route');
issuesMap.set(customerId, existing);
});
});
return Array.from(issuesMap.values()).map((issue) => ({
customerName: issue.customerName,
routeNames: Array.from(issue.routeNames)
}));
};
const buildCustomerSpecialNeedsMismatchIssues = (routes = []) => {
const customerProfileById = new Map((customers || []).map((customer) => [customer.id, customer]));
const mismatchMap = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customerInRoute) => {
const customerId = customerInRoute?.customer_id;
if (!customerId) return;
const customerProfile = customerProfileById.get(customerId);
if (!customerProfile) return;
const routeNote = `${customerInRoute?.customer_special_needs || ''}`.trim();
const dbNoteToDriver = `${customerProfile?.notes_for_driver || ''}`.trim();
if (routeNote === dbNoteToDriver) return;
const existing = mismatchMap.get(customerId) || {
customerId,
customerName: customerInRoute?.customer_name || customerProfile?.name || 'Unknown',
dbNoteToDriver,
mismatchedRoutes: []
};
existing.mismatchedRoutes.push({
routeId: route?.id,
routeName: route?.name || 'Unnamed Route',
routeNote: routeNote || ''
});
mismatchMap.set(customerId, existing);
});
});
return Array.from(mismatchMap.values()).map((item) => ({
...item,
mismatchedRoutes: item.mismatchedRoutes.filter((route, idx, arr) => arr.findIndex((r) => r.routeId === route.routeId) === idx)
}));
};
const buildCustomerNoteMismatchIssues = (routes = []) => {
const customerProfileById = new Map((customers || []).map((customer) => [customer.id, customer]));
const mismatchMap = new Map();
(routes || []).forEach((route) => {
(route?.route_customer_list || []).forEach((customerInRoute) => {
const customerId = customerInRoute?.customer_id;
if (!customerId) return;
const customerProfile = customerProfileById.get(customerId);
if (!customerProfile) return;
const normalizeNullableNote = (value) => {
const normalized = `${value || ''}`.trim().toLowerCase();
if (normalized === 'null') return '';
return `${value || ''}`.trim();
};
const routeNote = normalizeNullableNote(customerInRoute?.customer_note);
const dbNoteToDriver = normalizeNullableNote(customerProfile?.notes_for_driver);
if (routeNote === dbNoteToDriver) return;
const existing = mismatchMap.get(customerId) || {
customerId,
customerName: customerInRoute?.customer_name || customerProfile?.name || 'Unknown',
dbNoteToDriver,
mismatchedRoutes: []
};
existing.mismatchedRoutes.push({
routeId: route?.id,
routeName: route?.name || 'Unnamed Route',
routeNote: routeNote || ''
});
mismatchMap.set(customerId, existing);
});
});
return Array.from(mismatchMap.values()).map((item) => ({
...item,
mismatchedRoutes: item.mismatchedRoutes.filter((route, idx, arr) => arr.findIndex((r) => r.routeId === route.routeId) === idx)
}));
};
const syncCustomerTypeForMismatch = async (issue) => {
if (!issue?.customerId || !issue?.dbType || !issue?.mismatchedRoutes?.length) return;
setCustomerTypeFixing((prev) => Object.assign({}, prev, { [issue.customerId]: true }));
try {
const routeMap = new Map([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])].map((route) => [route.id, route]));
const updatePromises = issue.mismatchedRoutes.map((routeMeta) => {
const route = routeMap.get(routeMeta.routeId);
if (!route) return Promise.resolve();
const nextCustomerList = (route.route_customer_list || []).map((customer) => {
if (customer?.customer_id !== issue.customerId) return customer;
return Object.assign({}, customer, { customer_type: issue.dbType });
});
return TransRoutesService.updateRoute(route.id, Object.assign({}, route, { route_customer_list: nextCustomerList }));
});
await Promise.all(updatePromises);
setSuccessMessage(`Synced member type for ${issue.customerName}.`);
setTimeout(() => setSuccessMessage(undefined), 5000);
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
await runCheckRoutes();
} catch (error) {
console.error('Error syncing customer type mismatch:', error);
setErrorMessage(`Failed to sync member type for ${issue.customerName}.`);
setTimeout(() => setErrorMessage(undefined), 5000);
} finally {
setCustomerTypeFixing((prev) => Object.assign({}, prev, { [issue.customerId]: false }));
}
};
const syncCustomerSpecialNeedsForMismatch = async (issue) => {
if (!issue?.customerId || !issue?.mismatchedRoutes?.length) return;
setCustomerSpecialNeedsFixing((prev) => Object.assign({}, prev, { [issue.customerId]: true }));
try {
const routeMap = new Map([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])].map((route) => [route.id, route]));
const updatePromises = issue.mismatchedRoutes.map((routeMeta) => {
const route = routeMap.get(routeMeta.routeId);
if (!route) return Promise.resolve();
const nextCustomerList = (route.route_customer_list || []).map((customer) => {
if (customer?.customer_id !== issue.customerId) return customer;
return Object.assign({}, customer, { customer_special_needs: issue.dbNoteToDriver || '' });
});
return TransRoutesService.updateRoute(route.id, Object.assign({}, route, { route_customer_list: nextCustomerList }));
});
await Promise.all(updatePromises);
setSuccessMessage(`Synced note to driver for ${issue.customerName}.`);
setTimeout(() => setSuccessMessage(undefined), 5000);
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
await runCheckRoutes();
} catch (error) {
console.error('Error syncing note to driver mismatch:', error);
setErrorMessage(`Failed to sync note to driver for ${issue.customerName}.`);
setTimeout(() => setErrorMessage(undefined), 5000);
} finally {
setCustomerSpecialNeedsFixing((prev) => Object.assign({}, prev, { [issue.customerId]: false }));
}
};
const syncCustomerNoteForMismatch = async (issue) => {
if (!issue?.customerId || !issue?.mismatchedRoutes?.length) return;
setCustomerNoteFixing((prev) => Object.assign({}, prev, { [issue.customerId]: true }));
try {
const routeMap = new Map([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])].map((route) => [route.id, route]));
const updatePromises = issue.mismatchedRoutes.map((routeMeta) => {
const route = routeMap.get(routeMeta.routeId);
if (!route) return Promise.resolve();
const nextCustomerList = (route.route_customer_list || []).map((customer) => {
if (customer?.customer_id !== issue.customerId) return customer;
return Object.assign({}, customer, { customer_note: issue.dbNoteToDriver || '' });
});
return TransRoutesService.updateRoute(route.id, Object.assign({}, route, { route_customer_list: nextCustomerList }));
});
await Promise.all(updatePromises);
setSuccessMessage(`Synced route note for ${issue.customerName}.`);
setTimeout(() => setSuccessMessage(undefined), 5000);
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
await runCheckRoutes();
} catch (error) {
console.error('Error syncing customer note mismatch:', error);
setErrorMessage(`Failed to sync route note for ${issue.customerName}.`);
setTimeout(() => setErrorMessage(undefined), 5000);
} finally {
setCustomerNoteFixing((prev) => Object.assign({}, prev, { [issue.customerId]: false }));
}
};
const runCheckRoutes = async () => {
const inboundIssues = buildRouteConflictsByDirection(tmrInboundRoutes || []);
const outboundIssues = buildRouteConflictsByDirection(tmrOutboundRoutes || []);
const addressMismatchIssues = buildAddressMismatchIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
const customerTypeMismatchIssues = buildCustomerTypeMismatchIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
const customerSpecialNeedsMismatchIssues = buildCustomerSpecialNeedsMismatchIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
const customerNoteMismatchIssues = buildCustomerNoteMismatchIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
const dischargedOnRouteIssues = buildDischargedCustomerRouteIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
let attendanceIssues = [];
try {
attendanceIssues = await buildAttendanceNoteRouteIssues();
} catch (error) {
console.error('Error checking attendance notes against routes:', error);
}
setCheckRoutesResult({ inbound: inboundIssues, outbound: outboundIssues, attendance: attendanceIssues, addressMismatch: addressMismatchIssues, customerTypeMismatch: customerTypeMismatchIssues, customerSpecialNeedsMismatch: customerSpecialNeedsMismatchIssues, customerNoteMismatch: customerNoteMismatchIssues, dischargedOnRoute: dischargedOnRouteIssues });
setShowCheckRoutesModal(true);
};
const processRoutesForAbsentCustomers = useCallback((inboundRoutes, outboundRoutes) => {
// Get customers who are absent in inbound routes
const absentCustomerIds = new Set();
inboundRoutes.forEach(inboundRoute => {
inboundRoute.route_customer_list?.forEach(customer => {
if (customer.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT ||
customer.customer_route_status === PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT) {
absentCustomerIds.add(customer.customer_id);
}
});
});
// Remove absent customers from outbound routes for display
const processedOutboundRoutes = outboundRoutes.map(outboundRoute => ({
...outboundRoute,
route_customer_list: outboundRoute.route_customer_list?.filter(customer =>
!absentCustomerIds.has(customer.customer_id)
) || []
}));
return processedOutboundRoutes;
}, [])
useEffect(() => {
setPageLoading(true);
if (scheduleDate) {
const [year, month, day] = scheduleDate?.split('-').map(Number);
setDateSelected(new Date(year, month-1, day));
} else {
setDateSelected(new Date())
}
const site = EventsService.site;
Promise.all([
DriverService.getAllActiveDrivers('driver', 'active').then((data) => {
setDriverList(data.data);
}),
CustomerService.getAllCustomers().then((data) => setCustomers(data?.data)),
CustomerService.getAvatar(`center_director_signature_site_${site}`).then(data => {
if (data?.data) {
setDirectorSignature(data?.data)
}
}).catch(() => {}),
DailyRoutesTemplateService.getAll().then((response) => {
setTemplates(response.data || []);
}).catch(err => {
console.error('Error fetching templates:', err);
})
]).finally(() => {
setPageLoading(false);
});
}, []);
useEffect(() => {
TransRoutesService.getAll(moment(dateSelected)?.format('MM/DD/YYYY')).then(data => {
const routesResults = data.data;
const finalRoutes = routesResults.map(async (routeItem) => {
const dateArr = moment(dateSelected)?.format('MM/DD/YYYY')?.split('/') || [];
try {
const result = await CustomerService.getAvatar(`${routeItem.id}_${routeItem.driver}_${dateArr[0]}_${dateArr[1]}`);
return result?.data ? Object.assign({}, routeItem, {signature: result?.data}) : routeItem;
} catch (ex) {
return routeItem;
}
});
Promise.all(finalRoutes).then(finalRoutesData => {
setRoutesForSignature(finalRoutesData);
})
});
const selectedDateString = getDateString(dateSelected);
if (selectedDateString === getDateString(new Date())) {
dispatch(fetchAllRoutes());
} else {
if (dateSelected > new Date()) {
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
} else {
dispatch(fetchAllHistoryRoutes({dateText: getDateString(dateSelected)}))
}
}
}, [dateSelected]);
useEffect(() => {
const selectedDateString = getDateString(dateSelected);
if (!dateSelected || selectedDateString === getDateString(new Date())) {
setRoutesForShowing(allRoutes);
setRoutesInboundForShowing(inboundRoutes);
setRoutesOutboundForShowing(processRoutesForAbsentCustomers(inboundRoutes, outboundRoutes));
} else {
if (dateSelected > new Date()) {
setRoutesForShowing(allTomorrowRoutes);
setRoutesInboundForShowing(tmrInboundRoutes);
setRoutesOutboundForShowing(processRoutesForAbsentCustomers(tmrInboundRoutes, tmrOutboundRoutes));
} else {
setRoutesForShowing(allHistoryRoutes);
setRoutesOutboundForShowing(processRoutesForAbsentCustomers(historyInboundRoutes, historyOutboundRoutes));
setRoutesInboundForShowing(historyInboundRoutes);
}
}
}, [allRoutes, allHistoryRoutes, allTomorrowRoutes]);
const now = new Date();
const yesterday = new Date();
const tomorrow = new Date();
const isFutureScheduleSelected = moment(dateSelected).startOf('day').isAfter(moment().startOf('day'));
const startScheduleImportProgress = (label) => {
if (scheduleImportProgressTimerRef.current) {
clearInterval(scheduleImportProgressTimerRef.current);
}
setScheduleImportLabel(label);
setScheduleImportProgress(10);
setIsScheduleImporting(true);
scheduleImportProgressTimerRef.current = setInterval(() => {
setScheduleImportProgress((prev) => (prev >= 90 ? 90 : prev + 5));
}, 250);
};
const finishScheduleImportProgress = () => {
if (scheduleImportProgressTimerRef.current) {
clearInterval(scheduleImportProgressTimerRef.current);
scheduleImportProgressTimerRef.current = null;
}
setScheduleImportProgress(100);
setTimeout(() => {
setIsScheduleImporting(false);
setScheduleImportProgress(0);
setScheduleImportLabel('');
}, 400);
};
const refreshRoutesForCurrentDate = () => {
const selectedDateString = getDateString(dateSelected);
if (selectedDateString === getDateString(new Date())) {
dispatch(fetchAllRoutes());
} else if (dateSelected > new Date()) {
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
} else {
dispatch(fetchAllHistoryRoutes({dateText: getDateString(dateSelected)}));
}
};
const buildImportedRouteData = (draftRoute, targetDateText) => {
return {
name: draftRoute?.name,
vehicle: draftRoute?.vehicle,
driver: draftRoute?.driver,
type: draftRoute?.type,
schedule_date: targetDateText,
start_mileage: draftRoute?.start_mileage || vehicles.find((vehicle) => vehicle.id === draftRoute?.vehicle)?.mileage,
end_mileage: draftRoute?.end_mileage,
status: [],
start_time: null,
end_time: null,
estimated_start_time: null,
checklist_result: draftRoute?.checklist_result || [],
route_customer_list: (draftRoute.route_customer_list || []).map((customer) => ({
...customer,
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_estimated_pickup_time: null,
customer_estimated_dropoff_time: null,
customer_route_status: null,
customer_address_override: null
}))
};
};
const importFromDate = async () => {
if (!importSourceDate) {
window.alert('Please select a source date.');
return;
}
startScheduleImportProgress('Importing schedule from selected date...');
try {
const sourceDateText = getDateString(importSourceDate);
const targetDateText = getDateString(dateSelected);
const [{ data: sourceRoutes }, { data: existingRoutes }] = await Promise.all([
TransRoutesService.getAll(sourceDateText),
TransRoutesService.getAll(targetDateText)
]);
let skippedCount = 0;
const createPromises = [];
for (const draftRoute of (sourceRoutes || [])) {
const existed = (existingRoutes || []).find((r) => r.name === draftRoute.name && r.type === draftRoute.type);
if (existed) {
skippedCount++;
continue;
}
const routeData = buildImportedRouteData(draftRoute, targetDateText);
createPromises.push(TransRoutesService.createNewRoute(routeData));
}
await Promise.all(createPromises);
refreshRoutesForCurrentDate();
setShowImportDateDropdown(false);
setSuccessMessage(`Imported ${createPromises.length} route(s) from ${sourceDateText}.`);
setTimeout(() => setSuccessMessage(undefined), 5000);
if (skippedCount > 0) {
window.alert(`${skippedCount} route(s) already existed on target date and were skipped.`);
}
} catch (error) {
console.error('Failed to import routes from date:', error);
setErrorMessage(error?.message || 'Failed to import routes from date');
setTimeout(() => setErrorMessage(undefined), 5000);
} finally {
finishScheduleImportProgress();
}
};
const importFromTemplate = async () => {
if (!importTemplateId) {
window.alert('Please select a template.');
return;
}
const selectedTemplate = templates.find((t) => t.id === importTemplateId);
if (!selectedTemplate) {
window.alert('Template not found.');
return;
}
startScheduleImportProgress('Copying routes from template...');
try {
const targetDateText = getDateString(dateSelected);
const { data: existingRoutes } = await TransRoutesService.getAll(targetDateText);
let skippedCount = 0;
const createPromises = [];
for (const route of (selectedTemplate.routes || [])) {
const existed = (existingRoutes || []).find((r) => r.name === route.name && r.type === route.type);
if (existed) {
skippedCount++;
continue;
}
const routeData = buildImportedRouteData(route, targetDateText);
createPromises.push(TransRoutesService.createNewRoute(routeData));
}
await Promise.all(createPromises);
refreshRoutesForCurrentDate();
setShowImportTemplateDropdown(false);
setImportTemplateId('');
setSuccessMessage(`Imported ${createPromises.length} route(s) from template "${selectedTemplate.name}".`);
setTimeout(() => setSuccessMessage(undefined), 5000);
if (skippedCount > 0) {
window.alert(`${skippedCount} route(s) already existed on target date and were skipped.`);
}
} catch (error) {
console.error('Failed to import routes from template:', error);
setErrorMessage(error?.message || 'Failed to import routes from template');
setTimeout(() => setErrorMessage(undefined), 5000);
} finally {
finishScheduleImportProgress();
}
};
yesterday.setDate(now.getDate() - 1);
tomorrow.setDate(now.getDate() + 1);
const directToSchedule = () => {
setDateSelected(tomorrow);
}
const generateRouteReport = () => {
window.open(`${reportRootUrl}?token=${localStorage.getItem('token')}&date=${getDateString(now)}`, '_blank');
}
const goToHistoryPage = () => {
navigate('/trans-routes/history');
}
const createVehicle = () => {
navigate('/vehicles?redirect=schedule');
}
const createDriver = () => {
navigate('/employees?redirect=schedule&type=driver');
}
// const goToSignature = () => {
// navigate('/trans-routes/route-signature');
// }
const createNewRoute = () => {
navigate('/trans-routes/create')
}
const goToCreateRoute = (type) => {
navigate(`/trans-routes/create?type=${type}&date=${dateSelected? moment(dateSelected).format('YYYY-MM-DD'): 'tomorrow'}`);
}
const goToRouteDetails = (routeId) => {
if (!routeId) return;
const selectedDateString = getDateString(dateSelected);
if (selectedDateString === getDateString(new Date())) {
navigate(`/trans-routes/${routeId}`);
return;
}
navigate(`/trans-routes/${routeId}?dateSchedule=${moment(dateSelected).format('YYYY-MM-DD')}`);
}
const changeTab = (k) => {
setCurrentTab(k);
setKeyword('');
setSorting({key: '', order: ''});
setDateSelected(new Date());
setOriginDateSelected(undefined);
setTargetedDateSelected(undefined);
// setSelectedDriver(undefined);
}
const cleanupSchedule = () => {
const routesToDelete = deleteRouteType === 'inbound'
? (tmrInboundRoutes || [])
: deleteRouteType === 'outbound'
? (tmrOutboundRoutes || [])
: (allTomorrowRoutes || []);
routesToDelete.forEach((route) => {
TransRoutesService.deleteRoute(route.id);
});
setTimeout(() => {
closeDeleteModal();
window.location.reload();
}, 1000)
}
const closeDeleteModal = () => {
setShowDeleteModal(false);
setDeleteRouteType('');
}
const triggerShowDeleteModal = (routeType = '') => {
setDeleteRouteType(routeType);
setShowDeleteModal(true);
}
const startToScheduleDate = (v) => {
// setDateSchedule(v);
setSearchParams({dateSchedule: moment(v).format('YYYY-MM-DD')});
dispatch(fetchAllTomorrowRoutes({dateText: moment(v).format('MM/DD/YYYY')}));
}
const validateSchedule = () => {
const inboundCustomersRouteMap = {};
let success = true;
for (const inboundRoute of tmrInboundRoutes) {
for (const inboundCustomer of inboundRoute.route_customer_list) {
if (Object.keys(inboundCustomersRouteMap).includes(inboundCustomer.customer_id) && inboundCustomer.customer_route_status !== PERSONAL_ROUTE_STATUS.DISABLED) {
setSuccessMessage(undefined);
success = false;
setErrorMessage(`Error: Customer ${inboundCustomer.customer_name} was scheduled in both inbound ${inboundRoute.name} and ${inboundCustomersRouteMap[inboundCustomer.customer_id].name}.`)
break;
} else {
if (inboundCustomer.customer_route_status !== PERSONAL_ROUTE_STATUS.DISABLED) {
inboundCustomersRouteMap[inboundCustomer.customer_id] = inboundRoute;
}
}
}
}
const outboundCustomersRouteMap = {};
for (const outboundRoute of tmrOutboundRoutes) {
for (const outboundCustomer of outboundRoute.route_customer_list) {
if (Object.keys(outboundCustomersRouteMap).includes(outboundCustomer.customer_id) && outboundCustomer.customer_route_status !== PERSONAL_ROUTE_STATUS.DISABLED) {
setSuccessMessage(undefined);
success = false;
setErrorMessage(`Error: Customer ${outboundCustomer.customer_name} was scheduled in both outbound ${outboundRoute.name} and ${outboundCustomersRouteMap[outboundCustomer.customer_id].name}.`)
break;
} else {
if (outboundCustomer.customer_route_status !== PERSONAL_ROUTE_STATUS.DISABLED) {
outboundCustomersRouteMap[outboundCustomer.customer_id] = outboundRoute;
}
}
}
}
if (success) {
setErrorMessage(undefined);
setSuccessMessage(`Routes Schedule Validate Successfully`);
}
}
const copyTodayRoutesOver = () => {
setShowCopyTodayLoading(true);
setIsLoading(true);
let count = 0;
TransRoutesService.getAll(((now.getMonth() > 8) ? (now.getMonth() + 1) : ('0' + (now.getMonth() + 1))) + '/' + ((now.getDate() > 9) ? now.getDate() : ('0' + now.getDate())) + '/' + now.getFullYear()).then(existingRoutes => {
for (const draftRoute of allRoutes) {
const existed = existingRoutes?.data?.find(r => r.name === draftRoute.name);
if (draftRoute && !existed) {
const data = Object.assign({}, {
name: draftRoute.name,
schedule_date: ((now.getMonth() > 8) ? (now.getMonth() + 1) : ('0' + (now.getMonth() + 1))) + '/' + ((now.getDate() > 9) ? now.getDate() : ('0' + now.getDate())) + '/' + now.getFullYear(),
vehicle: draftRoute.vehicle,
driver: draftRoute.driver,
type: draftRoute.type,
start_mileage: vehicles.find((vehicle) => vehicle.id === draftRoute.vehicle)?.mileage,
route_customer_list: draftRoute.route_customer_list?.map((customer) => {
return Object.assign({}, customer, {
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_estimated_pickup_time: null,
customer_estimated_dropoff_time: null,
customer_route_status: null,
customer_address_override: null
})
})
});
dispatch(createRoute({ data }));
} else {
if (existed) {
count++;
}
}
}
setTimeout(() => {
dispatch(fetchAllTomorrowRoutes({}));
setShowCopyTodayLoading(false);
setCopyDisabled(true);
setIsLoading(false);
setSuccessMessage('Routes Copied Successfully, please do not click the button again!');
if (count > 0) {
window.alert(`${count} routes has existed in selected date and is not copied again!`)
}
}, 2000);
})
}
const copyDateRoutesOver = (withTargetDate) => {
const dateOrigin = new Date(originDateSelected);
const newScheduleDate = withTargetDate ? new Date(targetedDateSelected) : now;
if (withTargetDate) {
setShowCopyDateTargetLoading(true);
} else {
setShowCopyDateLoading(true);
}
setIsLoading(true);
let count = 0;
TransRoutesService.getAll(getDateString(dateOrigin)).then(({data: allHistoryRoutes}) => {
TransRoutesService.getAll(((newScheduleDate.getMonth() > 8) ? (newScheduleDate.getMonth() + 1) : ('0' + (newScheduleDate.getMonth() + 1))) + '/' + ((newScheduleDate.getDate() > 9) ? newScheduleDate.getDate() : ('0' + newScheduleDate.getDate())) + '/' + newScheduleDate.getFullYear()).then(existingRoutes => {
for (const draftRoute of allHistoryRoutes) {
const existed = existingRoutes?.data?.find((r) => r. name === draftRoute.name);
if (draftRoute && !existed) {
const data = Object.assign({}, {
name: draftRoute.name,
schedule_date: ((newScheduleDate.getMonth() > 8) ? (newScheduleDate.getMonth() + 1) : ('0' + (newScheduleDate.getMonth() + 1))) + '/' + ((newScheduleDate.getDate() > 9) ? newScheduleDate.getDate() : ('0' + newScheduleDate.getDate())) + '/' + newScheduleDate.getFullYear(),
vehicle: draftRoute.vehicle,
driver: draftRoute.driver,
type: draftRoute.type,
start_mileage: vehicles.find((vehicle) => vehicle.id === draftRoute.vehicle)?.mileage,
route_customer_list: draftRoute.route_customer_list?.map((customer) => {
return Object.assign({}, customer, {
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_estimated_pickup_time: null,
customer_estimated_dropoff_time: null,
customer_route_status: null,
customer_address_override: null
})
})
});
dispatch(createRoute({ data }));
} else {
if (existed) {
count++;
}
}
}
setTimeout(() => {
// dispatch(fetchAllTomorrowRoutes({}));
startToScheduleDate(withTargetDate ? targetedDateSelected : now);
if (withTargetDate) {
setShowCopyDateTargetLoading(false);
} else {
setShowCopyDateLoading(false);
}
setIsLoading(false);
// setCopyDateDisabled(true);
setSuccessMessage('Routes Copied Successfully, please do not click the button again!');
setDateSelected(targetedDateSelected);
setOriginDateSelected(undefined);
setTargetedDateSelected(undefined);
if (count > 0) {
window.alert(`${count} routes has existed on selected date and are not copied again.`)
}
}, 2000);
});
});
}
const uploadDirectorSignature = () => {
const formData = new FormData();
const site = EventsService.site;
formData.append("file", selectedFile);
if (selectedFile) {
if (directorSignature) {
CustomerService.deleteFile({'name': `center_director_signature_site_${site}`}).then(() => {
CustomerService.uploadAvatar(`center_director_signature_site_${site}`, formData).then(() => {
CustomerService.getAvatar(`center_director_signature_site_${site}`).then(data => {
if (data?.data) {
setDirectorSignature(data?.data)
}
});
})
})
} else {
CustomerService.uploadAvatar(`center_director_signature_site_${site}`, formData).then(() => {
CustomerService.getAvatar(`center_director_signature_site_${site}`).then(data => {
if (data?.data) {
setDirectorSignature(data?.data)
}
});
})
}
}
}
const getAllUniqueCustomers = (routes) => {
let result = [];
for (const route of routes) {
const customerList = route.route_customer_list.map(item => Object.assign({}, item, {routeType: route.type, routeId: route.id, route: route, customer_status_inbound: route.type === 'inbound' && item.customer_route_status, customer_status_outbound: route.type === 'outbound' && item.customer_route_status, inbound: route.type === 'inbound' && route, outbound: route.type === 'outbound' && route}))
for (const customer of customerList) {
const existItem = result.find((item => (item.customer_id === customer.customer_id) || (item?.customer_name?.replaceAll(' ', '')?.toLowerCase() === customer?.customer_name?.replaceAll(' ', '')?.toLowerCase()) ));
if (existItem) {
result = result.filter(item => item !== existItem);
const newItem = Object.assign({}, existItem, {
customer_enter_center_time: existItem?.customer_enter_center_time || customer?.customer_enter_center_time,
customer_leave_center_time: existItem?.customer_leave_center_time || customer?.customer_leave_center_time,
customer_pickup_time: existItem?.customer_pickup_time || customer?.customer_pickup_time,
customer_dropoff_time: existItem?.customer_dropoff_time || customer?.customer_dropoff_time,
inbound: existItem?.inbound || customer?.inbound,
outbound: existItem?.outbound || customer?.outbound,
customer_status_inbound: existItem?.customer_status_inbound || customer?.customer_status_inbound,
customer_status_outbound: existItem?.customer_status_outbound || customer?.customer_status_outbound
})
result.push(newItem);
} else {
result.push(customer);
}
}
}
return result.sort((a, b) => {
if (a.customer_name < b.customer_name) {
return -1;
} else {
return 1;
}
});
}
const copyYesterdayRoutes = () => {
setShowCopyDateLoading(true);
TransRoutesService.getAll(getDateString(yesterday)).then(({data: yesterdayRoutes}) => {
for (const draftRoute of yesterdayRoutes) {
if (draftRoute) {
const data = Object.assign({}, {
name: draftRoute.name,
schedule_date: ((now.getMonth() > 8) ? (now.getMonth() + 1) : ('0' + (now.getMonth() + 1))) + '/' + ((now.getDate() > 9) ? now.getDate() : ('0' + now.getDate())) + '/' + now.getFullYear(),
vehicle: draftRoute.vehicle,
driver: draftRoute.driver,
type: draftRoute.type,
start_mileage: vehicles.find((vehicle) => vehicle.id === draftRoute.vehicle)?.mileage,
route_customer_list: draftRoute.route_customer_list?.map((customer) => {
return Object.assign({}, customer, {
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_estimated_pickup_time: null,
customer_estimated_dropoff_time: null,
customer_route_status: null
})
})
});
dispatch(createRoute({ data }));
}
}
setTimeout(() => {
dispatch(fetchAllRoutes());
setShowCopyDateLoading(false);
}, 2000);
});
}
const cleanAllRoutesStatus = () => {
const updatePromises = allRoutes.map(route => {
let cleanRoute = Object.assign({}, route, { status: [ROUTE_STATUS.NOT_STARTED] });
let newCustomers = [];
for (let i=0; i< cleanRoute.route_customer_list.length; i++) {
const newCustomerListObject = {
...cleanRoute.route_customer_list[i],
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_estimated_pickup_time: null,
customer_estimated_dropoff_time: null,
customer_route_status: cleanRoute.route_customer_list[i].customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT ? PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT : PERSONAL_ROUTE_STATUS.NO_STATUS
}
newCustomers.push(newCustomerListObject);
}
cleanRoute = Object.assign({}, cleanRoute, {route_customer_list: newCustomers});
return TransRoutesService.updateRoute(route.id, cleanRoute);
});
Promise.all(updatePromises)
.then(() => {
dispatch(fetchAllRoutes());
})
.catch(err => {
console.error('Error cleaning route statuses:', err);
});
}
const handleCleanAllRoutesStatus = () => {
if (window.confirm('Are you sure you want to do this? This cannot be undone.')) {
cleanAllRoutesStatus();
}
}
const syncCustomersInfo = () => {
setShowSyncCustomersLoading(true);
CustomerService.getAllCustomers().then(data => {
const customers = data.data;
const customersMap = new Map();
for (const customer of customers) {
customersMap.set(customer.id, {
customer_name: `${customer.name} ${customer.name_cn?.length > 0 ? `(${customer.name_cn})` : ``}`,
customer_type: customer.type,
customer_pickup_status: customer.pickup_status,
customer_note: customer.note,
customer_special_needs: customer.special_needs,
customer_phone: customer.phone || customer.mobile_phone || customer.home_phone,
customer_table_id: customer.table_id,
customer_language: customer.language
})
};
// Batch all route updates, then refetch once after all complete
const updatePromises = allRoutes.map(route => {
const customerList = route.route_customer_list;
const newCustomerList = customerList.map((customerItem) => Object.assign({}, customerItem, customersMap.get(customerItem.customer_id)));
const newRouteObject = Object.assign({}, route, {route_customer_list: newCustomerList});
return TransRoutesService.updateRoute(route.id, newRouteObject);
});
Promise.all(updatePromises)
.then(() => {
dispatch(fetchAllRoutes());
})
.catch(err => {
console.error('Error syncing customer info:', err);
})
.finally(() => {
setShowSyncCustomersLoading(false);
});
})
}
const cleanFilterAndClose = () => {
setSelectedDriver(null);
// setDateSelected(new Date())
setShowFilterDropdown(false);
setShowDateDropdown(false);
setCustomerTableId('');
setStatusFilter('');
setCustomerNameFilter('');
setCustomerTypeFilter('');
setKeyword('');
setRouteTypeFilter('');
setShowFilterReportDropdown(false);
}
const cleanDate = () => {
setDateSelected(new Date());
setShowDateDropdown(false);
}
const FilterAndClose = () => {
setShowFilterDropdown(false);
setShowDateDropdown(false);
setShowFilterReportDropdown(false);
}
const getSortingImg = (key) => {
return sorting.key === key ? (sorting.order === 'asc' ? 'up_arrow' : 'down_arrow') : 'default';
}
const sortTableWithField = (key) => {
let newSorting = {
key,
order: 'asc',
}
if (sorting.key === key && sorting.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
}
setSorting(newSorting);
}
const pickOriginDateSelected = (v) => {
setOriginDateSelected(v);
setDateSelected(v);
}
const columns = [
{
key: 'name',
label: 'Route Name'
},
{
key: 'driver',
label: 'Driver'
},
{
key: 'end_time',
label: 'Route End Time'
},
{
key: 'type',
label: 'Route Type'
},
];
const customMenu = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<h6>Filter By</h6>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Driver</div>
<select value={selectedDriver} onChange={e => setSelectedDriver(e.target.value)}>
<option value={null}></option>
{driverList.map((driver) => <option value={driver?.id}>{driver?.name}</option>)}
</select>
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => cleanFilterAndClose()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => FilterAndClose()}> Filter </button>
</div>
</div>
</div>
);
},
);
const customReportFilterMenu = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<h6>Filter By</h6>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Participate Status</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
{
[['', {text: ''}], ...Object.entries(PERSONAL_ROUTE_STATUS_TEXT)].map(([key, {text}]) => (
<option key={key} value={key}>{text}</option>
))
}
</select>
</div>
</div>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Participant Type</div>
<select
value={customerTypeFilter}
onChange={(e) => setCustomerTypeFilter(e.target.value)}
>
{
[['', ''], ...Object.entries(CUSTOMER_TYPE_TEXT)].map(([key, text]) => (
<option key={key} value={key}>{text}</option>
))
}
</select>
</div>
</div>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Routes Type</div>
<select
value={routeTypeFilter}
onChange={(e) => setRouteTypeFilter(e.target.value)}
>
<option value=""></option>
<option value="inbound">inbound</option>
<option value="outbound">outbound</option>
</select>
</div>
</div>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Participant Name</div>
<input type="text" value={customerNameFilter} onChange={(e) => setCustomerNameFilter(e.target.value)} />
</div>
<div className="me-4">
<div className="field-label">Table Id</div>
<input type="text" value={customerTableId} onChange={(e) => setCustomerTableId(e.target.value)} />
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => cleanFilterAndClose()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => FilterAndClose()}> Filter </button>
</div>
</div>
</div>
);
},
);
const customMenuDate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
const handleDateChange = (v) => {
setDateSelected(v);
setShowDateDropdown(false);
};
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Date to View Report</div>
<DatePicker selected={dateSelected} onChange={handleDateChange} />
</div>
</div>
</div>
);
},
);
const customMenuOriginDate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Date to Start</div>
<DatePicker selected={originDateSelected} onChange={(v) => pickOriginDateSelected(v)} />
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => setShowOriginDateDropdown(false)}> Close </button>
</div>
</div>
</div>
);
},
);
const customMenuImportFromDate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Source Date (Past 14 Days)</div>
<DatePicker
selected={importSourceDate}
onChange={(v) => setImportSourceDate(v)}
minDate={moment().subtract(14, 'days').toDate()}
maxDate={new Date()}
/>
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => setShowImportDateDropdown(false)}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={importFromDate}> Import </button>
</div>
</div>
</div>
);
},
);
const customMenuImportFromTemplate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Saved Template</div>
<select
className="form-select"
value={importTemplateId}
onChange={(e) => setImportTemplateId(e.target.value)}
>
<option value="">Choose a template</option>
{templates.map((template) => (
<option key={template.id} value={template.id}>
{template.name} - {template.template_date}
</option>
))}
</select>
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => setShowImportTemplateDropdown(false)}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={importFromTemplate}> Copy </button>
</div>
</div>
</div>
);
},
);
const customMenuTargetDate = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Select Date to Start</div>
<DatePicker selected={targetedDateSelected} onChange={(v) => setTargetedDateSelected(v)} />
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => {setTargetedDateSelected(undefined); setShowTargetDateDropdown(false)}}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => {copyDateRoutesOver(true); setShowTargetDateDropdown(false)}}> Start to Copy </button>
</div>
</div>
</div>
);
},
);
// Save template modal handlers
const openSaveTemplateModal = () => {
setShowSaveTemplateModal(true);
setTemplateName('');
};
const closeSaveTemplateModal = () => {
setShowSaveTemplateModal(false);
setTemplateName('');
};
const saveRoutesAsTemplate = () => {
if (!templateName || templateName.trim() === '') {
alert('Please enter a template name');
return;
}
setSavingTemplate(true);
// Get the current date string in MM/DD/YYYY format
const templateDate = getDateString(dateSelected);
// Get current user from localStorage
const currentUser = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user'))?.name : '';
// Clean the routes data - remove all date fields and status
const cleanedRoutes = routesForShowing.map(route => {
// Clean customer list - set all date fields to null and reset dynamic fields to undefined
const cleanedCustomerList = route.route_customer_list?.map(customer => ({
...customer,
customer_enter_center_time: null,
customer_leave_center_time: null,
customer_pickup_time: null,
customer_dropoff_time: null,
customer_route_status: null,
// customer_estimated_pickup_time: undefined,
// customer_estimated_dropoff_time: undefined,
// customer_pickup_status: undefined,
// customer_route_status: undefined,
// customer_transfer_to_route: undefined,
// customer_address_override: undefined
})) || [];
// Return cleaned route
return {
name: route.name,
schedule_date: route.schedule_date,
vehicle: route.vehicle,
status: [], // Empty array
driver: route.driver,
type: route.type,
start_mileage: route.start_mileage,
end_mileage: route.end_mileage,
start_time: null,
end_time: null,
estimated_start_time: null,
route_customer_list: cleanedCustomerList,
checklist_result: route.checklist_result || []
};
});
// Create the template payload
const templatePayload = {
name: templateName,
template_date: templateDate,
routes: cleanedRoutes,
create_by: currentUser
};
// Call the service to create the template
DailyRoutesTemplateService.createNewDailyRoutesTemplate(templatePayload)
.then(() => {
setSavingTemplate(false);
closeSaveTemplateModal();
setSuccessMessage(`Template "${templateName}" saved successfully!`);
setTimeout(() => setSuccessMessage(undefined), 5000);
// Refresh templates list
DailyRoutesTemplateService.getAll().then((response) => {
setTemplates(response.data || []);
});
})
.catch(err => {
setSavingTemplate(false);
setErrorMessage(err.message || 'Failed to save template');
setTimeout(() => setErrorMessage(undefined), 5000);
});
};
// Apply template handler
const applyTemplate = () => {
if (!selectedTemplateId) {
alert('Please select a template');
return;
}
setApplyingTemplate(true);
// Find the selected template
const selectedTemplate = templates.find(t => t.id === selectedTemplateId);
if (!selectedTemplate) {
alert('Template not found');
setApplyingTemplate(false);
return;
}
// Get the target date in MM/DD/YYYY format
const targetDate = getDateString(dateSelected);
// Create promises for all route creations
const createPromises = selectedTemplate.routes.map(route => {
const routeData = {
...route,
schedule_date: targetDate,
route_customer_list: (route.route_customer_list || []).map((customer) => ({
...customer,
customer_route_status: null
}))
};
return TransRoutesService.createNewRoute(routeData);
});
// Execute all route creations
Promise.all(createPromises)
.then(() => {
setApplyingTemplate(false);
setSelectedTemplateId('');
setSuccessMessage(`Template "${selectedTemplate.name}" applied successfully to ${targetDate}!`);
setTimeout(() => setSuccessMessage(undefined), 5000);
// Refresh the routes based on current date selection
const selectedDateString = getDateString(dateSelected);
if (selectedDateString === getDateString(new Date())) {
dispatch(fetchAllRoutes());
} else {
if (dateSelected > new Date()) {
dispatch(fetchAllTomorrowRoutes({dateText: moment(dateSelected).format('MM/DD/YYYY')}));
} else {
dispatch(fetchAllHistoryRoutes({dateText: getDateString(dateSelected)}));
}
}
})
.catch(err => {
setApplyingTemplate(false);
setErrorMessage(err.message || 'Failed to apply template');
setTimeout(() => setErrorMessage(undefined), 5000);
});
};
useEffect(() => {
return () => {
if (scheduleImportProgressTimerRef.current) {
clearInterval(scheduleImportProgressTimerRef.current);
}
};
}, []);
return (
<>
{pageLoading && <div className="spinner-overlay">
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>}
{isScheduleImporting && <div className="spinner-overlay" style={{ flexDirection: 'column', gap: '12px', zIndex: 1060 }}>
<div style={{ width: '360px', maxWidth: '80vw', textAlign: 'center' }}>
<div style={{ marginBottom: '8px', color: '#333' }}>{scheduleImportLabel || 'Importing schedule...'}</div>
<ProgressBar now={scheduleImportProgress} animated label={`${Math.round(scheduleImportProgress)}%`} />
</div>
</div>}
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
<Breadcrumb.Item active>
Transportation Routes
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
All Routes - {moment(dateSelected).format('MM/DD/YYYY (dddd)')}
</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="allRoutesOverview" id="routes-tab" onSelect={k => changeTab(k)}>
<Tab eventKey="allRoutesOverview" title="All Routes Overview">
{(!dateSelected || getDateString(dateSelected) === getDateString(new Date())) && <div className="app-main-content-fields-section with-function">
<button className="btn btn-primary me-2" onClick={() => directToSchedule()}><CalendarCheck size={16} className="me-2"></CalendarCheck> Schedule Tomorrow's Routes</button>
{/* <button className="btn btn-primary me-2" onClick={() => syncCustomersInfo()}><Clock size={16} className="me-2"></Clock> {showSyncCustomersLoading? <Spinner size={12}></Spinner> : `Sync Customers Data`}</button> */}
<button className="btn btn-primary me-2" onClick={()=> handleCleanAllRoutesStatus()}><Eraser size={16} className="me-2"></Eraser > Clean Route Status</button>
{/* <button className="btn btn-primary me-2" onClick={() => goToHistoryPage()}><ClockHistory size={16} className="me-2"></ClockHistory> View History</button> */}
<button className="btn btn-primary me-2" onClick={() => copyYesterdayRoutes()}><Copy size={16} className="me-2"></Copy>{showCopyDateLoading? <Spinner size={12}></Spinner> : `Copy Yesterday Routes`}</button>
<button className="btn btn-primary" onClick={() => generateRouteReport()}><Download size={16} className="me-2"></Download>Export Route Report</button>
</div>}
{(dateSelected && dateSelected > new Date()) && <div className="app-main-content-fields-section with-function">
{/* <button type="button" className="btn btn-primary btn-sm me-2" onClick={()=> validateSchedule()}><Check size={16} className="me-2"></Check> Validate and Finish Planning</button> */}
</div>}
{ (dateSelected <= new Date() || !dateSelected) && <div className="list row">
{
showCopyDateTargetLoading ? <><Spinner></Spinner></> : <>
<div className="col-md-12 mb-4">
{(routesInboundForShowing && routesInboundForShowing.length > 0) || (routesOutboundForShowing && routesOutboundForShowing.length > 0) ? (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
) : (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<select
className="form-select"
style={{width: 'auto', display: 'inline-block'}}
value={selectedTemplateId}
onChange={(e) => setSelectedTemplateId(e.target.value)}
disabled={applyingTemplate}
>
<option value="">Choose a daily template to apply to this day</option>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.name} - {template.template_date}
</option>
))}
</select>
<button
className="btn btn-primary btn-sm"
onClick={applyTemplate}
disabled={!selectedTemplateId || applyingTemplate}
>
{applyingTemplate ? <><Spinner size="sm" className="me-1" />Applying...</> : 'Submit'}
</button>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
)}
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={routesInboundForShowing} drivers={drivers} vehicles={vehicles} sectionName="Inbound Routes"/>
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={routesOutboundForShowing} drivers={drivers} vehicles={vehicles} sectionName="Outbound Routes"/>
</div>
</>
}
</div>}
{
dateSelected > new Date() && <>
{errorMessage && <div className="alert alert-danger alert-dismissible fade show" role="alert">
{errorMessage}
<button onClick={() => setErrorMessage(undefined)} type="button" className="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>}
{successMessage && <div className="alert alert-success alert-dismissible fade show" role="alert">
{successMessage}
<button onClick={() => setSuccessMessage(undefined)} type="button" className="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>}
<div className="list row">
{isLoading && <div className="col-md-12"><Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner></div>}
{!isLoading && <>
<div className="col-md-12 mb-4">
{(tmrInboundRoutes && tmrInboundRoutes.length > 0) || (tmrOutboundRoutes && tmrOutboundRoutes.length > 0) ? (
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
{/* <button className="btn btn-secondary btn-sm" onClick={() => navigate('/trans-routes/daily-templates/list')}>
View and Update Daily Route Templates
</button> */}
</div>
) : null}
</div>
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={tmrInboundRoutes} copyList={tmrOutboundRoutes} addText="Add New Route" copyText={AuthService.canAddOrEditRoutes() ? "Import Outbound Routes" : null} canAddNew={AuthService.canAddOrEditRoutes()} drivers={drivers} vehicles={vehicles} redirect={goToCreateRoute} routeType="inbound" sectionName="Inbound Routes" showCheckedInText={false} showRouteHeaderButtons={true} clearAllRoutesAction={AuthService.canAddOrEditRoutes() ? () => triggerShowDeleteModal('inbound') : null}/>
</div>
<hr />
<div className="col-md-12 mb-4">
<RoutesSection transRoutes={tmrOutboundRoutes} copyList={tmrInboundRoutes} addText="Add New Route" copyText={AuthService.canAddOrEditRoutes() ? "Import Inbound Routes" : null} canAddNew={AuthService.canAddOrEditRoutes()} drivers={drivers} vehicles={vehicles} redirect={goToCreateRoute} routeType="outbound" sectionName="Outbound Routes" showRouteHeaderButtons={true} clearAllRoutesAction={AuthService.canAddOrEditRoutes() ? () => triggerShowDeleteModal('outbound') : null}/>
</div>
<hr />
</>}
</div>
</>
}
</Tab>
{!isFutureScheduleSelected && <Tab eventKey="allRoutesSignature" title="All Routes Signature">
<input className="me-2 mb-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
<table className="personnel-info-table me-4">
<thead>
<tr>
<th className="th-index">No.</th>
{
columns.map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithField(column.key)}><img src={`/images/${getSortingImg(column.key)}.png`}></img></span>
</th>)
}
<th>Signature</th>
</tr>
</thead>
<tbody>
{
routesForSignature && routesForSignature.filter((route) => {
if (!selectedDriver) {
return route;
} else {
return route?.driver === selectedDriver;
}
}).filter((route) => route.name?.toLowerCase().includes(keyword?.toLowerCase()) || drivers.find((d) => d.id === route?.driver)?.name?.toLowerCase().includes(keyword?.toLowerCase()))?.map(({id, name, end_time, driver, type, signature}, index) => {
return (<tr key={index}>
<td className="td-index">{index + 1}</td>
<td>
<button
type="button"
className="btn btn-link p-0"
style={{ fontSize: 'inherit', verticalAlign: 'baseline' }}
onClick={() => goToRouteDetails(id)}
>
{name}
</button>
</td>
<td>{drivers.find((d) => d.id === driver)?.name}</td>
<td>{end_time? moment(end_time).format('HH:mm'): ''}</td>
<td>{type}</td>
<td>
{/* {images.find((img) => img.id === id)?.image && <img width="100px" src={`data:image/jpg;base64, ${images.find((img) => img.id === id)?.image}`}/>} */}
{signature && <img width="100px" src={`data:image/jpg;base64, ${signature}`}/>}
</td>
</tr>)
})
}
</tbody>
</table>
</Tab>}
{!isFutureScheduleSelected && <Tab eventKey="allRoutesStatus" title="All Routes Status">
<div className="list row">
<div className="col-md-12 mb-4">
<PersonnelSection transRoutes={routesForShowing} showCompletedInfo={false}
showGroupInfo={false} allowForceEdit={AuthService.canAddOrEditRoutes()}
showFilter={true} sectionName="Personnel Status (click on each user to edit)"
vehicles={vehicles} keyword={keyword}
statusFilter={statusFilter}
customerTypeFilter={customerTypeFilter}
customerNameFilter={customerNameFilter}
customerTableId={customerTableId}
routeTypeFilter={routeTypeFilter}
/>
</div>
</div>
</Tab>}
</Tabs>
<div className="list-func-panel">
{
currentTab === 'allRoutesOverview' && <> {
!showCopyDateTargetLoading && <>
<Dropdown
key={'signature-date'}
id="signature-date"
className="me-2"
show={showOriginDateDropdown}
disabled
onToggle={() => setShowOriginDateDropdown(!showOriginDateDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="outline-secondary">
<CalendarWeek size={16} className="me-2"></CalendarWeek>View By Date
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuOriginDate}/>
</Dropdown>
{isFutureScheduleSelected && <>
<Dropdown
key={'import-from-date'}
id="import-from-date"
className="me-2"
show={showImportDateDropdown}
onToggle={() => setShowImportDateDropdown(!showImportDateDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="outline-secondary" disabled={isScheduleImporting}>
<Copy size={16} className="me-2"></Copy>Import From Date
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuImportFromDate}/>
</Dropdown>
<Dropdown
key={'import-from-template'}
id="import-from-template"
className="me-2"
show={showImportTemplateDropdown}
onToggle={() => setShowImportTemplateDropdown(!showImportTemplateDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="outline-secondary" disabled={isScheduleImporting}>
<Copy size={16} className="me-2"></Copy>Import From Template
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuImportFromTemplate}/>
</Dropdown>
<button className="btn btn-outline-secondary me-2" onClick={() => createVehicle()} disabled={isScheduleImporting}>
<Plus size={16} className="me-2"></Plus>Add New Vehicle
</button>
<button className="btn btn-outline-secondary me-2" onClick={runCheckRoutes} disabled={isScheduleImporting}>
<Check size={16} className="me-2"></Check>Check Routes
</button>
</>}
</>
}
{!isFutureScheduleSelected && <button className="btn btn-primary me-2" onClick={() => goToCreateRoute()}><Plus size={16}></Plus>Add New Route</button>}
</>
}
{ currentTab === 'allRoutesSignature' && <>
<Dropdown
key={'signature-date'}
id="signature-date"
className="me-2"
show={showDateDropdown}
onToggle={() => setShowDateDropdown(!showDateDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="primary">
<CalendarWeek size={16} className="me-2"></CalendarWeek>Select Date to View Report
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuDate}/>
</Dropdown>
<Dropdown
key={'filter-signature'}
id="filter-signature"
className="me-2"
show={showFilterDropdown}
onToggle={() => setShowFilterDropdown(!showFilterDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="primary">
<Filter size={16} className="me-2"></Filter>Filter
</Dropdown.Toggle>
<Dropdown.Menu as={customMenu}/>
</Dropdown>
</> }
{ currentTab === 'allRoutesStatus' && <>
{/* <input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} /> */}
<Dropdown
key={'status-date'}
id="status-date"
className="me-2"
show={showDateDropdown}
onToggle={() => setShowDateDropdown(!showDateDropdown)}
autoClose="outside"
>
<Dropdown.Toggle variant="primary">
<CalendarWeek size={16} className="me-2"></CalendarWeek>Select Date to View Report
</Dropdown.Toggle>
<Dropdown.Menu as={customMenuDate}/>
</Dropdown>
<Dropdown
key={'filter-report'}
id="filter-report"
className="me-2"
show={showFilterReportDropdown}
onToggle={() => setShowFilterReportDropdown(!showFilterReportDropdown)}
autoClose="outside"
>
<Dropdown.Toggle variant="primary">
<Filter size={16} className="me-2"></Filter>Filter
</Dropdown.Toggle>
<Dropdown.Menu as={customReportFilterMenu}/>
</Dropdown>
<button className="btn btn-primary"><Download size={16} className="me-2"></Download>Export</button>
</> }
</div>
<Modal show={showDeleteModal} onHide={() => closeDeleteModal()}>
<Modal.Header closeButton>
<Modal.Title>Delete Schedule</Modal.Title>
</Modal.Header>
<Modal.Body>
<div>
{deleteRouteType === 'inbound'
? 'Are you sure you want to delete all inbound routes for this date?'
: deleteRouteType === 'outbound'
? 'Are you sure you want to delete all outbound routes for this date?'
: 'Are you sure you want to delete all the schedule?'}
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => closeDeleteModal()}>
No
</Button>
<Button variant="primary" onClick={() => cleanupSchedule()}>
Yes
</Button>
</Modal.Footer>
</Modal>
<Modal show={showSaveTemplateModal} onHide={closeSaveTemplateModal}>
<Modal.Header closeButton>
<Modal.Title>Save Routes as Template</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="mb-3">
<label className="form-label">Template Name</label>
<input
type="text"
className="form-control"
placeholder="Enter template name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
disabled={savingTemplate}
/>
</div>
<div className="text-muted">
<small>This will save all routes from {getDateString(dateSelected)} as a reusable template.</small>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={closeSaveTemplateModal} disabled={savingTemplate}>
Cancel
</Button>
<Button variant="primary" onClick={saveRoutesAsTemplate} disabled={savingTemplate}>
{savingTemplate ? <><Spinner size="sm" className="me-2" />Saving...</> : 'Submit'}
</Button>
</Modal.Footer>
</Modal>
<Modal show={showCheckRoutesModal} onHide={() => setShowCheckRoutesModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Check Routes Result</Modal.Title>
</Modal.Header>
<Modal.Body>
{(checkRoutesResult.inbound.length === 0 && checkRoutesResult.outbound.length === 0 && checkRoutesResult.attendance.length === 0 && checkRoutesResult.addressMismatch.length === 0 && checkRoutesResult.customerTypeMismatch.length === 0 && checkRoutesResult.customerSpecialNeedsMismatch.length === 0 && checkRoutesResult.customerNoteMismatch.length === 0 && checkRoutesResult.dischargedOnRoute.length === 0) ? (
<div className="text-success d-flex align-items-center">
<Check size={18} className="me-2"></Check>
<span>No issue found.</span>
</div>
) : (
<>
{checkRoutesResult.inbound.length > 0 && (
<div className="mb-3">
<div className="fw-bold mb-2">Inbound Issues</div>
{checkRoutesResult.inbound.map((issue, idx) => (
<div key={`inbound-issue-${idx}`} className="mb-2">
{issue.customerName} appears on multiple routes: {formatRouteAddressList(issue.routes)}
</div>
))}
</div>
)}
{checkRoutesResult.outbound.length > 0 && (
<div>
<div className="fw-bold mb-2">Outbound Issues</div>
{checkRoutesResult.outbound.map((issue, idx) => (
<div key={`outbound-issue-${idx}`} className="mb-2">
{issue.customerName} appears on multiple routes: {formatRouteAddressList(issue.routes)}
</div>
))}
</div>
)}
{checkRoutesResult.attendance.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Attendance Note Issues</div>
{checkRoutesResult.attendance.map((issue, idx) => (
<div key={`attendance-issue-${idx}`} className="mb-2">
{issue.customerName} is scheduled absent on {issue.noteDate} but still appears on Route {joinRouteNames(issue.routeNames)}.
</div>
))}
</div>
)}
{checkRoutesResult.addressMismatch.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Address Mismatch Issues</div>
{checkRoutesResult.addressMismatch.map((issue, idx) => (
<div key={`address-mismatch-issue-${idx}`} className="mb-2">
Customer {issue.customerName}&apos;s address: {issue.routeAddress || 'N/A'} is not matching his existing addresses stored in system, on route {joinRouteNames(issue.routeNames)}.
</div>
))}
</div>
)}
{checkRoutesResult.customerTypeMismatch.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Member Type Mismatch Issues</div>
{checkRoutesResult.customerTypeMismatch.map((issue, idx) => (
<div key={`customer-type-mismatch-issue-${idx}`} className="mb-2 d-flex align-items-center justify-content-between" style={{ gap: '12px' }}>
<div>
Customer {issue.customerName}&apos;s member type is mismatched on route {joinRouteNames(issue.mismatchedRoutes.map((route) => route.routeName))}. Latest type in system is {getCustomerTypeLabel(issue.dbType)}.
</div>
<Button
variant="outline-primary"
size="sm"
disabled={!!customerTypeFixing[issue.customerId]}
onClick={() => syncCustomerTypeForMismatch(issue)}
>
{customerTypeFixing[issue.customerId] ? 'Fixing...' : 'Fix'}
</Button>
</div>
))}
</div>
)}
{checkRoutesResult.customerSpecialNeedsMismatch.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Note to Driver Mismatch Issues</div>
{checkRoutesResult.customerSpecialNeedsMismatch.map((issue, idx) => (
<div key={`customer-special-needs-mismatch-issue-${idx}`} className="mb-2" style={{ whiteSpace: 'normal', overflowWrap: 'anywhere' }}>
<div>
Customer {issue.customerName}&apos;s note to driver is mismatched on route {joinRouteNames(issue.mismatchedRoutes.map((route) => route.routeName))}.
</div>
<div className="mt-1">
Latest note in system:
<div style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
{issue.dbNoteToDriver || 'empty'}
</div>
</div>
<div className="mt-2">
<Button
variant="outline-primary"
size="sm"
disabled={!!customerSpecialNeedsFixing[issue.customerId]}
onClick={() => syncCustomerSpecialNeedsForMismatch(issue)}
>
{customerSpecialNeedsFixing[issue.customerId] ? 'Fixing...' : 'Fix'}
</Button>
</div>
</div>
))}
</div>
)}
{checkRoutesResult.customerNoteMismatch.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Route Note Mismatch Issues</div>
{checkRoutesResult.customerNoteMismatch.map((issue, idx) => (
<div key={`customer-note-mismatch-issue-${idx}`} className="mb-2" style={{ whiteSpace: 'normal', overflowWrap: 'anywhere' }}>
<div>
Customer {issue.customerName}&apos;s route note is mismatched on route {joinRouteNames(issue.mismatchedRoutes.map((route) => route.routeName))}.
</div>
<div className="mt-1">
Latest note in system:
<div style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
{issue.dbNoteToDriver || 'empty'}
</div>
</div>
<div className="mt-2">
<Button
variant="outline-primary"
size="sm"
disabled={!!customerNoteFixing[issue.customerId]}
onClick={() => syncCustomerNoteForMismatch(issue)}
>
{customerNoteFixing[issue.customerId] ? 'Fixing...' : 'Fix'}
</Button>
</div>
</div>
))}
</div>
)}
{checkRoutesResult.dischargedOnRoute.length > 0 && (
<div className="mt-3">
<div className="fw-bold mb-2">Discharged Customer Issues</div>
{checkRoutesResult.dischargedOnRoute.map((issue, idx) => (
<div key={`discharged-route-issue-${idx}`} className="mb-2">
Customer {issue.customerName} has been discharged and he is still on route {joinRouteNames(issue.routeNames)}.
</div>
))}
</div>
)}
</>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => setShowCheckRoutesModal(false)}>
Close
</Button>
</Modal.Footer>
</Modal>
</div>
</div>
{/* <div className="list row mb-4">
<div className=" text-primary mb-2">
Today's Date: { new Date().toLocaleDateString() }
</div>
<div className="text-primary mb-2">
<button type="button" className="btn btn-primary btn-sm me-1 mb-2" onClick={()=> syncCustomersInfo()}>{showSyncCustomersLoading? 'Loading...' : `Sync Customers Data`}</button>
<button type="button" className="btn btn-primary btn-sm me-1 mb-2" onClick={()=> generateRouteReport()}>Generate Route Report</button>
{AuthService.canAddOrEditRoutes() &&
<button type="button" className="btn btn-primary btn-sm me-1 mb-2" onClick={() => directToSchedule()}>Plan Tomorrow's Route Schedule</button>
}
<button type="button" className="btn btn-primary btn-sm me-1 mb-2" onClick={()=> createNewRoute()}>Create New Route</button>
<button type="button" className="btn btn-primary btn-sm me-1 mb-2" onClick={()=> copyYesterdayRoutes()}>{showCopyDateLoading? 'Loading...' : `Copy Yesterday Routes`}</button>
<button type="button" className="btn btn-secondary btn-sm me-1 mb-2" onClick={()=> goToHistoryPage()}>View History</button>
<button type="button" className="btn btn-danger btn-sm me-1 mb-2" onClick={()=> cleanAllRoutesStatus()}>Clean All Routes Status</button>
<button type="button" className="btn btn-secondary btn-sm me-1 mb-2" onClick={()=> goToSignature()}>View Routes Signature</button>
</div>
</div> */}
</>
);
};
export default RoutesDashboard;