Files
worldshine-redesign/client/src/components/trans-routes/RouteReportWithSignature.js
Lixian Zhou d715d2d7fc
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 28s
fix
2026-03-09 15:33:01 -04:00

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;