diff --git a/app/controllers/resource.controller.js b/app/controllers/resource.controller.js
index 15cbe1c..fc16f27 100644
--- a/app/controllers/resource.controller.js
+++ b/app/controllers/resource.controller.js
@@ -15,6 +15,7 @@ exports.createResource = (req, res) => {
office_name: req.body.office_name || '',
specialty: req.body.specialty,
type: req.body.type, // value may be ['doctor', 'pharmacy' or 'other']
+ type_other: req.body.type_other || '',
// Legacy fields for backward compatibility
name_original: req.body.name_original || req.body.office_name || '',
diff --git a/app/models/resource.model.js b/app/models/resource.model.js
index 7c65a12..2f7fe5c 100644
--- a/app/models/resource.model.js
+++ b/app/models/resource.model.js
@@ -10,6 +10,7 @@ module.exports = mongoose => {
office_name: String, // Merged from name_original and name_branch
specialty: String,
type: String, // value may be ['doctor', 'pharmacy' or 'other']
+ type_other: String,
// Legacy fields for backward compatibility
name_original: String,
diff --git a/client/src/components/event-request/EventRequestList.js b/client/src/components/event-request/EventRequestList.js
index 6bec837..23902d7 100644
--- a/client/src/components/event-request/EventRequestList.js
+++ b/client/src/components/event-request/EventRequestList.js
@@ -36,6 +36,11 @@ const EventRequestList = () => {
label: 'Type',
show: true
},
+ {
+ key: 'transportation',
+ label: 'Transportation',
+ show: true
+ },
{
key: 'symptom',
label: 'Symptom Or Special Need',
@@ -278,6 +283,7 @@ const EventRequestList = () => {
{columns.find(col => col.key === 'resource_display')?.show &&
{eventRequest?.resource_display} }
{columns.find(col => col.key === 'source')?.show && {EventRequestsService.sourceList.find((item) => item?.value === eventRequest?.source)?.label || eventRequest?.source} }
{columns.find(col => col.key === 'type')?.show && {eventRequest?.type} }
+ {columns.find(col => col.key === 'transportation')?.show && {eventRequest?.transportation || '-'} }
{columns.find(col => col.key === 'symptom')?.show && {eventRequest?.symptom} }
{columns.find(col => col.key === 'np')?.show && {eventRequest?.np} }
{columns.find(col => col.key === 'upload')?.show && {eventRequest?.upload} }
diff --git a/client/src/components/events/UpdateEvent.js b/client/src/components/events/UpdateEvent.js
index deaee2f..feacdf9 100644
--- a/client/src/components/events/UpdateEvent.js
+++ b/client/src/components/events/UpdateEvent.js
@@ -585,7 +585,7 @@ const UpdateEvent = () => {
- setShowResourceModal(false)}>
+ setShowResourceModal(false)}>
Select the Provider
diff --git a/client/src/components/resources/CreateResource.js b/client/src/components/resources/CreateResource.js
index 79a10d3..5c4b70a 100644
--- a/client/src/components/resources/CreateResource.js
+++ b/client/src/components/resources/CreateResource.js
@@ -13,6 +13,7 @@ const CreateResource = () => {
const [officeName, setOfficeName] = useState('');
const [specialty, setSpecialty] = useState('');
const [type, setType] = useState('');
+ const [typeOther, setTypeOther] = useState('');
// Contact Information
const [phone, setPhone] = useState(''); // Office Phone Number
@@ -48,6 +49,9 @@ const CreateResource = () => {
if (!phone || phone.trim() === '') {
errors.push('Office Phone Number');
}
+ if (type === 'other' && (!typeOther || typeOther.trim() === '')) {
+ errors.push('Other-please specify');
+ }
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
@@ -74,6 +78,7 @@ const CreateResource = () => {
name_original: officeName, // Legacy field
specialty,
type,
+ type_other: type === 'other' ? typeOther : '',
// Contact Information
phone, // Office Phone Number
@@ -144,13 +149,23 @@ const CreateResource = () => {
Type
-
setType(e.target.value)}>
+ {
+ const nextType = e.target.value;
+ setType(nextType);
+ if (nextType !== 'other') {
+ setTypeOther('');
+ }
+ }}>
Select...
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
{item.label}
))}
+ {type === 'other' &&
+
Other-please specify
+
setTypeOther(e.target.value)} />
+
}
Specialty
setSpecialty(e.target.value)}>
diff --git a/client/src/components/resources/ResourcesList.js b/client/src/components/resources/ResourcesList.js
index 80d1e52..4f3a32f 100644
--- a/client/src/components/resources/ResourcesList.js
+++ b/client/src/components/resources/ResourcesList.js
@@ -347,9 +347,9 @@ const ResourcesList = () => {
{table}
- {/*
+
{table}
- */}
+
setKeyword(e.currentTarget.value)} />
diff --git a/client/src/components/resources/UpdateResource.js b/client/src/components/resources/UpdateResource.js
index c232b7a..8ed4628 100644
--- a/client/src/components/resources/UpdateResource.js
+++ b/client/src/components/resources/UpdateResource.js
@@ -15,6 +15,7 @@ const UpdateResource = () => {
const [officeName, setOfficeName] = useState('');
const [specialty, setSpecialty] = useState('');
const [type, setType] = useState('');
+ const [typeOther, setTypeOther] = useState('');
// Contact Information
const [phone, setPhone] = useState(''); // Office Phone Number
@@ -53,6 +54,9 @@ const UpdateResource = () => {
if (!phone || phone.trim() === '') {
errors.push('Office Phone Number');
}
+ if (type === 'other' && (!typeOther || typeOther.trim() === '')) {
+ errors.push('Other-please specify');
+ }
if (errors.length > 0) {
window.alert(`Please fill in the following required fields:\n${errors.join('\n')}`);
@@ -78,6 +82,7 @@ const UpdateResource = () => {
name_original: officeName, // Legacy field
specialty,
type,
+ type_other: type === 'other' ? typeOther : '',
// Contact Information
phone, // Office Phone Number
@@ -144,6 +149,7 @@ const UpdateResource = () => {
setOfficeName(currentResource?.office_name || currentResource?.name_original || '');
setSpecialty(currentResource?.specialty || '');
setType(currentResource?.type || '');
+ setTypeOther(currentResource?.type_other || '');
// Contact Information
setPhone(currentResource?.phone || '');
@@ -195,13 +201,23 @@ const UpdateResource = () => {
Type
-
setType(e.target.value)}>
+ {
+ const nextType = e.target.value;
+ setType(nextType);
+ if (nextType !== 'other') {
+ setTypeOther('');
+ }
+ }}>
Select...
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
{item.label}
))}
+ {type === 'other' &&
+
Other-please specify
+
setTypeOther(e.target.value)} />
+
}
Specialty
setSpecialty(e.target.value)}>
diff --git a/client/src/components/resources/ViewResource.js b/client/src/components/resources/ViewResource.js
index 0a3747e..4c98892 100644
--- a/client/src/components/resources/ViewResource.js
+++ b/client/src/components/resources/ViewResource.js
@@ -95,7 +95,10 @@ const ViewResource = () => {
Type
-
{RESOURCE_TYPE_TEXT[currentResource?.type] || currentResource?.type || '-'}
+
+ {RESOURCE_TYPE_TEXT[currentResource?.type] || currentResource?.type || '-'}
+ {currentResource?.type === 'other' && currentResource?.type_other ? ` (${currentResource?.type_other})` : ''}
+
diff --git a/client/src/components/trans-routes/RouteView.js b/client/src/components/trans-routes/RouteView.js
index f0495d8..f527510 100644
--- a/client/src/components/trans-routes/RouteView.js
+++ b/client/src/components/trans-routes/RouteView.js
@@ -98,10 +98,8 @@ const RouteView = () => {
try {
setIsSavingRouteStatus(true);
const nextStatus = routeStatusValue ? [routeStatusValue] : [];
- await TransRoutesService.updateRoute(currentRoute.id, {
- ...currentRoute,
- status: nextStatus
- });
+ await TransRoutesService.updateRoute(currentRoute.id, { status: nextStatus });
+ await refreshRouteStatusData();
window.alert('Route status updated successfully.');
} catch (error) {
console.error('Error updating route status:', error);
diff --git a/client/src/components/trans-routes/RoutesDashboard.js b/client/src/components/trans-routes/RoutesDashboard.js
index 5489179..739de83 100644
--- a/client/src/components/trans-routes/RoutesDashboard.js
+++ b/client/src/components/trans-routes/RoutesDashboard.js
@@ -89,6 +89,9 @@ const RoutesDashboard = () => {
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: [], dischargedOnRoute: [] });
+ const [customerTypeFixing, setCustomerTypeFixing] = useState({});
const scheduleImportProgressTimerRef = useRef(null);
@@ -99,6 +102,372 @@ const RoutesDashboard = () => {
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();
+ };
+
+ 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 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 timeKey = getCustomerTimeKey(route, customer);
+ if (!timeKey) 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',
+ timeKey
+ };
+ const existing = byCustomer.get(customerKey) || [];
+ byCustomer.set(customerKey, [...existing, record]);
+ });
+ });
+
+ const issues = [];
+ const seenPairs = new Set();
+ byCustomer.forEach((records) => {
+ if (records.length < 2) return;
+ for (let i = 0; i < records.length; i += 1) {
+ for (let j = i + 1; j < records.length; j += 1) {
+ const first = records[i];
+ const second = records[j];
+ if (first.routeId === second.routeId) continue;
+ if (first.timeKey !== second.timeKey) continue;
+ const pairKey = [first.customerKey, first.timeKey, first.routeId, second.routeId].sort().join('|');
+ if (seenPairs.has(pairKey)) continue;
+ seenPairs.add(pairKey);
+ issues.push({
+ customerName: first.customerName || second.customerName,
+ customerAddressA: first.customerAddress || '',
+ customerAddressB: second.customerAddress || '',
+ routeA: first.routeName,
+ routeB: second.routeName,
+ time: first.timeKey
+ });
+ }
+ }
+ });
+ 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 = normalizeAddress(routeAddress);
+ if (!normalizedRouteAddress) return;
+
+ const storedAddressSet = new Set(
+ getStoredCustomerAddresses(customerProfile)
+ .map((address) => normalizeAddress(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 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 runCheckRoutes = async () => {
+ const inboundIssues = buildRouteConflictsByDirection(tmrInboundRoutes || []);
+ const outboundIssues = buildRouteConflictsByDirection(tmrOutboundRoutes || []);
+ const addressMismatchIssues = buildAddressMismatchIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
+ const customerTypeMismatchIssues = buildCustomerTypeMismatchIssues([...(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, dischargedOnRoute: dischargedOnRouteIssues });
+ setShowCheckRoutesModal(true);
+ };
+
const processRoutesForAbsentCustomers = useCallback((inboundRoutes, outboundRoutes) => {
// Get customers who are absent in inbound routes
const absentCustomerIds = new Set();
@@ -1572,7 +1941,7 @@ const RoutesDashboard = () => {
createVehicle()} disabled={isScheduleImporting}>
Add New Vehicle
-
+
Check Routes
>}
@@ -1691,6 +2060,97 @@ const RoutesDashboard = () => {
{savingTemplate ? <> Saving...> : 'Submit'}
+
+ setShowCheckRoutesModal(false)}>
+
+ Check Routes Result
+
+
+ {(checkRoutesResult.inbound.length === 0 && checkRoutesResult.outbound.length === 0 && checkRoutesResult.attendance.length === 0 && checkRoutesResult.addressMismatch.length === 0 && checkRoutesResult.customerTypeMismatch.length === 0 && checkRoutesResult.dischargedOnRoute.length === 0) ? (
+
+
+ No issue found.
+
+ ) : (
+ <>
+ {checkRoutesResult.inbound.length > 0 && (
+
+
Inbound Issues
+ {checkRoutesResult.inbound.map((issue, idx) => (
+
+ both {issue.routeA} and {issue.routeB} route has {issue.customerName}, {issue.customerName}'s address on {issue.routeA} is {issue.customerAddressA || 'N/A'}, on {issue.routeB} is {issue.customerAddressB || 'N/A'}
+
+ ))}
+
+ )}
+ {checkRoutesResult.outbound.length > 0 && (
+
+
Outbound Issues
+ {checkRoutesResult.outbound.map((issue, idx) => (
+
+ both {issue.routeA} and {issue.routeB} route has {issue.customerName}, {issue.customerName}'s address on {issue.routeA} is {issue.customerAddressA || 'N/A'}, on {issue.routeB} is {issue.customerAddressB || 'N/A'}
+
+ ))}
+
+ )}
+ {checkRoutesResult.attendance.length > 0 && (
+
+
Attendance Note Issues
+ {checkRoutesResult.attendance.map((issue, idx) => (
+
+ {issue.customerName} has an attendance note for {issue.noteDate} and {issue.customerName} is on route {issue.routeNames.join(' and ')}.
+
+ ))}
+
+ )}
+ {checkRoutesResult.addressMismatch.length > 0 && (
+
+
Address Mismatch Issues
+ {checkRoutesResult.addressMismatch.map((issue, idx) => (
+
+ Customer {issue.customerName}'s address: {issue.routeAddress || 'N/A'} is not matching his existing addresses stored in system, on route {joinRouteNames(issue.routeNames)}.
+
+ ))}
+
+ )}
+ {checkRoutesResult.customerTypeMismatch.length > 0 && (
+
+
Member Type Mismatch Issues
+ {checkRoutesResult.customerTypeMismatch.map((issue, idx) => (
+
+
+ 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)}.
+
+
syncCustomerTypeForMismatch(issue)}
+ >
+ {customerTypeFixing[issue.customerId] ? 'Fixing...' : 'Fix'}
+
+
+ ))}
+
+ )}
+ {checkRoutesResult.dischargedOnRoute.length > 0 && (
+
+
Discharged Customer Issues
+ {checkRoutesResult.dischargedOnRoute.map((issue, idx) => (
+
+ Customer {issue.customerName} has been discharged and he is still on route {joinRouteNames(issue.routeNames)}.
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+ setShowCheckRoutesModal(false)}>
+ Close
+
+
diff --git a/client/src/shared/constants/resource.constant.js b/client/src/shared/constants/resource.constant.js
index 7f019a5..f175cac 100644
--- a/client/src/shared/constants/resource.constant.js
+++ b/client/src/shared/constants/resource.constant.js
@@ -1,6 +1,7 @@
// Resource Type Options
export const RESOURCE_TYPE_OPTIONS = [
{ value: 'doctor', label: 'Doctor' },
+ { value: 'cma', label: 'CMA' },
{ value: 'pharmacy', label: 'Pharmacy' },
{ value: 'hospital', label: 'Hospital' },
{ value: 'surgical center', label: 'Surgical Center' },
@@ -10,6 +11,7 @@ export const RESOURCE_TYPE_OPTIONS = [
export const RESOURCE_TYPE_TEXT = {
'doctor': 'Doctor',
+ 'cma': 'CMA',
'pharmacy': 'Pharmacy',
'hospital': 'Hospital',
'surgical center': 'Surgical Center',