)}
@@ -712,7 +722,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
@@ -742,7 +752,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
{item.customers.map(customer =>
- {customer.customer_name}
+ {customer.customer_name}
{customer.customer_address}
{customer.customer_pickup_status}
)}
@@ -753,7 +763,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
return
{`Stop ${index+1}`}
- {item.customer_name}
+ {item.customer_name}
{item.customer_address}
{item.customer_pickup_status}
diff --git a/client/src/components/trans-routes/RouteEdit.js b/client/src/components/trans-routes/RouteEdit.js
index 4c7e9b0..e375f09 100644
--- a/client/src/components/trans-routes/RouteEdit.js
+++ b/client/src/components/trans-routes/RouteEdit.js
@@ -303,12 +303,28 @@ const RouteEdit = () => {
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
const absentCustomerIds = new Set();
+ const attendanceReasonsByCustomer = new Map();
+ const addAttendanceReason = (customerId, reasonText) => {
+ if (!customerId || !reasonText) return;
+ const cleaned = `${reasonText}`.trim();
+ if (!cleaned) return;
+ const existing = attendanceReasonsByCustomer.get(customerId) || [];
+ if (!existing.includes(cleaned)) {
+ attendanceReasonsByCustomer.set(customerId, [...existing, cleaned]);
+ }
+ };
+ const extractAttendanceReason = (item) => {
+ return item?.description || item?.data?.description || item?.data?.note || item?.title || '';
+ };
// 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));
+ singleNotes.forEach(note => {
+ absentCustomerIds.add(note.target_uuid);
+ addAttendanceReason(note.target_uuid, extractAttendanceReason(note));
+ });
// Recurring attendance rules that fall on this date
const recurRules = (recurRes?.data || []).filter(
@@ -317,6 +333,7 @@ const RouteEdit = () => {
recurRules.forEach(rule => {
if (recurRuleFallsOnDate(rule, routeDateObj)) {
absentCustomerIds.add(rule.target_uuid);
+ addAttendanceReason(rule.target_uuid, extractAttendanceReason(rule));
}
});
@@ -325,12 +342,14 @@ const RouteEdit = () => {
absentCustomerIds.forEach(customerId => {
const customer = allCustomers.find(c => c.id === customerId);
if (customer) {
+ const reasons = attendanceReasonsByCustomer.get(customer.id) || [];
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_note: reasons.join('; '),
_attendance_based: true // flag to identify these are from attendance notes
});
}
@@ -562,6 +581,16 @@ const RouteEdit = () => {
+ {(() => {
+ const existingScheduledAbsentIds = (currentRoute?.route_customer_list || [])
+ .filter((customer) => customer?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT)
+ .map((customer) => customer?.customer_id)
+ .filter(Boolean);
+ const attendanceBasedAbsentIds = (attendanceAbsentCustomers || [])
+ .map((customer) => customer?.customer_id)
+ .filter(Boolean);
+ const mergedAbsentIds = Array.from(new Set([].concat(existingScheduledAbsentIds, attendanceBasedAbsentIds)));
+ return (
{
} : undefined}
setNewCustomerList={setNewCustomerList}
onAddCustomer={(addFn) => setAddCustomerToRoute(() => addFn)}
+ scheduledAbsentCustomerIds={mergedAbsentIds}
/>
+ );
+ })()}
@@ -613,7 +645,7 @@ const RouteEdit = () => {
{abItem.customer_name}
{abItem.customer_address}
- (Attendance Note)
+ {!!abItem.attendance_note && ({abItem.attendance_note})}
));
diff --git a/client/src/components/vehicles/VehicleList.js b/client/src/components/vehicles/VehicleList.js
index 01217e8..e84d8ad 100644
--- a/client/src/components/vehicles/VehicleList.js
+++ b/client/src/components/vehicles/VehicleList.js
@@ -2,9 +2,10 @@ import React, {useState, useEffect} from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { AuthService, VehicleService } from "../../services";
-import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab } from "react-bootstrap";
+import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown } from "react-bootstrap";
import { Columns, Download, Filter, PersonSquare, Plus } from "react-bootstrap-icons";
import { ManageTable, Export } from "../../shared/components";
+import { FUEL_TYPE, FUEL_TYPE_TEXT, VEHICLE_TITLE, VEHICLE_TITLE_TEXT, SEATING_CAPACITY_OPTIONS, LIFT_EQUIPPED, LIFT_EQUIPPED_TEXT } from "../../shared";
const VehicleList = () => {
const navigate = useNavigate();
@@ -16,6 +17,11 @@ const VehicleList = () => {
const [selectedItems, setSelectedItems] = useState([]);
const [filteredVehicles, setFilteredVehicles] = useState(vehicles);
const [showInactive, setShowInactive] = useState(false);
+ const [showFilterDropdown, setShowFilterDropdown] = useState(false);
+ const [seatingCapacityFilter, setSeatingCapacityFilter] = useState('');
+ const [fuelTypeFilter, setFuelTypeFilter] = useState('');
+ const [titleFilter, setTitleFilter] = useState('');
+ const [liftEquippedFilter, setLiftEquippedFilter] = useState('');
const [columns, setColumns] = useState([
{
key: 'vehicle_number',
@@ -32,6 +38,11 @@ const VehicleList = () => {
label: 'Seating Capacity',
show: true
},
+ {
+ key: 'responsible_driver',
+ label: 'Responsible Driver',
+ show: true
+ },
{
key: 'mileage',
label: 'Mileage',
@@ -51,6 +62,36 @@ const VehicleList = () => {
key: 'year',
label: 'Year',
show: true
+ },
+ {
+ key: 'vin',
+ label: 'VIN Number',
+ show: true
+ },
+ {
+ key: 'gps_tag',
+ label: 'GPS ID',
+ show: true
+ },
+ {
+ key: 'ezpass',
+ label: 'E-ZPass',
+ show: true
+ },
+ {
+ key: 'has_lift_equip',
+ label: 'Lift Equipped',
+ show: true
+ },
+ {
+ key: 'fuel_type',
+ label: 'Fuel Type',
+ show: true
+ },
+ {
+ key: 'title',
+ label: 'Title',
+ show: true
}
]);
@@ -72,24 +113,44 @@ const VehicleList = () => {
item?.tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.responsible_driver?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
- item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
+ item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.vin?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.fuel_type?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.title?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
item?.status?.toLowerCase() !== 'active'
- ))
+ ).filter((item) => {
+ if (seatingCapacityFilter && `${item?.capacity || ''}` !== `${seatingCapacityFilter}`) return false;
+ if (fuelTypeFilter && `${item?.fuel_type || ''}` !== fuelTypeFilter) return false;
+ if (titleFilter && `${item?.title || ''}` !== titleFilter) return false;
+ if (liftEquippedFilter && `${item?.has_lift_equip}` !== liftEquippedFilter) return false;
+ return true;
+ }))
} else {
setFilteredVehicles(vehicles && vehicles.filter(item =>
(item?.vehicle_number?.toString()?.startsWith(keyword.toLowerCase()) ||
item?.tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.ezpass?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.gps_tag?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.responsible_driver?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.make?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
item?.vehicle_model?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
- item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
+ item?.year?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.vin?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.fuel_type?.toLowerCase()?.startsWith(keyword.toLowerCase()) ||
+ item?.title?.toLowerCase()?.startsWith(keyword.toLowerCase())) &&
item?.status?.toLowerCase() === 'active'
- ))
+ ).filter((item) => {
+ if (seatingCapacityFilter && `${item?.capacity || ''}` !== `${seatingCapacityFilter}`) return false;
+ if (fuelTypeFilter && `${item?.fuel_type || ''}` !== fuelTypeFilter) return false;
+ if (titleFilter && `${item?.title || ''}` !== titleFilter) return false;
+ if (liftEquippedFilter && `${item?.has_lift_equip}` !== liftEquippedFilter) return false;
+ return true;
+ }))
}
- }, [keyword, vehicles]);
+ }, [keyword, vehicles, showInactive, seatingCapacityFilter, fuelTypeFilter, titleFilter, liftEquippedFilter]);
useEffect(() => {
const newVehicles = [...vehicles];
@@ -154,6 +215,10 @@ const VehicleList = () => {
// Recover all filters
setKeyword('');
setTag('');
+ setSeatingCapacityFilter('');
+ setFuelTypeFilter('');
+ setTitleFilter('');
+ setLiftEquippedFilter('');
setSorting({key: '', order: ''});
setSelectedItems([]);
}
@@ -162,6 +227,77 @@ const VehicleList = () => {
return selectedItems.length === filteredVehicles.length && selectedItems.length > 0;
}
+ const clearAndCloseFilter = () => {
+ setSeatingCapacityFilter('');
+ setFuelTypeFilter('');
+ setTitleFilter('');
+ setLiftEquippedFilter('');
+ setShowFilterDropdown(false);
+ };
+
+ const applyAndCloseFilter = () => {
+ setShowFilterDropdown(false);
+ };
+
+ const customFilterMenu = React.forwardRef(
+ ({ children, style, className, 'aria-labelledby': labeledBy }, ref) => (
+
+
Filter By
+
+
+
Seating Capacity
+
+
+
+
Fuel Type
+
+
+
+
+
+
Title
+
+
+
+
Lift Equipped
+
+
+
+
+
+ )
+ );
+
const table =
@@ -186,10 +322,17 @@ const VehicleList = () => {
{columns.find(col => col.key === 'vehicle_number')?.show &&
{AuthService.canViewVechiles() ? : vehicle?.vehicle_number } | }
{columns.find(col => col.key === 'tag')?.show &&
{vehicle?.tag} | }
{columns.find(col => col.key === 'capacity')?.show &&
{vehicle?.capacity} | }
+ {columns.find(col => col.key === 'responsible_driver')?.show &&
{vehicle?.responsible_driver} | }
{columns.find(col => col.key === 'mileage')?.show &&
{vehicle?.mileage} | }
{columns.find(col => col.key === 'make')?.show &&
{vehicle?.make} | }
{columns.find(col => col.key === 'model')?.show &&
{vehicle?.vehicle_model} | }
{columns.find(col => col.key === 'year')?.show &&
{vehicle?.year} | }
+ {columns.find(col => col.key === 'vin')?.show &&
{vehicle?.vin} | }
+ {columns.find(col => col.key === 'gps_tag')?.show &&
{vehicle?.gps_tag} | }
+ {columns.find(col => col.key === 'ezpass')?.show &&
{vehicle?.ezpass} | }
+ {columns.find(col => col.key === 'has_lift_equip')?.show &&
{vehicle?.has_lift_equip ? 'Yes' : 'No'} | }
+ {columns.find(col => col.key === 'fuel_type')?.show &&
{FUEL_TYPE_TEXT[vehicle?.fuel_type] || vehicle?.fuel_type} | }
+ {columns.find(col => col.key === 'title')?.show &&
{VEHICLE_TITLE_TEXT[vehicle?.title] || vehicle?.title} | }
)
}
@@ -226,8 +369,20 @@ const VehicleList = () => {
setKeyword(e.currentTarget.value)} />
- {/*
*/}
+
setShowFilterDropdown(isOpen)}
+ autoClose={false}
+ >
+
+ Filter
+
+
+
{
const showManageTableDropdown = show !== undefined ? show : internalShow;
const handleToggle = onToggle || (() => setInternalShow(!internalShow));
+ React.useEffect(() => {
+ setTempColumns(columns);
+ }, [columns]);
+
const handleColumnToggle = (columnKey) => {
const updatedColumns = tempColumns.map(col =>
col.key === columnKey ? { ...col, show: !col.show } : col
@@ -46,8 +50,8 @@ const ManageTable = ({ columns, onColumnsChange, show, onToggle }) => {
>
Manage Table Columns