All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 31s
374 lines
17 KiB
JavaScript
374 lines
17 KiB
JavaScript
import React, {useState, useEffect} from "react";
|
|
import { useSelector } from "react-redux";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { selectAllRoutes, selectTomorrowAllRoutes, selectAllActiveDrivers, selectAllActiveVehicles, selectHistoryRoutes } from "./../../store";
|
|
import PersonnelSection from "./PersonnelSection";
|
|
import { AuthService, CustomerService, SignatureRequestService, EmployeeService, TransRoutesService } from "../../services";
|
|
import { ROUTE_STATUS } from "../../shared";
|
|
import moment from 'moment';
|
|
import { Breadcrumb, Tabs, Tab, Dropdown, Spinner, Modal, Button } from "react-bootstrap";
|
|
import { Download, Pencil } from "react-bootstrap-icons";
|
|
import RouteCustomerEditor from "./RouteCustomerEditor";
|
|
|
|
const RouteView = () => {
|
|
const params = useParams();
|
|
const allRoutes = useSelector(selectAllRoutes);
|
|
const tomorrowRoutes = useSelector(selectTomorrowAllRoutes);
|
|
const historyRoutes = useSelector(selectHistoryRoutes);
|
|
const drivers = useSelector(selectAllActiveDrivers);
|
|
const vehicles = useSelector(selectAllActiveVehicles);
|
|
const currentRoute = (allRoutes.find(item => item.id === params.id)) || (tomorrowRoutes.find(item => item.id === params.id)) || (historyRoutes.find(item => item.id === params.id));
|
|
const currentVehicle = vehicles.find(item => item.id === currentRoute?.vehicle );
|
|
const currentDriver = drivers.find(item => item.id === currentRoute?.driver);
|
|
const [fallbackDriver, setFallbackDriver] = useState(undefined);
|
|
const [showVehicleDetails, setShowVehicleDetails] = useState(false);
|
|
const [signature, setSignature] = useState(undefined);
|
|
const [signatureRequest, setSignatureRequest] = useState(undefined);
|
|
const [routeStatusValue, setRouteStatusValue] = useState('');
|
|
const [isSavingRouteStatus, setIsSavingRouteStatus] = useState(false);
|
|
const [latestRouteForStatus, setLatestRouteForStatus] = useState(undefined);
|
|
const [routeSnapshot, setRouteSnapshot] = useState(undefined);
|
|
const [customerMetaById, setCustomerMetaById] = useState(new Map());
|
|
const paramsQuery = new URLSearchParams(window.location.search);
|
|
const scheduleDate = paramsQuery.get('dateSchedule');
|
|
|
|
const navigate = useNavigate();
|
|
const resolvedDriverId = currentDriver?.id || fallbackDriver?.id || currentRoute?.driver;
|
|
const resolvedDriverName = currentDriver?.name || fallbackDriver?.name || '';
|
|
const routeStatusOptions = [
|
|
{ value: '', label: 'Not Started' },
|
|
{ value: ROUTE_STATUS.ENROUTE, label: 'En Route' },
|
|
{ value: ROUTE_STATUS.ENROUTE_TO_CENTER, label: 'En Route To Center' },
|
|
{ value: ROUTE_STATUS.DROPPED_OFF_ALL, label: 'Dropped Off All' },
|
|
{ value: ROUTE_STATUS.SIGN_OFF, label: 'Signed Off' },
|
|
{ value: ROUTE_STATUS.UNEXPECTED_ABSENT, label: 'Unexpected Absent' },
|
|
];
|
|
const mergeRouteCustomerMeta = (route) => {
|
|
if (!route || !Array.isArray(route?.route_customer_list) || customerMetaById.size === 0) {
|
|
return route;
|
|
}
|
|
return Object.assign({}, route, {
|
|
route_customer_list: route.route_customer_list.map((customerInRoute) => {
|
|
const customerMeta = customerMetaById.get(customerInRoute?.customer_id);
|
|
if (!customerMeta) return customerInRoute;
|
|
return Object.assign({}, customerInRoute, {
|
|
customer_program_type: customerMeta.program_type || '',
|
|
customer_pay_source: customerMeta.pay_source || ''
|
|
});
|
|
})
|
|
});
|
|
};
|
|
const routeForStatusView = mergeRouteCustomerMeta(latestRouteForStatus || routeSnapshot || currentRoute);
|
|
const routeForAssignmentView = routeSnapshot || currentRoute;
|
|
const closeModal = () => {
|
|
setShowVehicleDetails(false);
|
|
}
|
|
const openModal = () => {
|
|
setShowVehicleDetails(true);
|
|
}
|
|
const getRelatedOutboundRoutesForThisView = () => {
|
|
if (allRoutes.find(item => item.id === params.id)) {
|
|
return allRoutes.filter(item => item.type==='outbound');
|
|
}
|
|
if (tomorrowRoutes.find(item => item.id === params.id)) {
|
|
return tomorrowRoutes.filter(item => item.type==='outbound');
|
|
}
|
|
if (historyRoutes.find(item => item.id === params.id)) {
|
|
return historyRoutes.filter(item => item.type==='outbound');
|
|
}
|
|
}
|
|
const directToDashboad = () => {
|
|
navigate(`/trans-routes/dashboard?dateSchedule=${moment(currentRoute?.schedule_date).format('YYYY-MM-DD')}`);
|
|
}
|
|
const edit = (editSection) => {
|
|
if (scheduleDate) {
|
|
navigate(`/trans-routes/edit/${currentRoute?.id}?dateSchedule=${scheduleDate}&editSection=${editSection}`)
|
|
} else {
|
|
navigate(`/trans-routes/edit/${currentRoute?.id}?editSection=${editSection}`)
|
|
}
|
|
}
|
|
const deleteFile = () => {
|
|
if (signature) {
|
|
const dateArr = moment(currentRoute?.schedule_date)?.format('MM/DD/YYYY')?.split('/') || [];
|
|
CustomerService.deleteFile({name: `${currentRoute?.id}_${currentRoute?.driver}_${dateArr[0]}_${dateArr[1]}`}).then(data => {
|
|
setSignature(undefined);
|
|
})
|
|
}
|
|
}
|
|
const generateSignatureRequest = () => {
|
|
if (!resolvedDriverId) {
|
|
window.alert('Driver is not assigned for this route.');
|
|
return;
|
|
}
|
|
SignatureRequestService.createNewSignatureRequest({
|
|
driver_id: resolvedDriverId,
|
|
driver_name: resolvedDriverName,
|
|
route_id: currentRoute?.id,
|
|
route_date: currentRoute?.schedule_date,
|
|
route_name: currentRoute?.name,
|
|
status: 'active',
|
|
}).then((data) => {
|
|
setSignatureRequest(data.data);
|
|
})
|
|
}
|
|
const saveRouteStatus = async () => {
|
|
if (!currentRoute?.id || isSavingRouteStatus) return;
|
|
try {
|
|
setIsSavingRouteStatus(true);
|
|
const nextStatus = routeStatusValue ? [routeStatusValue] : [];
|
|
await TransRoutesService.updateRoute(currentRoute.id, { status: nextStatus });
|
|
await refreshRouteStatusData();
|
|
window.alert('Route status updated successfully.');
|
|
} catch (error) {
|
|
console.error('Error updating route status:', error);
|
|
window.alert('Failed to update route status.');
|
|
} finally {
|
|
setIsSavingRouteStatus(false);
|
|
}
|
|
};
|
|
|
|
const refreshRouteStatusData = async () => {
|
|
if (!currentRoute?.id) return;
|
|
try {
|
|
const response = await TransRoutesService.getRoute(currentRoute.id);
|
|
if (response?.data) {
|
|
setLatestRouteForStatus(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error refreshing route status data:', error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const currentStatus = Array.isArray(currentRoute?.status) ? currentRoute.status[0] : '';
|
|
setRouteStatusValue(currentStatus || '');
|
|
}, [currentRoute?.id, currentRoute?.status]);
|
|
|
|
useEffect(() => {
|
|
setLatestRouteForStatus(undefined);
|
|
}, [currentRoute?.id]);
|
|
|
|
useEffect(() => {
|
|
const routeId = currentRoute?.id || params?.id;
|
|
if (!routeId) return;
|
|
TransRoutesService.getRoute(routeId)
|
|
.then((response) => {
|
|
if (response?.data) {
|
|
setRouteSnapshot(response.data);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
setRouteSnapshot(undefined);
|
|
});
|
|
}, [currentRoute?.id, params?.id]);
|
|
|
|
useEffect(() => {
|
|
CustomerService.getAllCustomers()
|
|
.then((res) => {
|
|
const nextMap = new Map();
|
|
(res?.data || []).forEach((customer) => {
|
|
nextMap.set(customer?.id, {
|
|
program_type: customer?.program_type || '',
|
|
pay_source: customer?.pay_source || ''
|
|
});
|
|
});
|
|
setCustomerMetaById(nextMap);
|
|
})
|
|
.catch(() => {
|
|
setCustomerMetaById(new Map());
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!currentRoute?.driver || currentDriver?.id) {
|
|
setFallbackDriver(undefined);
|
|
return;
|
|
}
|
|
EmployeeService.getEmployee(currentRoute?.driver)
|
|
.then((data) => {
|
|
setFallbackDriver(data?.data);
|
|
})
|
|
.catch(() => {
|
|
setFallbackDriver(undefined);
|
|
});
|
|
}, [currentRoute?.driver, currentDriver?.id]);
|
|
|
|
useEffect(() => {
|
|
const dateArr = moment(currentRoute?.schedule_date)?.format('MM/DD/YYYY')?.split('/') || [];
|
|
|
|
CustomerService.getAvatar(`${currentRoute?.id}_${currentRoute?.driver}_${dateArr[0]}_${dateArr[1]}`).then(data => {
|
|
setSignature(data.data);
|
|
});
|
|
SignatureRequestService.getAllSignatureRequests({driver_id: resolvedDriverId, route_id: currentRoute?.id, route_date: currentRoute?.schedule_date}).then((data) => {
|
|
if (data?.data?.length > 0) {
|
|
setSignatureRequest(data?.data[0]);
|
|
}
|
|
})
|
|
}, [currentRoute, resolvedDriverId]);
|
|
return (
|
|
<>
|
|
<div className="list row mb-4">
|
|
<Breadcrumb>
|
|
<Breadcrumb.Item href="/trans-routes/dashboard">Transportation</Breadcrumb.Item>
|
|
<Breadcrumb.Item href="/trans-routes/dashboard">
|
|
Transportation Routes
|
|
</Breadcrumb.Item>
|
|
<Breadcrumb.Item active>
|
|
View Route
|
|
</Breadcrumb.Item>
|
|
</Breadcrumb>
|
|
<div className="col-md-12 text-primary">
|
|
<h4>
|
|
View Route Information <button className="btn btn-link btn-sm" onClick={() => {directToDashboad()}}>Back</button>
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
<div className="app-main-content-list-container form-page">
|
|
<div className="app-main-content-list-func-container">
|
|
<Tabs defaultActiveKey="routeOverview" id="route-view-tab">
|
|
<Tab eventKey="routeOverview" title="Route Information">
|
|
<h6 className="text-primary">Route Details <button className="btn btn-sm btn-primary" onClick={() => edit('info')}><Pencil size={16} className="me-2"></Pencil>Edit </button></h6>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="field-body">
|
|
<div className="field-label">Route Name</div>
|
|
<div className="field-value">{currentRoute?.name}</div>
|
|
</div>
|
|
<div className="field-body">
|
|
<div className="field-label">Vehicle</div>
|
|
<div className="field-value">{currentVehicle?.vehicle_number}</div>
|
|
</div>
|
|
<div className="field-body">
|
|
<div className="field-label">Driver</div>
|
|
<div className="field-value">{resolvedDriverName || 'Not assigned'}</div>
|
|
</div>
|
|
<div className="field-body">
|
|
<div className="field-label">Route Type</div>
|
|
<div className="field-value">{currentRoute?.type}</div>
|
|
</div>
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="field-body">
|
|
<div className="field-label">Route Start Time</div>
|
|
<div className="field-value">{currentRoute?.start_time && (new Date(currentRoute?.start_time))?.toLocaleTimeString()}</div>
|
|
</div>
|
|
<div className="field-body">
|
|
<div className="field-label">Route End Time</div>
|
|
<div className="field-value">{currentRoute?.end_time && (new Date(currentRoute?.end_time))?.toLocaleTimeString()}</div>
|
|
</div>
|
|
{currentRoute?.type === 'outbound' &&<div className="field-body">
|
|
<div className="field-label">Leave Center Time</div>
|
|
<div className="field-value">{currentRoute?.start_time && (new Date(currentRoute?.start_time))?.toLocaleTimeString()}</div>
|
|
</div>}
|
|
{currentRoute?.type === 'outbound' &&<div className="field-body">
|
|
<div className="field-label">Estimated Start Time</div>
|
|
<div className="field-value">{currentRoute?.estimated_start_time && (new Date(currentRoute?.estimated_start_time))?.toLocaleTimeString()}</div>
|
|
</div>}
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="field-body">
|
|
<div className="field-label">Start Mileage</div>
|
|
<div className="field-value">{ currentRoute?.start_mileage}</div>
|
|
</div>
|
|
<div className="field-body">
|
|
<div className="field-label">End Mileage</div>
|
|
<div className="field-value">{ currentRoute?.end_mileage}</div>
|
|
</div>
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
{signature &&<div className="field-body">
|
|
<div className="field-label">Driver Signature</div>
|
|
<div className="field-value">
|
|
{signature && <img width="100px" src={`data:image/jpg;base64, ${signature}`}/>}
|
|
|
|
</div>
|
|
</div>}
|
|
{!signature && !signatureRequest && <div className="field-body">
|
|
<div className="field-label">Signature Request</div>
|
|
<div className="field-value"><button className="btn btn-sm btn-primary" onClick={() => generateSignatureRequest()}>Generate Signature Link For Driver</button></div>
|
|
</div>}
|
|
{!signature && signatureRequest && <div className="alert alert-success fade show mb-2 mt-2" role="alert">
|
|
<div>Please send this to the driver to get signature:</div>
|
|
<div>{`${window.location.origin}/signature/${signatureRequest?.id}`}</div>
|
|
</div>}
|
|
</div>
|
|
<div className="app-main-content-fields-section">
|
|
<div className="field-body">
|
|
<div className="field-label">Checklist</div>
|
|
<div className="field-value">
|
|
{currentRoute && currentRoute?.checklist_result?.map(item => <div>{`${item?.item}: ${item?.result ? 'Yes': "No"}`}</div>)}
|
|
{currentRoute && currentRoute?.checklist_result.length === 0 && <>No Checklist found</>}</div>
|
|
</div>
|
|
</div>
|
|
<RouteCustomerEditor currentRoute={routeForAssignmentView} viewMode={true} editFun={edit}></RouteCustomerEditor>
|
|
</Tab>
|
|
<Tab eventKey="routeStatus" title="Route Status">
|
|
<div className="list row">
|
|
<div className="col-md-12 mb-3">
|
|
<div className="app-main-content-fields-section">
|
|
<div className="field-body">
|
|
<div className="field-label">Route Status</div>
|
|
<div className="field-value d-flex align-items-center gap-2">
|
|
<select
|
|
className="form-select"
|
|
style={{ width: '260px' }}
|
|
value={routeStatusValue}
|
|
onChange={(e) => setRouteStatusValue(e.target.value)}
|
|
disabled={isSavingRouteStatus}
|
|
>
|
|
{routeStatusOptions.map((option) => (
|
|
<option key={option.value || 'not-started'} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
className="btn btn-primary btn-sm"
|
|
onClick={saveRouteStatus}
|
|
disabled={isSavingRouteStatus}
|
|
>
|
|
{isSavingRouteStatus ? 'Saving...' : 'Save Status'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-md-12 mb-4">
|
|
{routeForStatusView && <PersonnelSection transRoutes={[routeForStatusView]} showCompletedInfo={true} showGroupInfo={true} isInbound={routeForStatusView?.type === 'inbound' } allowForceEdit={AuthService.canViewRoutes()} sectionName="Personnel Status (click on each user to edit)" relatedOutbound={getRelatedOutboundRoutesForThisView()} vehicle={currentVehicle} driverName={resolvedDriverName} deleteFile={deleteFile} onRouteUpdated={refreshRouteStatusData}/>}
|
|
</div>
|
|
</div>
|
|
</Tab>
|
|
</Tabs>
|
|
{/* <div className="list-func-panel">
|
|
<button className="btn btn-primary"><Download size={16} className="me-2"></Download>Export</button>
|
|
</div> */}
|
|
</div>
|
|
<Modal show={showVehicleDetails} onHide={() => closeModal()}>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title>Vehicle Info</Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<>
|
|
<div className="mb-2">Vehicle Number: {currentVehicle?.vehicle_number}</div>
|
|
<div className="mb-2">Tag: {currentVehicle?.tag}</div>
|
|
<div className="mb-2">EzPass: {currentVehicle?.ezpass}</div>
|
|
<div className="mb-2">GPS: {currentVehicle?.gps_tag}</div>
|
|
<div className="mb-2">Capacity: {currentVehicle?.capacity}</div>
|
|
<div className="mb-2">Status: {currentVehicle?.status}</div>
|
|
<div className="mb-2">Mileage: {currentVehicle?.mileage}</div>
|
|
</>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="secondary" onClick={() => closeModal()}>
|
|
Close
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
</div>
|
|
|
|
|
|
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default RouteView; |