All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 52s
2263 lines
101 KiB
JavaScript
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}'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}'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}'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}'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; |