This commit is contained in:
2026-02-10 14:20:48 -05:00
parent 323add8d9a
commit 4295d1bc0f
9 changed files with 150 additions and 16 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.57cff37a.css",
"main.js": "/static/js/main.1a821513.js",
"main.js": "/static/js/main.68aeca0a.js",
"static/js/787.c4e7f8f9.chunk.js": "/static/js/787.c4e7f8f9.chunk.js",
"static/media/landing.png": "/static/media/landing.d4c6072db7a67dff6a78.png",
"index.html": "/index.html",
"main.57cff37a.css.map": "/static/css/main.57cff37a.css.map",
"main.1a821513.js.map": "/static/js/main.1a821513.js.map",
"main.68aeca0a.js.map": "/static/js/main.68aeca0a.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.57cff37a.css",
"static/js/main.1a821513.js"
"static/js/main.68aeca0a.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.1a821513.js"></script><link href="/static/css/main.57cff37a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.68aeca0a.js"></script><link href="/static/css/main.57cff37a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
client/.DS_Store vendored

Binary file not shown.

View File

@@ -4,7 +4,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { selectAllRoutes, transRoutesSlice, vehicleSlice, selectTomorrowAllRoutes, selectAllActiveDrivers, selectAllActiveVehicles, selectHistoryRoutes } from "./../../store";
import { Modal, Button, Breadcrumb, Tabs, Tab } from "react-bootstrap";
import RouteCustomerEditor from "./RouteCustomerEditor";
import { AuthService, TransRoutesService, CustomerService } from "../../services";
import { AuthService, TransRoutesService, CustomerService, EventsService } from "../../services";
import TimePicker from 'react-time-picker';
import 'react-time-picker/dist/TimePicker.css';
import moment from 'moment';
@@ -42,6 +42,7 @@ const RouteEdit = () => {
const [allCustomers, setAllCustomers] = useState([]);
const [unassignedCustomers, setUnassignedCustomers] = useState([]);
const [addCustomerToRoute, setAddCustomerToRoute] = useState(null);
const [attendanceAbsentCustomers, setAttendanceAbsentCustomers] = useState([]); // customers with attendance notes on route date
const paramsQuery = new URLSearchParams(window.location.search);
const scheduleDate = paramsQuery.get('dateSchedule');
const editSection = paramsQuery.get('editSection')
@@ -95,7 +96,18 @@ const RouteEdit = () => {
if (!validateRoute()) {
return;
}
let data = Object.assign({}, currentRoute, {name: routeName, driver: newDriver, vehicle: newVehicle, type: newRouteType, route_customer_list: newCustomerList});
// Merge attendance-based absences into the customer list for saving
const existingAbsentInRoute = (currentRoute?.route_customer_list || []).filter(
c => c?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
);
const existingAbsentIds = new Set(existingAbsentInRoute.map(a => a.customer_id));
// Also exclude attendance-absent customers from newCustomerList (they shouldn't be in the main list)
const attendanceAbsentIds = new Set(attendanceAbsentCustomers.map(a => a.customer_id));
const filteredCustomerList = (newCustomerList || []).filter(c => !attendanceAbsentIds.has(c.customer_id));
// Add attendance-based absences that aren't already in existing absences
const newAttendanceAbsences = attendanceAbsentCustomers.filter(a => !existingAbsentIds.has(a.customer_id));
const fullCustomerList = [...filteredCustomerList, ...existingAbsentInRoute, ...newAttendanceAbsences];
let data = Object.assign({}, currentRoute, {name: routeName, driver: newDriver, vehicle: newVehicle, type: newRouteType, route_customer_list: fullCustomerList});
if (estimatedStartTime && estimatedStartTime !== '') {
data = Object.assign({}, data, {estimated_start_time: combineDateAndTime(currentRoute.schedule_date, estimatedStartTime)})
}
@@ -187,6 +199,43 @@ const RouteEdit = () => {
});
}
// Helper: check if a recurring rule has an occurrence on a specific date
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);
// Normalize to date-only comparison
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++;
}
return false;
};
useEffect(() => {
if (!AuthService.canAddOrEditRoutes()) {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.')
@@ -229,9 +278,67 @@ const RouteEdit = () => {
{ route_customer_list: newCustomerList || [] }
];
const unassigned = calculateUnassignedCustomers(allCustomers, routesWithCurrentEdits);
// Also exclude attendance-absent customers from unassigned list
const attendanceAbsentIds = new Set(attendanceAbsentCustomers.map(c => c.customer_id));
const unassigned = calculateUnassignedCustomers(allCustomers, routesWithCurrentEdits)
.filter(customer => !attendanceAbsentIds.has(customer.id));
setUnassignedCustomers(unassigned);
}, [allCustomers, allRoutes, tomorrowRoutes, historyRoutes, currentRoute, newCustomerList]);
}, [allCustomers, allRoutes, tomorrowRoutes, historyRoutes, currentRoute, newCustomerList, attendanceAbsentCustomers]);
// Fetch attendance notes (single + recurring) for the route date
useEffect(() => {
if (!currentRoute?.schedule_date || !allCustomers?.length) return;
const routeDate = currentRoute.schedule_date; // MM/DD/YYYY
// Convert to YYYY-MM-DD for the API
const dateParts = routeDate.split('/');
if (dateParts.length !== 3) return;
const apiDate = `${dateParts[2]}-${dateParts[0].padStart(2, '0')}-${dateParts[1].padStart(2, '0')}`;
// Parse into a Date object for recurring rule checking
const routeDateObj = new Date(parseInt(dateParts[2]), parseInt(dateParts[0]) - 1, parseInt(dateParts[1]));
routeDateObj.setHours(0, 0, 0, 0);
// Fetch single attendance notes and recurring rules in parallel
Promise.all([
EventsService.getAllEvents({ date: apiDate, type: 'incident' }),
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
const absentCustomerIds = new Set();
// Single attendance notes for this date
const singleNotes = (eventsRes?.data || []).filter(
e => e.type === 'incident' && e.status === 'active' && e.target_uuid
);
singleNotes.forEach(note => absentCustomerIds.add(note.target_uuid));
// Recurring attendance rules that fall on this date
const recurRules = (recurRes?.data || []).filter(
r => r.type === 'incident' && r.status === 'active' && r.target_uuid && r.rrule
);
recurRules.forEach(rule => {
if (recurRuleFallsOnDate(rule, routeDateObj)) {
absentCustomerIds.add(rule.target_uuid);
}
});
// Build the list of absent customer objects
const absentList = [];
absentCustomerIds.forEach(customerId => {
const customer = allCustomers.find(c => c.id === customerId);
if (customer) {
absentList.push({
customer_id: customer.id,
customer_name: customer.name,
customer_address: customer.address1 || '',
customer_route_status: PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT,
customer_pickup_status: 'scheduleAbsent',
_attendance_based: true // flag to identify these are from attendance notes
});
}
});
setAttendanceAbsentCustomers(absentList);
});
}, [currentRoute?.schedule_date, allCustomers]);
// useEffect(() => {
// if (currentRoute) {
@@ -462,7 +569,8 @@ const RouteEdit = () => {
currentRoute={currentRoute ? {
...currentRoute,
route_customer_list: currentRoute.route_customer_list?.filter(
customer => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT
customer => customer?.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT &&
!attendanceAbsentCustomers.some(a => a.customer_id === customer.customer_id)
) || []
} : undefined}
setNewCustomerList={setNewCustomerList}
@@ -473,10 +581,16 @@ const RouteEdit = () => {
</div>
<div className="column-container">
<div className="column-card adjust">
<h6 className="text-primary">Scheduled Absences ({currentRoute?.route_customer_list?.filter(item => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.length || 0})</h6>
<h6 className="text-primary">Scheduled Absences ({(() => {
const existingAbsent = currentRoute?.route_customer_list?.filter(item => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) || [];
const existingIds = new Set(existingAbsent.map(a => a.customer_id));
const attendanceOnly = attendanceAbsentCustomers.filter(a => !existingIds.has(a.customer_id));
return existingAbsent.length + attendanceOnly.length;
})()})</h6>
<div className="customers-container mb-4">
{
currentRoute?.route_customer_list.filter(customer => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.map((abItem) => {
// Existing scheduled absences from route data
currentRoute?.route_customer_list?.filter(customer => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)?.map((abItem) => {
return <div key={abItem.customer_id} className="customers-dnd-item-container-absent">
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
@@ -484,10 +598,30 @@ const RouteEdit = () => {
<small className="me-2">{abItem.customer_address}</small>
<small className="me-2">{abItem.customer_pickup_status}</small>
</div>
</div>
})
}
{
// Attendance-based absences (not already in existing scheduled absences)
(() => {
const existingIds = new Set(
(currentRoute?.route_customer_list?.filter(c => c?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) || [])
.map(a => a.customer_id)
);
return attendanceAbsentCustomers
.filter(a => !existingIds.has(a.customer_id))
.map((abItem) => (
<div key={`att-${abItem.customer_id}`} className="customers-dnd-item-container-absent" style={{ opacity: 0.85 }}>
<GripVertical className="me-4" size={14}></GripVertical>
<div className="customer-dnd-item">
<span>{abItem.customer_name} </span>
<small className="me-2">{abItem.customer_address}</small>
<small className="me-2 text-muted">(Attendance Note)</small>
</div>
</div>
));
})()
}
</div>
</div>
</div>