fix
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 39s

This commit is contained in:
2026-03-10 14:10:56 -04:00
parent 082b060f4c
commit 22c316989d
8 changed files with 240 additions and 32 deletions

View File

@@ -93,6 +93,29 @@ const uploadPhysicalFile = async (req, res) => {
}
}
const deletePhysicalFile = async (req, res) => {
try {
const { objectId, fileType, model, fileName } = req.body || {};
if (!objectId || !fileType || !model || !fileName) {
return res.status(400).send({ message: 'Required fields missed' });
}
const BASE_UPLOAD_DIR = `/www/wwwroot/upload/`;
const targetFilePath = path.join(BASE_UPLOAD_DIR, model, objectId, fileType, fileName);
if (!fs.existsSync(targetFilePath)) {
return res.status(200).send({ message: 'File already removed.' });
}
await fs.promises.unlink(targetFilePath);
return res.status(200).send({ message: 'File deleted successfully.' });
} catch (error) {
return res.status(500).send({
message: error.message,
});
}
}
const getFilesByType = async (req, res) => {
try {
const {objectId, fileType, name, model} = req.params;
@@ -158,5 +181,6 @@ module.exports = {
getFile,
deleteFile,
uploadPhysicalFile,
deletePhysicalFile,
getFilesByType
};

View File

@@ -17,6 +17,7 @@ module.exports = app => {
router.post("/upload/:filename", upload.uploadFiles);
router.post("/upload-physical", handleUploadMiddleware, upload.uploadPhysicalFile);
router.post("/delete", upload.deleteFile);
router.post("/delete-physical", upload.deletePhysicalFile);
router.get("/uploadedDocs/:model/:objectId/type/:fileType/name/:name", upload.getFilesByType);
app.use('/api/files', router);
};

View File

@@ -1,6 +1,6 @@
import React, {useState} from "react";
import { useDispatch } from "react-redux";
import { CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE } from "../../shared";
import { CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE, PROGRAM_TYPE_TEXT, PAY_SOURCE_TEXT } from "../../shared";
import { Modal, Button } from "react-bootstrap";
import { transRoutesSlice } from "./../../store";
import { CSVLink } from "react-csv";
@@ -153,6 +153,23 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
return dateObj;
}
const getDisplayNameWithProgramAndPaySource = (customer) => {
const baseName = customer?.customer_name || '';
const programTypeRaw = (customer?.customer_program_type || customer?.program_type || '').toString().trim();
const paySourceRaw = (customer?.customer_pay_source || customer?.pay_source || '').toString().trim();
const qualifiers = [];
if (programTypeRaw && programTypeRaw.toLowerCase() !== 'amdc') {
qualifiers.push(PROGRAM_TYPE_TEXT[programTypeRaw] || programTypeRaw);
}
if (paySourceRaw && paySourceRaw.toLowerCase() !== 'medicaid') {
qualifiers.push(PAY_SOURCE_TEXT[paySourceRaw] || paySourceRaw);
}
if (qualifiers.length === 0) return baseName;
return `${baseName} (${qualifiers.join(', ')})`;
};
const saveRouteCustomerInfo = () => {
const routeId = customerInEdit.routeId;
let removeSignature = false
@@ -752,8 +769,7 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
<th>Leave Center Time</th>
<th>Drop Off Time</th>
{showCompletedInfo && (<th>Schedule Absent</th>)}
{showCompletedInfo && (<th>Schedule Absent Note</th>)}
{showCompletedInfo && (<th>Special Needs</th>)}
{showCompletedInfo && (<th>Note</th>)}
{showCompletedInfo && (<th>Pickup Order</th>)}
{showCompletedInfo && (<th>Estimated Pickup Time</th>)}
{!showCompletedInfo && (<th>Vehicle Number</th>)}
@@ -802,7 +818,7 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
return (<tr key={index}>
<td className="td-index"> {index + 1}</td>
<td>
{ customer.customer_name}
{ getDisplayNameWithProgramAndPaySource(customer)}
</td>
{showCompletedInfo && (<td>
{ customer.customer_address_override || customer.customer_address }
@@ -826,9 +842,6 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{showCompletedInfo && (<td>
{ customer.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }
</td>)}
{showCompletedInfo && (<td>
{ customer.customer_note }
</td>)}
{showCompletedInfo && (<td>
{ customer.customer_special_needs }
</td>)}
@@ -857,7 +870,7 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
return (<tr key={index} >
<td className="td-index">{stopNo}</td>
<td>
{ customerItem.customer_name}
{ getDisplayNameWithProgramAndPaySource(customerItem)}
</td>
{showCompletedInfo && (<td>
{ customerItem.customer_address_override || customerItem.customer_address }
@@ -881,9 +894,6 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{showCompletedInfo && (<td>
{ customerItem.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }
</td>)}
{showCompletedInfo && (<td>
{ customerItem.customer_note }
</td>)}
{showCompletedInfo && (<td>
{ customerItem.customer_special_needs }
</td>)}
@@ -916,7 +926,7 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
customerItem.customers?.map((customer) => (<tr key={customer.customer_id} style={groupedRowsStyle} onClick={() => openForceEditModal(customer)}>
<td className="td-index"></td>
<td className="children">
{ customer.customer_name}
{ getDisplayNameWithProgramAndPaySource(customer)}
</td>
{showCompletedInfo && (<td>
{ customer.customer_address_override || customer.customer_address }
@@ -940,9 +950,6 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
{showCompletedInfo && (<td>
{ customer.customer_pickup_status === PICKUP_STATUS.SCHEDULE_ABSENT ? 'Yes' : "No" }
</td>)}
{showCompletedInfo && (<td>
{ customer.customer_note }
</td>)}
{showCompletedInfo && (<td>
{ customer.customer_special_needs }
</td>)}
@@ -1044,13 +1051,15 @@ const PersonnelInfoTable = ({transRoutes, showCompletedInfo,
</div>
</div>
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note
</div>
<textarea value={customerNote} onChange={(e) => {setCustomerNote(e.target.value)}}></textarea>
</div>
</div>
{false && (
<div className="app-main-content-fields-section">
<div className="me-4">
<div className="field-label">Note
</div>
<textarea value={customerNote} onChange={(e) => {setCustomerNote(e.target.value)}}></textarea>
</div>
</div>
)}
</>
</Modal.Body>
<Modal.Footer>

View File

@@ -416,8 +416,20 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
}
const addPersonnel = () => {
const result = [].concat(customers).concat(newRouteCustomerList);
setCustomers(result.filter((item, pos) => result.indexOf(item) === pos));
const merged = [...customers];
newRouteCustomerList.forEach((newCustomer) => {
const existingIndex = merged.findIndex((item) => item?.customer_id === newCustomer?.customer_id);
if (existingIndex >= 0) {
// If the customer already exists, overwrite with latest selection (including address).
merged[existingIndex] = {
...merged[existingIndex],
...newCustomer,
};
} else {
merged.push(newCustomer);
}
});
setCustomers(merged);
setShowAddPersonnelModal(false);
setNewRouteCustomerList([]);
}

View File

@@ -4,7 +4,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { selectAllRoutes, selectTomorrowAllRoutes, selectAllActiveDrivers, selectAllActiveVehicles, selectHistoryRoutes } from "../../store";
import { CustomerService, SignatureRequestService, EventsService } from "../../services";
import moment from 'moment';
import { CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS } from "../../shared";
import { CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS, PROGRAM_TYPE_TEXT, PAY_SOURCE_TEXT } from "../../shared";
const RouteReportWithSignature = () => {
const params = useParams();
@@ -121,6 +121,23 @@ const RouteReportWithSignature = () => {
return safeA - safeB;
});
const getDisplayNameWithProgramAndPaySource = (customer) => {
const baseName = customer?.customer_name || '';
const programTypeRaw = (customer?.customer_program_type || customer?.program_type || '').toString().trim();
const paySourceRaw = (customer?.customer_pay_source || customer?.pay_source || '').toString().trim();
const qualifiers = [];
if (programTypeRaw && programTypeRaw.toLowerCase() !== 'amdc') {
qualifiers.push(PROGRAM_TYPE_TEXT[programTypeRaw] || programTypeRaw);
}
if (paySourceRaw && paySourceRaw.toLowerCase() !== 'medicaid') {
qualifiers.push(PAY_SOURCE_TEXT[paySourceRaw] || paySourceRaw);
}
if (qualifiers.length === 0) return baseName;
return `${baseName} (${qualifiers.join(', ')})`;
};
return (
<>
<style>
@@ -253,7 +270,7 @@ const RouteReportWithSignature = () => {
return (
<tr key={index}>
<td style={{ textAlign: 'center' }}>{index + 1}</td>
<td>{customer?.customer_name}</td>
<td>{getDisplayNameWithProgramAndPaySource(customer)}</td>
<td>{customer?.customer_phone}</td>
<td>{customer?.customer_address_override || customer?.customer_address}</td>
<td></td>

View File

@@ -86,8 +86,9 @@ const RoutesDashboard = () => {
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 [checkRoutesResult, setCheckRoutesResult] = useState({ inbound: [], outbound: [], attendance: [], addressMismatch: [], customerTypeMismatch: [], customerSpecialNeedsMismatch: [], dischargedOnRoute: [] });
const [customerTypeFixing, setCustomerTypeFixing] = useState({});
const [customerSpecialNeedsFixing, setCustomerSpecialNeedsFixing] = useState({});
const scheduleImportProgressTimerRef = useRef(null);
@@ -424,6 +425,40 @@ const RoutesDashboard = () => {
}));
};
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 syncCustomerTypeForMismatch = async (issue) => {
if (!issue?.customerId || !issue?.dbType || !issue?.mismatchedRoutes?.length) return;
setCustomerTypeFixing((prev) => Object.assign({}, prev, { [issue.customerId]: true }));
@@ -452,11 +487,40 @@ const RoutesDashboard = () => {
}
};
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 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 dischargedOnRouteIssues = buildDischargedCustomerRouteIssues([...(tmrInboundRoutes || []), ...(tmrOutboundRoutes || [])]);
let attendanceIssues = [];
try {
@@ -464,7 +528,7 @@ const RoutesDashboard = () => {
} catch (error) {
console.error('Error checking attendance notes against routes:', error);
}
setCheckRoutesResult({ inbound: inboundIssues, outbound: outboundIssues, attendance: attendanceIssues, addressMismatch: addressMismatchIssues, customerTypeMismatch: customerTypeMismatchIssues, dischargedOnRoute: dischargedOnRouteIssues });
setCheckRoutesResult({ inbound: inboundIssues, outbound: outboundIssues, attendance: attendanceIssues, addressMismatch: addressMismatchIssues, customerTypeMismatch: customerTypeMismatchIssues, customerSpecialNeedsMismatch: customerSpecialNeedsMismatchIssues, dischargedOnRoute: dischargedOnRouteIssues });
setShowCheckRoutesModal(true);
};
@@ -749,6 +813,15 @@ const RoutesDashboard = () => {
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('');
@@ -1760,7 +1833,16 @@ const RoutesDashboard = () => {
}).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>{name}</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>
@@ -1968,7 +2050,7 @@ const RoutesDashboard = () => {
<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) ? (
{(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.dischargedOnRoute.length === 0) ? (
<div className="text-success d-flex align-items-center">
<Check size={18} className="me-2"></Check>
<span>No issue found.</span>
@@ -2035,6 +2117,26 @@ const RoutesDashboard = () => {
))}
</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 d-flex align-items-center justify-content-between" style={{ gap: '12px' }}>
<div>
Customer {issue.customerName}&apos;s note to driver is mismatched on route {joinRouteNames(issue.mismatchedRoutes.map((route) => route.routeName))}. Latest note in system is {issue.dbNoteToDriver || 'empty'}.
</div>
<Button
variant="outline-primary"
size="sm"
disabled={!!customerSpecialNeedsFixing[issue.customerId]}
onClick={() => syncCustomerSpecialNeedsForMismatch(issue)}
>
{customerSpecialNeedsFixing[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>

View File

@@ -4,7 +4,7 @@ import { useSelector, useDispatch } from "react-redux";
import { AuthService, VehicleRepairService, VehicleService } from "../../services";
import { vehicleSlice, selectVehicleError } from "./../../store";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
import { Download, PencilSquare, Archive } from "react-bootstrap-icons";
import { Download, PencilSquare, Archive, Trash } from "react-bootstrap-icons";
import moment from "moment";
import { Export } from "../../shared/components";
import {
@@ -55,6 +55,37 @@ const ViewVehicle = () => {
});
}
const refreshVehicleDocuments = async () => {
if (!currentVehicle?.id || !currentVehicle?.vehicle_number) return;
const getInspectionDate = (name) => {
const arr1 = name.split('.');
const prefix = arr1[0];
if (prefix) {
const arr2 = prefix.split('_');
const dateNumber = arr2[arr2.length - 1];
return dateNumber ? new Date(parseInt(dateNumber)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}) : moment().format('MM/DD/YYYY');
} else {
return moment().format('MM/DD/YYYY');
}
};
const monthlyInspectionDocs = (await VehicleService.getAllVechileFiles(currentVehicle.id, currentVehicle.vehicle_number, 'monthlyInspection'))?.data?.data?.files || [];
const yearlyInspectionDocs = (await VehicleService.getAllVechileFiles(currentVehicle.id, currentVehicle.vehicle_number, 'yearlyInspection'))?.data?.data?.files || [];
setMonthlyDocs(monthlyInspectionDocs?.map(item => ({ ...item, inspectionDate: getInspectionDate(item?.name) })));
setYearlyDocs(yearlyInspectionDocs?.map(item => ({ ...item, inspectionDate: getInspectionDate(item?.name) })));
};
const deleteInspectionFile = async (fileType, fileName) => {
if (!currentVehicle?.id || !fileName) return;
if (!window.confirm('Are you sure you want to delete this inspection file?')) return;
try {
await VehicleService.deleteVechileFile(currentVehicle.id, fileType, fileName);
await refreshVehicleDocuments();
} catch (error) {
console.error('Failed to delete inspection file:', error);
window.alert('Failed to delete inspection file.');
}
};
const deactivateVehicle = () => {
const data = {
status: 'inactive'
@@ -310,6 +341,7 @@ const ViewVehicle = () => {
<td className="td-index">{index + 1}</td>
<td>
<PencilSquare size={14} className="clickable me-2" onClick={() => navigate(`/vehicles/${currentVehicle?.id}/inspections/monthly/edit?fileName=${encodeURIComponent(doc?.name)}&date=${encodeURIComponent(doc?.inspectionDate || '')}`)} />
<Trash size={14} className="clickable me-2 text-danger" onClick={() => deleteInspectionFile('monthlyInspection', doc?.name)} />
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a>
</td>
<td>{doc?.inspectionDate}</td>
@@ -342,6 +374,7 @@ const ViewVehicle = () => {
<td className="td-index">{index + 1}</td>
<td>
<PencilSquare size={14} className="clickable me-2" onClick={() => navigate(`/vehicles/${currentVehicle?.id}/inspections/yearly/edit?fileName=${encodeURIComponent(doc?.name)}&date=${encodeURIComponent(doc?.inspectionDate || '')}`)} />
<Trash size={14} className="clickable me-2 text-danger" onClick={() => deleteInspectionFile('yearlyInspection', doc?.name)} />
<a className="btn btn-link btn-sm" href={doc?.url} target="_blank">{doc?.name}</a>
</td>
<td>{doc?.inspectionDate}</td>

View File

@@ -31,6 +31,15 @@ const getAllVechileFiles = (vehicleId, name, fileType) => {
return http.get(`/files/uploadedDocs/vehicle/${vehicleId}/type/${fileType}/name/${name}`)
}
const deleteVechileFile = (vehicleId, fileType, fileName) => {
return http.post('/files/delete-physical', {
model: 'vehicle',
objectId: vehicleId,
fileType,
fileName
});
}
export const VehicleService = {
getAll,
getAllActiveVehicles,
@@ -40,5 +49,6 @@ export const VehicleService = {
getVehicle,
convertToDate,
uploadVechileFile,
getAllVechileFiles
getAllVechileFiles,
deleteVechileFile
};