All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 28s
384 lines
21 KiB
JavaScript
384 lines
21 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 { CustomerService, SignatureRequestService, EventsService } from "../../services";
|
|
import moment from 'moment';
|
|
import { CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS } from "../../shared";
|
|
|
|
const RouteReportWithSignature = () => {
|
|
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 routeIndex = currentRoute?.type === 'inbound' ?
|
|
((allRoutes.map((r) => Object.assign({}, r, {vehicleNumber: vehicles?.find((v) => r.vehicle === v.id)?.vehicle_number || 0})).sort((a, b) => a?.vehicleNumber - b?.vehicleNumber).filter(item => item.type === 'inbound').findIndex(item => item.id === params.id))) + 1 || ((tomorrowRoutes.filter(item => item.type === 'inbound').findIndex(item => item.id === params.id)))+1 || ((historyRoutes.filter(item => item.type === 'inbound').findIndex(item => item.id === params.id)))+1 :
|
|
((allRoutes.map((r) => Object.assign({}, r, {vehicleNumber: vehicles?.find((v) => r.vehicle === v.id)?.vehicle_number || 0})).sort((a, b) => a?.vehicleNumber - b?.vehicleNumber).filter(item => item.type === 'outbound').findIndex(item => item.id === params.id)))+1 || ((tomorrowRoutes.filter(item => item.type === 'outbound').findIndex(item => item.id === params.id))) +1 || ((historyRoutes.filter(item => item.type === 'outbound').findIndex(item => item.id === params.id)))+1;
|
|
const currentVehicle = vehicles.find(item => item.id === currentRoute?.vehicle );
|
|
const currentDriver = drivers.find(item => item.id === currentRoute?.driver);
|
|
|
|
const [signature, setSignature] = useState(undefined);
|
|
|
|
const [directorSignature, setDirectorSignature] = useState(undefined);
|
|
|
|
const site = EventsService.site;
|
|
|
|
// Check if route name is 'by own' (case-insensitive)
|
|
const isByOwnRoute = currentRoute?.name?.toLowerCase() === 'by own';
|
|
const driverLabel = isByOwnRoute ? 'Staff' : 'Driver';
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const getRelatedInboundOutboundRoutesForThisView = (routeType) => {
|
|
if (allRoutes.find(item => item.id === params.id)) {
|
|
return allRoutes.filter(item => item.type!== routeType);
|
|
}
|
|
if (tomorrowRoutes.find(item => item.id === params.id)) {
|
|
return tomorrowRoutes.filter(item => item.type!==routeType);
|
|
}
|
|
if (historyRoutes.find(item => item.id === params.id)) {
|
|
return historyRoutes.filter(item => item.type!==routeType);
|
|
}
|
|
}
|
|
|
|
const getInboundOrOutboundRouteCustomer = (customer_id) => {
|
|
return getRelatedInboundOutboundRoutesForThisView(currentRoute?.type)?.
|
|
find((route) => route?.name?.toLowerCase()?.replaceAll(' ', '') === currentRoute?.name?.toLowerCase()?.replaceAll(' ', '') && route?.schedule_date == currentRoute?.schedule_date)?.route_customer_list?.
|
|
find((cust) => cust?.customer_id === customer_id)
|
|
}
|
|
|
|
const getOtherRouteWithThisCustomer = (customer_id) => {
|
|
return getRelatedInboundOutboundRoutesForThisView(currentRoute?.type)?.find((route) => route?.name !== currentRoute?.name && route?.schedule_date == currentRoute?.schedule_date && route?.route_customer_list?.find(cust => cust?.customer_id === customer_id));
|
|
}
|
|
|
|
const getOtherOutboundRouteWithThisCustomer = (customer_id) => {
|
|
// console.log('what', getRelatedInboundOutboundRoutesForThisView('outbound'));
|
|
return getRelatedInboundOutboundRoutesForThisView('inbound')?.
|
|
find((route) => route?.schedule_date === currentRoute?.schedule_date && route?.id !== currentRoute?.id && route?.route_customer_list.find((cust) => cust?.customer_id === customer_id));
|
|
}
|
|
|
|
const directToView = () => {
|
|
navigate(`/trans-routes/${params.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);
|
|
});
|
|
CustomerService.getAvatar(`center_director_signature_site_${site}`).then(data => {
|
|
if (data?.data) {
|
|
setDirectorSignature(data?.data)
|
|
}
|
|
});
|
|
}, [currentRoute]);
|
|
const normalizeChecklistText = (value = '') =>
|
|
String(value)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, '');
|
|
|
|
const normalizedCurrentRouteName = currentRoute?.name?.toLowerCase()?.replaceAll(' ', '');
|
|
const checklistSourceRoutes = [
|
|
currentRoute,
|
|
...(getRelatedInboundOutboundRoutesForThisView(currentRoute?.type) || []).filter((route) =>
|
|
route?.schedule_date === currentRoute?.schedule_date &&
|
|
route?.name?.toLowerCase()?.replaceAll(' ', '') === normalizedCurrentRouteName &&
|
|
['inbound', 'outbound'].includes(route?.type)
|
|
)
|
|
].filter(Boolean);
|
|
|
|
const checklistItemsMap = new Map();
|
|
checklistSourceRoutes.forEach((route) => {
|
|
(route?.checklist_result || []).forEach((item) => {
|
|
const displayLabel = item?.item || item?.label || item?.key || '';
|
|
const normalizedKey = normalizeChecklistText(displayLabel);
|
|
if (!normalizedKey) return;
|
|
const current = checklistItemsMap.get(normalizedKey) || {
|
|
label: displayLabel,
|
|
inspected: false
|
|
};
|
|
checklistItemsMap.set(normalizedKey, {
|
|
label: current.label || displayLabel,
|
|
inspected: current.inspected || item?.result === true || item?.checked === true
|
|
});
|
|
});
|
|
});
|
|
const checklistItems = Array.from(checklistItemsMap.values());
|
|
const checklistRows = [];
|
|
for (let i = 0; i < checklistItems.length; i += 3) {
|
|
checklistRows.push(checklistItems.slice(i, i + 3));
|
|
}
|
|
|
|
const sortedRouteCustomers = [...(currentRoute?.route_customer_list || [])].sort((a, b) => {
|
|
const aOrder = Number(a?.customer_pickup_order);
|
|
const bOrder = Number(b?.customer_pickup_order);
|
|
const safeA = Number.isNaN(aOrder) ? Number.MAX_SAFE_INTEGER : aOrder;
|
|
const safeB = Number.isNaN(bOrder) ? Number.MAX_SAFE_INTEGER : bOrder;
|
|
return safeA - safeB;
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<style>
|
|
{`
|
|
.route-report-table th,
|
|
.route-report-table td {
|
|
min-width: auto !important;
|
|
width: auto !important;
|
|
max-width: none !important;
|
|
border: 1px solid #333;
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
}
|
|
.route-report-table {
|
|
table-layout: auto !important;
|
|
border-collapse: collapse;
|
|
}
|
|
.route-report-table thead th {
|
|
background-color: #f5f5f5 !important;
|
|
text-align: center;
|
|
color: #000 !important;
|
|
}
|
|
.route-report-header {
|
|
border: 1px solid #333;
|
|
padding: 8px 16px;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
}
|
|
.route-report-signature-row {
|
|
border: 1px solid #333;
|
|
padding: 8px 16px;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
}
|
|
.inspection-table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin-top: 16px;
|
|
}
|
|
.inspection-table th,
|
|
.inspection-table td {
|
|
border: 1px solid #333;
|
|
padding: 4px 8px;
|
|
font-size: 11px;
|
|
}
|
|
.inspection-table th {
|
|
background-color: #f5f5f5;
|
|
color: #000;
|
|
}
|
|
.bilingual-label {
|
|
display: block;
|
|
font-size: 10px;
|
|
color: #000 !important;
|
|
}
|
|
`}
|
|
</style>
|
|
<div className="list row noprint">
|
|
<div className="col-md-12 text-primary mb-2">
|
|
<button className="btn btn-link btn-sm" onClick={() => directToView()}>Back</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Header Row with Route Info */}
|
|
<div className="route-report-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>Route (路线):</strong> {currentRoute?.name}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>{driverLabel} ({isByOwnRoute ? '工作人员' : '司机'}):</strong> {currentDriver?.name}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>Vehicle (车号):</strong> {currentVehicle?.vehicle_number}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>Date (日期):</strong> {currentRoute?.schedule_date && moment(currentRoute?.schedule_date).format('MM/DD/YYYY')}
|
|
</div>
|
|
<div style={{ background: '#eee', padding: '8px 12px', fontWeight: 'bold' }}>
|
|
{routeIndex}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Signature Row */}
|
|
<div className="route-report-signature-row" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>{driverLabel}'s Signature ({isByOwnRoute ? '工作人员签字' : '司机签字'}):</strong>
|
|
{signature && <img width="100px" src={`data:image/jpg;base64, ${signature}`} style={{ marginLeft: '16px' }} alt="Driver Signature" />}
|
|
{currentRoute?.end_time && <span style={{ marginLeft: '16px' }}>{new Date(currentRoute?.end_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})}</span>}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<strong>Manager's Signature (经理签字):</strong>
|
|
<img width="100px" src="/images/signature.jpeg" style={{ marginLeft: '16px' }} alt="Manager Signature" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Customer Table */}
|
|
<div className="list row">
|
|
<div className="col-md-12 mb-4">
|
|
<table className="personnel-info-table route-report-table" style={{ tableLayout: 'auto', width: '100%' }}>
|
|
<thead>
|
|
{/* English Header Row */}
|
|
<tr>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>No.<span className="bilingual-label">序号</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Name<span className="bilingual-label">姓名</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Phone<span className="bilingual-label">联系电话</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Address<span className="bilingual-label">地址</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Unit<span className="bilingual-label">房间号</span></th>
|
|
<th colSpan={2} style={{ textAlign: 'center' }}>Attendance<span className="bilingual-label">出勤</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Pick-Up<span className="bilingual-label">接到时间</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Arrival<span className="bilingual-label">抵达中心</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Departure<span className="bilingual-label">离开中心</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Drop-Off<span className="bilingual-label">送达时间</span></th>
|
|
<th rowSpan={2} style={{ verticalAlign: 'middle' }}>Notice<span className="bilingual-label">备注</span></th>
|
|
</tr>
|
|
<tr>
|
|
<th style={{ textAlign: 'center' }}>Y</th>
|
|
<th style={{ textAlign: 'center' }}>N</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{
|
|
sortedRouteCustomers?.map((customer, index) => {
|
|
const relativeRouteCustomer = getInboundOrOutboundRouteCustomer(customer?.customer_id);
|
|
const otherRouteWithThisCustomer = getOtherRouteWithThisCustomer(customer?.customer_id);
|
|
const customerInOtherRoute = otherRouteWithThisCustomer?.route_customer_list?.find(cust => cust?.customer_id === customer?.customer_id);
|
|
const otherOutboundRoute = getOtherOutboundRouteWithThisCustomer(customer?.customer_id);
|
|
|
|
const isAttending = ![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(relativeRouteCustomer?.customer_route_status) &&
|
|
![PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT, PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT].includes(customer?.customer_route_status);
|
|
|
|
return (
|
|
<tr key={index}>
|
|
<td style={{ textAlign: 'center' }}>{index + 1}</td>
|
|
<td>{customer?.customer_name}</td>
|
|
<td>{customer?.customer_phone}</td>
|
|
<td>{customer?.customer_address_override || customer?.customer_address}</td>
|
|
<td></td>
|
|
<td style={{ textAlign: 'center' }}>{isAttending ? '✓' : ''}</td>
|
|
<td style={{ textAlign: 'center' }}>{!isAttending ? '✗' : ''}</td>
|
|
<td style={{ textAlign: 'center' }}>
|
|
{customer?.customer_pickup_time
|
|
? new Date(customer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (relativeRouteCustomer?.customer_pickup_time
|
|
? new Date(relativeRouteCustomer?.customer_pickup_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (customerInOtherRoute?.customer_pickup_time
|
|
? new Date(customerInOtherRoute?.customer_pickup_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: ''))}
|
|
</td>
|
|
<td style={{ textAlign: 'center' }}>
|
|
{customer?.customer_enter_center_time
|
|
? new Date(customer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (relativeRouteCustomer?.customer_enter_center_time
|
|
? new Date(relativeRouteCustomer?.customer_enter_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (customerInOtherRoute?.customer_enter_center_time
|
|
? new Date(customerInOtherRoute?.customer_enter_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: ''))}
|
|
</td>
|
|
<td style={{ textAlign: 'center' }}>
|
|
{customer?.customer_leave_center_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
|
|
? new Date(customer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (relativeRouteCustomer?.customer_leave_center_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
|
|
? new Date(relativeRouteCustomer?.customer_leave_center_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (customerInOtherRoute?.customer_leave_center_time
|
|
? new Date(customerInOtherRoute?.customer_leave_center_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: ''))}
|
|
</td>
|
|
<td style={{ textAlign: 'center' }}>
|
|
{customer?.customer_dropoff_time && customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
|
|
? new Date(customer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (relativeRouteCustomer?.customer_dropoff_time && relativeRouteCustomer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SKIP_DROPOFF
|
|
? new Date(relativeRouteCustomer?.customer_dropoff_time).toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: (customerInOtherRoute?.customer_dropoff_time
|
|
? new Date(customerInOtherRoute?.customer_dropoff_time)?.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'})
|
|
: ''))}
|
|
</td>
|
|
<td>
|
|
{customer?.customer_type !== CUSTOMER_TYPE.MEMBER && <div>{CUSTOMER_TYPE_TEXT[customer?.customer_type]}</div>}
|
|
{!relativeRouteCustomer && otherRouteWithThisCustomer && (
|
|
<div>{`${currentRoute?.type === 'inbound' ? 'Switch to Route ' : 'Switch from Route '} ${otherRouteWithThisCustomer?.name}`}</div>
|
|
)}
|
|
{customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SKIP_DROPOFF && otherOutboundRoute && (
|
|
<div>{`Switch to Route ${otherOutboundRoute?.name}`}</div>
|
|
)}
|
|
{/* Rest stop check for inbound */}
|
|
{currentRoute?.type === 'inbound' && (() => {
|
|
const pickupTime = customer?.customer_pickup_time || relativeRouteCustomer?.customer_pickup_time || customerInOtherRoute?.customer_pickup_time;
|
|
const enterTime = customer?.customer_enter_center_time || relativeRouteCustomer?.customer_enter_center_time || customerInOtherRoute?.customer_enter_center_time;
|
|
if (pickupTime && enterTime) {
|
|
const diffMs = new Date(enterTime) - new Date(pickupTime);
|
|
const diffHours = diffMs / (1000 * 60 * 60);
|
|
if (diffHours > 1) return <div>Rest Stop</div>;
|
|
}
|
|
return null;
|
|
})()}
|
|
{/* Rest stop check for outbound */}
|
|
{currentRoute?.type === 'outbound' && (() => {
|
|
const leaveTime = customer?.customer_leave_center_time || relativeRouteCustomer?.customer_leave_center_time || customerInOtherRoute?.customer_leave_center_time;
|
|
const dropoffTime = customer?.customer_dropoff_time || relativeRouteCustomer?.customer_dropoff_time || customerInOtherRoute?.customer_dropoff_time;
|
|
if (leaveTime && dropoffTime) {
|
|
const diffMs = new Date(dropoffTime) - new Date(leaveTime);
|
|
const diffHours = diffMs / (1000 * 60 * 60);
|
|
if (diffHours > 1) return <div>Rest Stop</div>;
|
|
}
|
|
return null;
|
|
})()}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vehicle Inspection Checklist */}
|
|
{checklistItems.length > 0 && (
|
|
<div className="list row">
|
|
<div className="col-md-12">
|
|
<table className="inspection-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Item</th>
|
|
<th>Inspected</th>
|
|
<th>Item</th>
|
|
<th>Inspected</th>
|
|
<th>Item</th>
|
|
<th>Inspected</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{checklistRows.map((row, rowIndex) => (
|
|
<tr key={`checklist-row-${rowIndex}`}>
|
|
{[0, 1, 2].map((cellIndex) => {
|
|
const item = row[cellIndex];
|
|
if (!item) {
|
|
return (
|
|
<React.Fragment key={`checklist-empty-${rowIndex}-${cellIndex}`}>
|
|
<td></td>
|
|
<td></td>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
return (
|
|
<React.Fragment key={`checklist-item-${rowIndex}-${cellIndex}`}>
|
|
<td>{item.label}</td>
|
|
<td style={{ textAlign: 'center' }}>{item.inspected ? '✓' : ''}</td>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default RouteReportWithSignature; |