This commit is contained in:
@@ -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 || '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && <td>{eventRequest?.resource_display}</td>}
|
||||
{columns.find(col => col.key === 'source')?.show && <td>{EventRequestsService.sourceList.find((item) => item?.value === eventRequest?.source)?.label || eventRequest?.source}</td>}
|
||||
{columns.find(col => col.key === 'type')?.show && <td>{eventRequest?.type}</td>}
|
||||
{columns.find(col => col.key === 'transportation')?.show && <td>{eventRequest?.transportation || '-'}</td>}
|
||||
{columns.find(col => col.key === 'symptom')?.show && <td>{eventRequest?.symptom}</td>}
|
||||
{columns.find(col => col.key === 'np')?.show && <td>{eventRequest?.np}</td>}
|
||||
{columns.find(col => col.key === 'upload')?.show && <td>{eventRequest?.upload}</td>}
|
||||
|
||||
@@ -585,7 +585,7 @@ const UpdateEvent = () => {
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<Modal show={showResourceModal} fullscreen={'xxl-down'} onHide={() => setShowResourceModal(false)}>
|
||||
<Modal show={showResourceModal} fullscreen={true} onHide={() => setShowResourceModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Select the Provider</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Type</div>
|
||||
<select value={type} onChange={e => setType(e.target.value)}>
|
||||
<select value={type} onChange={e => {
|
||||
const nextType = e.target.value;
|
||||
setType(nextType);
|
||||
if (nextType !== 'other') {
|
||||
setTypeOther('');
|
||||
}
|
||||
}}>
|
||||
<option value="">Select...</option>
|
||||
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
|
||||
<option key={index} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{type === 'other' && <div className="me-4">
|
||||
<div className="field-label">Other-please specify</div>
|
||||
<input type="text" value={typeOther} onChange={e => setTypeOther(e.target.value)} />
|
||||
</div>}
|
||||
<div className="me-4">
|
||||
<div className="field-label">Specialty</div>
|
||||
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
|
||||
|
||||
@@ -347,9 +347,9 @@ const ResourcesList = () => {
|
||||
<Tab eventKey="activeProviders" title="Active Providers">
|
||||
{table}
|
||||
</Tab>
|
||||
{/* <Tab eventKey="archivedProviders" title="Archived Providers">
|
||||
<Tab eventKey="archivedProviders" title="Archived Providers">
|
||||
{table}
|
||||
</Tab> */}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div className="list-func-panel">
|
||||
<input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Type</div>
|
||||
<select value={type} onChange={e => setType(e.target.value)}>
|
||||
<select value={type} onChange={e => {
|
||||
const nextType = e.target.value;
|
||||
setType(nextType);
|
||||
if (nextType !== 'other') {
|
||||
setTypeOther('');
|
||||
}
|
||||
}}>
|
||||
<option value="">Select...</option>
|
||||
{RESOURCE_TYPE_OPTIONS.map((item, index) => (
|
||||
<option key={index} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{type === 'other' && <div className="me-4">
|
||||
<div className="field-label">Other-please specify</div>
|
||||
<input type="text" value={typeOther} onChange={e => setTypeOther(e.target.value)} />
|
||||
</div>}
|
||||
<div className="me-4">
|
||||
<div className="field-label">Specialty</div>
|
||||
<select value={specialty} onChange={e => setSpecialty(e.target.value)}>
|
||||
|
||||
@@ -95,7 +95,10 @@ const ViewResource = () => {
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field-label">Type</div>
|
||||
<div className="field-value">{RESOURCE_TYPE_TEXT[currentResource?.type] || currentResource?.type || '-'}</div>
|
||||
<div className="field-value">
|
||||
{RESOURCE_TYPE_TEXT[currentResource?.type] || currentResource?.type || '-'}
|
||||
{currentResource?.type === 'other' && currentResource?.type_other ? ` (${currentResource?.type_other})` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
<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" disabled={isScheduleImporting}>
|
||||
<button className="btn btn-outline-secondary me-2" onClick={runCheckRoutes} disabled={isScheduleImporting}>
|
||||
<Check size={16} className="me-2"></Check>Check Routes
|
||||
</button>
|
||||
</>}
|
||||
@@ -1691,6 +2060,97 @@ const RoutesDashboard = () => {
|
||||
{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.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">
|
||||
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'}
|
||||
</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">
|
||||
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'}
|
||||
</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} has an attendance note for {issue.noteDate} and {issue.customerName} is on route {issue.routeNames.join(' and ')}.
|
||||
</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.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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user