This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
<h6>Filter By</h6>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div className="field-label">Tags</div>
|
||||
<div style={{ maxHeight: '150px', overflowY: 'auto' }}>
|
||||
{availableLabels.map((label) => (
|
||||
<div key={label.id} style={{ marginBottom: '5px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`tag-${label.id}`}
|
||||
checked={tagsFilter.includes(label.label_name)}
|
||||
onChange={() => toggleTagFilter(label.label_name)}
|
||||
/>
|
||||
<label htmlFor={`tag-${label.id}`} style={{ marginLeft: '5px' }}>
|
||||
{label.label_name}
|
||||
</label>
|
||||
</div>
|
||||
<div className="field-label">Customer Type</div>
|
||||
<select value={customerTypeFilter} onChange={(e) => setCustomerTypeFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{getOptionsFromEnum(CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT).map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</div>
|
||||
</select>
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Program Type</div>
|
||||
<select value={programTypeFilter} onChange={(e) => setProgramTypeFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{getOptionsFromEnum(PROGRAM_TYPE, PROGRAM_TYPE_TEXT).map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div className="field-label">Pay Source</div>
|
||||
<select value={paySourceFilter} onChange={(e) => setPaySourceFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{getOptionsFromEnum(PAY_SOURCE, PAY_SOURCE_TEXT).map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Tranportation Type</div>
|
||||
<select value={transportationTypeFilter} onChange={(e) => setTransportationTypeFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{getOptionsFromEnum(TRANSPORTATION_TYPE, TRANSPORTATION_TYPE_TEXT).map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div className="field-label">Eyes-On</div>
|
||||
<select value={eyesOnFilter} onChange={(e) => setEyesOnFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{getOptionsFromEnum(YES_NO, YES_NO_TEXT).map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="list row">
|
||||
|
||||
@@ -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
|
||||
<div className="customer-dnd-item-content">{item.customers.map(customer =>
|
||||
<div key={customer.customer_id}>
|
||||
|
||||
<small className="me-2">{customer.customer_name}</small>
|
||||
<small className="me-2" style={getAbsentNameStyle(customer.customer_id)}>{customer.customer_name}</small>
|
||||
<small className="me-2">{customer.customer_address}</small>
|
||||
<small className="me-2">{customer.customer_pickup_status}</small>
|
||||
</div>)}
|
||||
@@ -712,7 +722,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
|
||||
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
||||
<GripVertical className="me-4" size={20}></GripVertical>
|
||||
<div className="customer-dnd-item">
|
||||
<span>{item.customer_name} </span>
|
||||
<span style={getAbsentNameStyle(item.customer_id)}>{item.customer_name} </span>
|
||||
<small className="me-2">{item.customer_address}</small>
|
||||
<small className="me-2">{item.customer_pickup_status}</small>
|
||||
</div>
|
||||
@@ -742,7 +752,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
|
||||
<div className="customer-dnd-item-content">{item.customers.map(customer =>
|
||||
<div key={customer.customer_id}>
|
||||
|
||||
<small className="me-2">{customer.customer_name}</small>
|
||||
<small className="me-2" style={getAbsentNameStyle(customer.customer_id)}>{customer.customer_name}</small>
|
||||
<small className="me-2">{customer.customer_address}</small>
|
||||
<small className="me-2">{customer.customer_pickup_status}</small>
|
||||
</div>)}
|
||||
@@ -753,7 +763,7 @@ const RouteCustomerEditor = ({currentRoute, setNewCustomerList = (a) => {}, view
|
||||
return <div className="customers-dnd-item-container">
|
||||
<div className="stop-index"><span>{`Stop ${index+1}`}</span><RecordCircleFill size={16} color={"#0066B1"} className="ms-2"></RecordCircleFill> </div>
|
||||
<div className="customer-dnd-item">
|
||||
<span>{item.customer_name} </span>
|
||||
<span style={getAbsentNameStyle(item.customer_id)}>{item.customer_name} </span>
|
||||
<small className="me-2">{item.customer_address}</small>
|
||||
<small className="me-2">{item.customer_pickup_status}</small>
|
||||
</div>
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className="column-container">
|
||||
<div className="column-card adjust" style={{paddingRight: '30px'}}>
|
||||
<div className="col-md-12 mb-4">
|
||||
{(() => {
|
||||
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 (
|
||||
<RouteCustomerEditor
|
||||
currentRoute={currentRoute ? {
|
||||
...currentRoute,
|
||||
@@ -572,7 +601,10 @@ const RouteEdit = () => {
|
||||
} : undefined}
|
||||
setNewCustomerList={setNewCustomerList}
|
||||
onAddCustomer={(addFn) => setAddCustomerToRoute(() => addFn)}
|
||||
scheduledAbsentCustomerIds={mergedAbsentIds}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,7 +645,7 @@ const RouteEdit = () => {
|
||||
<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>
|
||||
{!!abItem.attendance_note && <small className="me-2 text-muted">({abItem.attendance_note})</small>}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
@@ -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) => (
|
||||
<div
|
||||
ref={ref}
|
||||
style={style}
|
||||
className={className}
|
||||
aria-labelledby={labeledBy}
|
||||
>
|
||||
<h6>Filter By</h6>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div className="field-label">Seating Capacity</div>
|
||||
<select value={seatingCapacityFilter} onChange={(e) => setSeatingCapacityFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{SEATING_CAPACITY_OPTIONS.map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Fuel Type</div>
|
||||
<select value={fuelTypeFilter} onChange={(e) => setFuelTypeFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{Object.values(FUEL_TYPE).map((value) => (
|
||||
<option key={value} value={value}>{FUEL_TYPE_TEXT[value] || value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div className="field-label">Title</div>
|
||||
<select value={titleFilter} onChange={(e) => setTitleFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{Object.values(VEHICLE_TITLE).map((value) => (
|
||||
<option key={value} value={value}>{VEHICLE_TITLE_TEXT[value] || value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="me-4">
|
||||
<div className="field-label">Lift Equipped</div>
|
||||
<select value={liftEquippedFilter} onChange={(e) => setLiftEquippedFilter(e.currentTarget.value)}>
|
||||
<option value="">All</option>
|
||||
{Object.values(LIFT_EQUIPPED).map((value) => (
|
||||
<option key={value} value={value}>{LIFT_EQUIPPED_TEXT[value] || value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="list row">
|
||||
<div className="col-md-12">
|
||||
<button className="btn btn-default btn-sm float-right" onClick={clearAndCloseFilter}> Cancel </button>
|
||||
<button className="btn btn-primary btn-sm float-right" onClick={applyAndCloseFilter}> Filter </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
const table = <div className="list row mb-4">
|
||||
<div className="col-md-12">
|
||||
@@ -186,10 +322,17 @@ const VehicleList = () => {
|
||||
{columns.find(col => col.key === 'vehicle_number')?.show && <td>{AuthService.canViewVechiles() ? <button className="btn btn-link btn-sm" onClick={() => goToView(vehicle?.id)}>{vehicle?.vehicle_number}</button> : vehicle?.vehicle_number }</td>}
|
||||
{columns.find(col => col.key === 'tag')?.show && <td>{vehicle?.tag}</td>}
|
||||
{columns.find(col => col.key === 'capacity')?.show && <td>{vehicle?.capacity}</td>}
|
||||
{columns.find(col => col.key === 'responsible_driver')?.show && <td>{vehicle?.responsible_driver}</td>}
|
||||
{columns.find(col => col.key === 'mileage')?.show && <td>{vehicle?.mileage}</td>}
|
||||
{columns.find(col => col.key === 'make')?.show && <td>{vehicle?.make}</td>}
|
||||
{columns.find(col => col.key === 'model')?.show && <td>{vehicle?.vehicle_model}</td>}
|
||||
{columns.find(col => col.key === 'year')?.show && <td>{vehicle?.year}</td>}
|
||||
{columns.find(col => col.key === 'vin')?.show && <td>{vehicle?.vin}</td>}
|
||||
{columns.find(col => col.key === 'gps_tag')?.show && <td>{vehicle?.gps_tag}</td>}
|
||||
{columns.find(col => col.key === 'ezpass')?.show && <td>{vehicle?.ezpass}</td>}
|
||||
{columns.find(col => col.key === 'has_lift_equip')?.show && <td>{vehicle?.has_lift_equip ? 'Yes' : 'No'}</td>}
|
||||
{columns.find(col => col.key === 'fuel_type')?.show && <td>{FUEL_TYPE_TEXT[vehicle?.fuel_type] || vehicle?.fuel_type}</td>}
|
||||
{columns.find(col => col.key === 'title')?.show && <td>{VEHICLE_TITLE_TEXT[vehicle?.title] || vehicle?.title}</td>}
|
||||
</tr>)
|
||||
}
|
||||
</tbody>
|
||||
@@ -226,8 +369,20 @@ const VehicleList = () => {
|
||||
</Tabs>
|
||||
<div className="list-func-panel">
|
||||
<input className="me-2 with-search-icon" type="text" placeholder="Search" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} />
|
||||
{/* <button className="btn btn-primary me-2"><Filter size={16} className="me-2"></Filter>Filter</button> */}
|
||||
<ManageTable columns={columns} onColumnsChange={setColumns} />
|
||||
<Dropdown
|
||||
key={'filter-vehicles'}
|
||||
id="filter-vehicles"
|
||||
className="me-2"
|
||||
show={showFilterDropdown}
|
||||
onToggle={(isOpen) => setShowFilterDropdown(isOpen)}
|
||||
autoClose={false}
|
||||
>
|
||||
<Dropdown.Toggle variant="primary">
|
||||
<Filter size={16} className="me-2"></Filter>Filter
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu as={customFilterMenu}/>
|
||||
</Dropdown>
|
||||
<button className="btn btn-primary me-2" onClick={() => goToCreateNew()}><Plus size={16}></Plus>Add New Vehicle</button>
|
||||
<Export
|
||||
columns={columns}
|
||||
|
||||
@@ -10,6 +10,10 @@ const ManageTable = ({ columns, onColumnsChange, show, onToggle }) => {
|
||||
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 }) => {
|
||||
>
|
||||
<h6>Manage Table Columns</h6>
|
||||
<div className="app-main-content-fields-section margin-sm dropdown-container">
|
||||
<div className="me-4">
|
||||
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
<div className="manage-table-columns-wrapper">
|
||||
<div className="manage-table-columns-scroll">
|
||||
{tempColumns.map((column) => (
|
||||
<div key={column.key} style={{ marginBottom: '8px' }}>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user