diff --git a/client/src/App.css b/client/src/App.css index 8f34e73..82e05b5 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -662,6 +662,19 @@ table .group td { height: 45px; } +.manage-table-columns-wrapper { + width: 100%; + margin-right: 0; +} + +.manage-table-columns-scroll { + max-height: 240px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 0; + margin-right: 0; +} + .app-main-content-fields-section.with-function .react-datepicker-wrapper input[type=text] { height: 45px; width: 120px; diff --git a/client/src/components/dashboard/DashboardCustomersList.js b/client/src/components/dashboard/DashboardCustomersList.js index bc90ea4..7940647 100644 --- a/client/src/components/dashboard/DashboardCustomersList.js +++ b/client/src/components/dashboard/DashboardCustomersList.js @@ -2,8 +2,8 @@ import React, {useState, useEffect} from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import { customerSlice } from "./../../store"; -import { AuthService, CustomerService, EventsService, LabelService } from "../../services"; -import { CUSTOMER_TYPE, ManageTable } from "../../shared"; +import { AuthService, CustomerService, EventsService } from "../../services"; +import { CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT, PROGRAM_TYPE, PROGRAM_TYPE_TEXT, PAY_SOURCE, PAY_SOURCE_TEXT, TRANSPORTATION_TYPE, TRANSPORTATION_TYPE_TEXT, YES_NO, YES_NO_TEXT, ManageTable } from "../../shared"; import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown, Modal } from "react-bootstrap"; import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons"; @@ -22,8 +22,11 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit const [showFilterDropdown, setShowFilterDropdown] = useState(false); const [showManageTableDropdown, setShowManageTableDropdown] = useState(false); const [showExportDropdown, setShowExportDropdown] = useState(false); - const [tagsFilter, setTagsFilter] = useState([]); - const [availableLabels, setAvailableLabels] = useState([]); + const [customerTypeFilter, setCustomerTypeFilter] = useState(''); + const [programTypeFilter, setProgramTypeFilter] = useState(''); + const [paySourceFilter, setPaySourceFilter] = useState(''); + const [transportationTypeFilter, setTransportationTypeFilter] = useState(''); + const [eyesOnFilter, setEyesOnFilter] = useState(''); const [showAvatarModal, setShowAvatarModal] = useState(false); const [avatarData, setAvatarData] = useState(null); const [avatarCustomerName, setAvatarCustomerName] = useState(''); @@ -110,19 +113,14 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit navigate(`/login`); } setShowSpinner(true); - Promise.all([ - CustomerService.getAllCustomers().then((data) => { - setCustomers(data.data.map((item) =>{ - item.phone = item?.phone || item?.home_phone || item?.mobile_phone; - item.address = item?.address1 || item?.address2 || item?.address3 || item?.address4|| item?.address5; + CustomerService.getAllCustomers().then((data) => { + setCustomers(data.data.map((item) =>{ + item.phone = item?.phone || item?.home_phone || item?.mobile_phone; + item.address = item?.address1 || item?.address2 || item?.address3 || item?.address4|| item?.address5; - return item; - }).sort((a, b) => a.lastname > b.lastname ? 1: -1)); - }), - LabelService.getAll().then((data) => { - setAvailableLabels(data.data); - }) - ]).finally(() => { + return item; + }).sort((a, b) => a.lastname > b.lastname ? 1: -1)); + }).finally(() => { setShowSpinner(false); }); }, []); @@ -166,16 +164,31 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit filtered = filtered.filter(item => item.status === 'active' && item.type !== CUSTOMER_TYPE.TRANSFERRED && item.type !== CUSTOMER_TYPE.DECEASED && item.type !== CUSTOMER_TYPE.DISCHARGED); } - // Tags filter - if (tagsFilter.length > 0) { - filtered = filtered.filter(item => { - if (!item?.tags || item.tags.length === 0) return false; - return tagsFilter.some(tag => item.tags.includes(tag)); + if (customerTypeFilter) { + filtered = filtered.filter((item) => (item?.type || '') === customerTypeFilter); + } + + if (programTypeFilter) { + filtered = filtered.filter((item) => (item?.program || '') === programTypeFilter); + } + + if (paySourceFilter) { + filtered = filtered.filter((item) => (item?.pay_source || '') === paySourceFilter); + } + + if (transportationTypeFilter) { + filtered = filtered.filter((item) => (item?.transportation_type || '') === transportationTypeFilter); + } + + if (eyesOnFilter) { + filtered = filtered.filter((item) => { + const normalized = `${item?.eyes_on || (item?.disability ? YES_NO.YES : '')}`.toLowerCase(); + return normalized === eyesOnFilter; }); } setFilteredCustomers(filtered); - }, [keyword, customers, showInactive, tagsFilter]) + }, [keyword, customers, showInactive, customerTypeFilter, programTypeFilter, paySourceFilter, transportationTypeFilter, eyesOnFilter]) useEffect(() => { const newCustomers = [...customers]; @@ -224,7 +237,11 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit } const cleanFilterAndClose = () => { - setTagsFilter([]); + setCustomerTypeFilter(''); + setProgramTypeFilter(''); + setPaySourceFilter(''); + setTransportationTypeFilter(''); + setEyesOnFilter(''); setShowFilterDropdown(false); } @@ -232,13 +249,12 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit setShowFilterDropdown(false); } - const toggleTagFilter = (tagName) => { - if (tagsFilter.includes(tagName)) { - setTagsFilter(tagsFilter.filter(tag => tag !== tagName)); - } else { - setTagsFilter([...tagsFilter, tagName]); - } - } + const getOptionsFromEnum = (enumMap, textMap) => { + return Object.values(enumMap).map((value) => ({ + value, + label: textMap?.[value] || value + })); + }; const handleColumnsChange = (newColumns) => { setColumns(newColumns); @@ -361,22 +377,53 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit
Filter By
-
Tags
-
- {availableLabels.map((label) => ( -
- toggleTagFilter(label.label_name)} - /> - -
+
Customer Type
+ +
+
+
Program Type
+ +
+
+
+
+
Pay Source
+ +
+
+
Tranportation Type
+ +
+
+
+
+
Eyes-On
+
diff --git a/client/src/components/trans-routes/RouteCustomerEditor.js b/client/src/components/trans-routes/RouteCustomerEditor.js index 59b31af..975ac3f 100644 --- a/client/src/components/trans-routes/RouteCustomerEditor.js +++ b/client/src/components/trans-routes/RouteCustomerEditor.js @@ -94,7 +94,7 @@ const Card = ({ content, index, moveCard }) => { ) } -const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun, onAddCustomer = null}) => { +const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, viewMode, editFun, onAddCustomer = null, scheduledAbsentCustomerIds = []}) => { const [customers, setCustomers] = useState([]); const [initializedRouteId, setInitializedRouteId] = useState(null); const [showAddPersonnelModal, setShowAddPersonnelModal] = useState(false); @@ -117,6 +117,16 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view const [pageCount, setPageCount] = useState(0); const itemsPerPage = 10; const hasActiveFilters = Boolean((customerFilter || '').trim()) || Boolean(lastNameFilter); + const scheduledAbsentIdsSet = new Set([ + ...(Array.isArray(scheduledAbsentCustomerIds) ? scheduledAbsentCustomerIds : []), + ...((currentRoute?.route_customer_list || []) + .filter((item) => item?.customer_route_status === PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) + .map((item) => item?.customer_id) + .filter(Boolean)) + ]); + const getAbsentNameStyle = (customerId) => ( + scheduledAbsentIdsSet.has(customerId) ? { color: '#dc3545', fontWeight: 700 } : {} + ); // Helper function to get all customer IDs already in the route const getAssignedCustomerIds = () => { @@ -699,7 +709,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
{item.customers.map(customer =>
- {customer.customer_name} + {customer.customer_name} {customer.customer_address} {customer.customer_pickup_status}
)} @@ -712,7 +722,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
{`Stop ${index+1}`}
- {item.customer_name} + {item.customer_name} {item.customer_address} {item.customer_pickup_status}
@@ -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
-
-
+
+
{tempColumns.map((column) => (