This commit is contained in:
2026-02-05 17:53:31 -05:00
parent 99d6265152
commit ed56d29088
14 changed files with 215 additions and 799 deletions

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.8bf7011f.css",
"main.js": "/static/js/main.5ccb77ca.js",
"main.css": "/static/css/main.01f852fa.css",
"main.js": "/static/js/main.d902953d.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.8bf7011f.css.map": "/static/css/main.8bf7011f.css.map",
"main.5ccb77ca.js.map": "/static/js/main.5ccb77ca.js.map",
"main.01f852fa.css.map": "/static/css/main.01f852fa.css.map",
"main.d902953d.js.map": "/static/js/main.d902953d.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.8bf7011f.css",
"static/js/main.5ccb77ca.js"
"static/css/main.01f852fa.css",
"static/js/main.d902953d.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.5ccb77ca.js"></script><link href="/static/css/main.8bf7011f.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.d902953d.js"></script><link href="/static/css/main.01f852fa.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -206,12 +206,12 @@ input {
padding: 4px 24px;
}
/* List pages: container yields to children width/height, no scrolling (browser-level scrolling) */
/* List pages: container constrained to page width, table scrolls inside */
.app-main-content-list-container.list-page {
max-width: none;
max-height: none;
width: fit-content;
min-width: 100%;
max-width: 100%;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.app-main-content-list-func-container {
@@ -243,11 +243,18 @@ input {
color: #0066B1 !important;
}
/* List pages: tab-pane should not scroll, let browser handle it */
/* List pages: tab-pane contains scrollable table */
.app-main-content-list-container.list-page .tab-pane {
border-top: 1px solid #ccc;
padding-top: 24px;
overflow: visible;
overflow-x: auto;
max-width: 100%;
}
/* List pages: table wrapper should scroll horizontally */
.app-main-content-list-container.list-page .col-md-12 {
overflow-x: auto;
max-width: 100%;
}
/* Form pages: tab-pane should not scroll either */
@@ -1803,6 +1810,12 @@ input[type="checkbox"] {
min-width: 0;
height: 100%;
gap: clamp(8px, 1vw, 16px);
min-height: 0;
}
/* Ensure middle column gallery fills remaining space */
.fullscreen-mode .column-container:nth-child(2) {
justify-content: flex-start;
}
.fullscreen-mode .column-card {
@@ -2056,12 +2069,55 @@ input[type="checkbox"] {
min-height: 0;
}
.fullscreen-mode .attendance-note-wrapper,
.fullscreen-mode .gallery-wrapper {
.fullscreen-mode .attendance-note-wrapper {
flex: 0 0 auto;
margin-bottom: clamp(8px, 1vw, 16px);
}
.fullscreen-mode .gallery-wrapper {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 0;
min-height: 0;
padding-bottom: clamp(8px, 1vw, 16px);
}
.fullscreen-mode .gallery-wrapper .card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.fullscreen-mode .gallery-wrapper .card-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.fullscreen-mode .gallery-wrapper .fullscreen-carousel {
flex: 1;
min-height: 0;
}
.fullscreen-mode .gallery-wrapper .fullscreen-carousel,
.fullscreen-mode .gallery-wrapper .carousel-inner,
.fullscreen-mode .gallery-wrapper .carousel-item {
height: 100% !important;
}
.fullscreen-mode .gallery-wrapper .fullscreen-carousel-img {
height: 100% !important;
object-fit: cover;
}
.fullscreen-mode .gallery-wrapper .fullscreen-placeholder {
flex: 1;
height: auto !important;
}
.fullscreen-mode .fullscreen-date-display {
margin-bottom: clamp(4px, 0.5vw, 12px);
}

View File

@@ -1,128 +1,32 @@
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, Export } from "../../shared";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown, OverlayTrigger, Popover } from "react-bootstrap";
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
import { useNavigate } from "react-router-dom";
import { AuthService, CustomerService } from "../../services";
import { Export } from "../../shared";
import { Plus } from "react-bootstrap-icons";
import DashboardCustomersList from "../dashboard/DashboardCustomersList";
const CustomersList = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [customers, setCustomers] = useState([]);
const [keyword, setKeyword] = useState('');
const [showInactive, setShowInactive] = useState(false);
const [transferMap, setTransferMap] = useState({});
// const [events, setEvents] = useState([]);
const [showSpinner, setShowSpinner] = useState(false);
const [sorting, setSorting] = useState({key: '', order: ''});
const [selectedItems, setSelectedItems] = useState([]);
const [filteredCustomers, setFilteredCustomers] = useState(customers);
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
const [healthConditionFilter, setHealthConditionFilter] = useState('');
const [paymentStatusFilter, setPaymentStatusFilter] = useState('');
const [serviceRequirementFilter, setServiceRequirementFilter] = useState('');
const [tagsFilter, setTagsFilter] = useState([]);
const [availableLabels, setAvailableLabels] = useState([]);
const [customerAvatars, setCustomerAvatars] = useState({});
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(25);
const [loadingAvatarId, setLoadingAvatarId] = useState(null);
const [columns, setColumns] = useState([
{
key: 'name',
label:'Name',
show: true
},
{
key: 'chinese_name',
label: 'Preferred Name',
show: true
},
{
key: 'email',
label: 'Email',
show: true
},
{
key: 'type',
label: 'Type',
show: true
},
{
key: 'pickup_status',
label: 'Pickup Status',
show: true
},
{
key: 'birth_date',
label: 'Date of Birth',
show: true
},
{
key: 'gender',
label: 'Gender',
show: true
},
{
key: 'language',
label: 'Language',
show: true
},
{
key: 'medicare_number',
label: 'Medicare Number',
show: true
},
{
key: 'medicaid_number',
label: 'Medicaid Number',
show: true
},
{
key: 'address',
label: 'Address',
show: true
},
{
key: 'phone',
label: 'Phone',
show: true
},
{
key: 'emergency_contact',
label: 'Fasting',
show: true
},
{
key: 'health_condition',
label: 'Health Condition',
show: true
},
{
key: 'payment_status',
label: 'Payment Status',
show: true
},
{
key: 'payment_due_date',
label: 'Payment Due Date',
show: true
},
{
key: 'service_requirement',
label: 'Service Requirement',
show: true
},
{
key: 'tags',
label: 'Tags',
show: true
}
const [columns] = useState([
{ key: 'name', label:'Name', show: true },
{ key: 'chinese_name', label: 'Preferred Name', show: true },
{ key: 'email', label: 'Email', show: true },
{ key: 'type', label: 'Type', show: true },
{ key: 'pickup_status', label: 'Pickup Status', show: true },
{ key: 'birth_date', label: 'Date of Birth', show: true },
{ key: 'gender', label: 'Gender', show: true },
{ key: 'language', label: 'Language', show: true },
{ key: 'medicare_number', label: 'Medicare Number', show: true },
{ key: 'medicaid_number', label: 'Medicaid Number', show: true },
{ key: 'address', label: 'Address', show: true },
{ key: 'phone', label: 'Phone', show: true },
{ key: 'emergency_contact', label: 'Fasting', show: true },
{ key: 'health_condition', label: 'Health Condition', show: true },
{ key: 'payment_status', label: 'Payment Status', show: true },
{ key: 'payment_due_date', label: 'Payment Due Date', show: true },
{ key: 'service_requirement', label: 'Service Requirement', show: true },
{ key: 'tags', label: 'Tags', show: true }
]);
useEffect(() => {
@@ -130,660 +34,47 @@ const CustomersList = () => {
window.alert('You haven\'t login yet OR this user does not have access to this page. Please change an admin account to login.')
AuthService.logout();
navigate(`/login`);
return;
}
CustomerService.getAllCustomers().then((data) => {
const customerData = 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);
const customerData = 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);
setCustomers(customerData);
// Removed bulk avatar loading - now loaded on-demand when user clicks
})
LabelService.getAll().then((data) => {
setAvailableLabels(data.data);
})
}, []);
// Load avatar on-demand when user clicks on profile icon
const loadAvatarOnDemand = (customerId) => {
if (customerAvatars[customerId] !== undefined) return; // Already loaded or attempted
setLoadingAvatarId(customerId);
CustomerService.getAvatarAsBlob(`customer_avatar_${customerId}`)
.then(result => {
if (result?.data && result.data.size > 0) {
const url = URL.createObjectURL(result.data);
setCustomerAvatars(prev => ({ ...prev, [customerId]: url }));
} else {
setCustomerAvatars(prev => ({ ...prev, [customerId]: null }));
}
setLoadingAvatarId(null);
})
.catch(() => {
setCustomerAvatars(prev => ({ ...prev, [customerId]: null }));
setLoadingAvatarId(null);
});
};
useEffect(() => {
let filtered = customers;
// Basic keyword filter
if (keyword) {
filtered = filtered.filter((item) => item?.name.toLowerCase().includes(keyword.toLowerCase()));
}
// Active/Inactive filter
if (showInactive) {
filtered = filtered.filter(item => (item.type === CUSTOMER_TYPE.TRANSFERRED || item.type === CUSTOMER_TYPE.DECEASED || item.type === CUSTOMER_TYPE.DISCHARED) && item.status !== 'active');
} else {
filtered = filtered.filter(item => (item.type !== CUSTOMER_TYPE.TRANSFERRED && item.type!=CUSTOMER_TYPE.DECEASED && item.type!=CUSTOMER_TYPE.DISCHARED) && item.status === 'active');
}
// Health condition filter
if (healthConditionFilter) {
filtered = filtered.filter(item => item?.health_condition === healthConditionFilter);
}
// Payment status filter
if (paymentStatusFilter) {
filtered = filtered.filter(item => item?.payment_status === paymentStatusFilter);
}
// Service requirement filter
if (serviceRequirementFilter) {
filtered = filtered.filter(item => item?.service_requirement === serviceRequirementFilter);
}
// 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));
});
}
setFilteredCustomers(filtered);
setCurrentPage(1); // Reset to first page when filters change
}, [keyword, customers, showInactive, healthConditionFilter, paymentStatusFilter, serviceRequirementFilter, tagsFilter])
// Pagination calculations
const totalPages = Math.ceil(filteredCustomers.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex);
const goToPage = (page) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
if (currentPage <= 3) {
for (let i = 1; i <= 4; i++) pages.push(i);
pages.push('...');
pages.push(totalPages);
} else if (currentPage >= totalPages - 2) {
pages.push(1);
pages.push('...');
for (let i = totalPages - 3; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
pages.push('...');
for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i);
pages.push('...');
pages.push(totalPages);
}
}
return pages;
};
useEffect(() => {
const newCustomers = [...customers];
const sortedCustomers = sorting.key === '' ? newCustomers : newCustomers.sort((a, b) => {
return a[sorting.key]?.localeCompare(b[sorting.key]);
});
setCustomers(
sorting.order === 'asc' ? sortedCustomers : sortedCustomers.reverse()
)
}, [sorting]);
const getSortingImg = (key) => {
return sorting.key === key ? (sorting.order === 'asc' ? 'up_arrow' : 'down_arrow') : 'default';
}
const sortTableWithField = (key) => {
let newSorting = {
key,
order: 'asc',
}
if (sorting.key === key && sorting.order === 'asc') {
newSorting = {...newSorting, order: 'desc'};
}
setSorting(newSorting);
}
const toggleSelectedAllItems = () => {
if (selectedItems.length !== filteredCustomers.length || selectedItems.length === 0) {
const newSelectedItems = [...filteredCustomers].map((customer) => customer.id);
setSelectedItems(newSelectedItems);
} else {
setSelectedItems([]);
}
}
const toggleItem = (id) => {
if (selectedItems.includes(id)) {
const newSelectedItems = [...selectedItems].filter((item) => item !== id);
setSelectedItems(newSelectedItems);
} else {
const newSelectedItems = [...selectedItems, id];
setSelectedItems(newSelectedItems);
}
}
const showArchive = (value) => {
console.log('here', value);
setShowInactive(value === 'archivedCustomers');
// Recover all filters
setKeyword('');
setSorting({key: '', order: ''});
setSelectedItems([]);
}
const checkSelectAll = () => {
return selectedItems.length === filteredCustomers.length && selectedItems.length > 0;
}
const cleanFilterAndClose = () => {
setHealthConditionFilter('');
setPaymentStatusFilter('');
setServiceRequirementFilter('');
setTagsFilter([]);
setShowFilterDropdown(false);
}
const FilterAndClose = () => {
setShowFilterDropdown(false);
}
const toggleTagFilter = (tagName) => {
if (tagsFilter.includes(tagName)) {
setTagsFilter(tagsFilter.filter(tag => tag !== tagName));
} else {
setTagsFilter([...tagsFilter, tagName]);
}
}
const handleColumnsChange = (newColumns) => {
setColumns(newColumns);
}
const goToEdit = (id) => {
navigate(`/customers/edit/${id}`)
}
}, [navigate]);
const goToCreateNew = () => {
navigate(`/customers`)
navigate(`/customers`);
}
const setTransferValue = (customerId, site) => {
const currentMap = Object.assign({}, transferMap);
if (site !== undefined && site !== null && site !== '' && site !== 0) {
currentMap[customerId] = site;
setTransferMap(currentMap);
} else {
if (customerId) {
delete currentMap[customerId];
setTransferMap(currentMap);
}
}
}
const goToView = (id) => {
navigate(`/customers/${id}`)
}
const site = EventsService.site;
const transferCustomer = (customerId) => {
if (site !== undefined && site !== null && site !== '' && site !== 0) {
setShowSpinner(true);
const currentCustomer = customers.find((c) => c.id === customerId);
if (currentCustomer) {
EventsService.getByCustomer({name: currentCustomer?.name, id: currentCustomer?.id, namecn: currentCustomer?.name_cn}).then((eventsData) => {
const events = eventsData?.data;
CustomerService.updateCustomer(customerId, { ...currentCustomer, site: transferMap[customerId] }).then(() => {
// const eventsWithCustomer = events.filter(ev => ev?.data?.customer === customerId || ev?.data?.client_name === currentCustomer?.name || ev?.target_name === currentCustomer?.name);
if (events?.length > 0) {
Promise.all(events?.map(
item => EventsService.updateEvent(item?.id, {
...item, site: transferMap[customerId]
}))).then(() => {
CustomerService.getAllCustomers().then((data) => {
setCustomers(data.data?.sort((a, b) => a.lastname > b.lastname ? 1: -1));
setShowSpinner(false);
})
setShowSpinner(false);
}).catch((err) => setShowSpinner(false))
} else {
CustomerService.getAllCustomers().then((data) => {
setCustomers(data.data?.sort((a, b) => a.lastname > b.lastname ? 1: -1));
setShowSpinner(false);
})
}
}).catch(err => setShowSpinner(false))
})
}
}
}
const exportCSV = (customer) => {
const csvString = [
[...Object.keys(customer)], // Specify your headers here
Object.keys(customer).map((key) => key && customer[key] && `"${customer[key]}"` || "") // Map your data fields accordingly
]
.map(row => row.join(","))
.join("\n");
// Create a Blob from the CSV string
const blob = new Blob([csvString], { type: 'text/csv' });
// Generate a download link and initiate the download
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `customer_${customer.name}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
EventsService.getByCustomer({name: customer?.name, id: customer?.id, namecn: customer?.name_cn}).then((data) => {
const events = data.data;
if (events && events?.length > 0) {
const lastEle = events[events.length - 1]
const eventscsvString = [
[...Object.keys(lastEle).filter(item => item !== 'data'), ...Object.keys(lastEle?.data)],
...events.map((event) => {
return [
...Object.keys(lastEle).filter(item => item !== 'data').map((key) => event[key] && `"${event[key]}"` || ''),
...Object.keys(lastEle?.data).map((key) => event?.data[key] && `"${event?.data[key]}"` || '')
]
})
].map(row => row.join(","))
.join("\n");
// Create a Blob from the CSV string
const blobCSV = new Blob([eventscsvString], { type: 'text/csv' });
// Generate a download link and initiate the download
const urlCSV = URL.createObjectURL(blobCSV);
const csvlink = document.createElement('a');
csvlink.href = urlCSV;
csvlink.download = `Customer_${customer.name}_Medical_Events.csv`;
document.body.appendChild(csvlink);
csvlink.click();
document.body.removeChild(csvlink);
URL.revokeObjectURL(urlCSV);
} else {
window.alert('No medical events found for this user')
}
})
}
const table = <div className="list row mb-4">
<div className="col-md-12">
<div style={{
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'auto',
position: 'relative'
}}>
<table className="personnel-info-table" style={{ position: 'relative' }}>
<thead>
<tr>
<th className="th-checkbox" style={{ position: 'sticky', left: 0, zIndex: 3, backgroundColor: '#0066B1', paddingLeft: '14px' }}><input type="checkbox" checked={checkSelectAll()} onClick={() => toggleSelectedAllItems()}></input></th>
<th className="th-index" style={{ position: 'sticky', left: '50px', zIndex: 3, backgroundColor: '#0066B1', paddingLeft: '14px' }}>No.</th>
{
columns.filter(col => col.show).map((column, index) => <th
className="sortable-header"
key={index}
style={column.key === 'name' ? {
position: 'sticky',
left: '100px',
zIndex: 3,
backgroundColor: '#0066B1',
paddingLeft: '14px',
//borderRight: '2px solid #0056a0',
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
} : {}}
>
{column.label} <span className="float-right" onClick={() => sortTableWithField(column.key)}><img src={`/images/${getSortingImg(column.key)}.png`}></img></span>
</th>)
}
<th></th>
<th>Transfer To</th>
</tr>
</thead>
<tbody>
{
paginatedCustomers.map((customer, index) => {
const avatarData = customerAvatars[customer.id];
const isLoadingAvatar = loadingAvatarId === customer.id;
const profilePopover = (
<Popover id={`popover-${customer.id}`}>
<Popover.Body style={{ textAlign: 'center' }}>
{isLoadingAvatar ? (
<Spinner animation="border" size="sm" />
) : avatarData ? (
<img
src={avatarData}
alt={customer.name}
style={{ width: '200px', height: '200px', objectFit: 'cover', borderRadius: '8px' }}
/>
) : (
<PersonSquare size={200} />
)}
</Popover.Body>
</Popover>
);
const actualIndex = startIndex + index; // Calculate actual index for display
const rowBgColor = index % 2 === 0 ? 'white' : '#eee';
const stickyBaseStyle = {
position: 'sticky',
zIndex: 2,
paddingLeft: '14px',
};
const checkboxStickyStyle = {
...stickyBaseStyle,
left: 0,
backgroundColor: rowBgColor,
};
const indexStickyStyle = {
...stickyBaseStyle,
left: '50px',
backgroundColor: rowBgColor,
};
const nameStickyStyle = {
...stickyBaseStyle,
left: '100px',
backgroundColor: rowBgColor,
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
};
return <tr key={customer.id}>
<td className="td-checkbox" style={checkboxStickyStyle}><input type="checkbox" checked={selectedItems.includes(customer.id)} onClick={()=>toggleItem(customer?.id)}/></td>
<td className="td-index" style={indexStickyStyle}>{actualIndex + 1}</td>
{columns.find(col => col.key === 'name')?.show && <td style={nameStickyStyle}>
{AuthService.canAddOrEditCustomers() && <PencilSquare size={16} className="clickable me-2" onClick={() => goToEdit(customer?.id)}></PencilSquare>}
{AuthService.canViewCustomers() && (
<OverlayTrigger
trigger="click"
placement="right"
overlay={profilePopover}
rootClose
>
<span style={{ cursor: 'pointer' }} onClick={() => loadAvatarOnDemand(customer.id)}>
<PersonSquare size={16} className="clickable me-2" />
</span>
</OverlayTrigger>
)}
<span className="clickable" onClick={() => goToView(customer?.id)} style={{ textDecoration: 'underline', cursor: 'pointer' }}>{customer?.name}</span>
</td>}
{columns.find(col => col.key === 'chinese_name')?.show && <td>{customer?.name_cn}</td>}
{columns.find(col => col.key === 'email')?.show && <td>{customer?.email}</td>}
{columns.find(col => col.key === 'type')?.show && <td>{customer?.type}</td>}
{columns.find(col => col.key === 'pickup_status')?.show && <td>{customer?.pickup_status}</td>}
{columns.find(col => col.key === 'birth_date')?.show && <td>{customer?.birth_date}</td>}
{columns.find(col => col.key === 'gender')?.show && <td>{customer?.gender}</td>}
{columns.find(col => col.key === 'language')?.show && <td>{customer?.language}</td>}
{columns.find(col => col.key === 'medicare_number')?.show && <td>{customer?.medicare_number}</td>}
{columns.find(col => col.key === 'medicaid_number')?.show && <td>{customer?.medicaid_number}</td>}
{columns.find(col => col.key === 'address')?.show && <td>{customer?.address1 || customer?.address2 || customer?.address3 || customer?.address4 || customer?.address5}</td>}
{columns.find(col => col.key === 'phone')?.show && <td>{customer?.phone || customer?.home_phone || customer?.mobile_phone}</td>}
{columns.find(col => col.key === 'emergency_contact')?.show && <td>{customer?.emergency_contact}</td>}
{columns.find(col => col.key === 'health_condition')?.show && <td>{customer?.health_condition}</td>}
{columns.find(col => col.key === 'payment_status')?.show && <td>{customer?.payment_status}</td>}
{columns.find(col => col.key === 'payment_due_date')?.show && <td>{customer?.payment_due_date}</td>}
{columns.find(col => col.key === 'service_requirement')?.show && <td>{customer?.service_requirement}</td>}
{columns.find(col => col.key === 'tags')?.show && <td>{customer?.tags?.join(', ')}</td>}
<td>
{AuthService.canViewCustomers() && <button className="btn btn-link btn-sm me-2" onClick={() => exportCSV(customer)}>Export Medical Events</button>}
</td>
<td>
{AuthService.canAddOrEditCustomers() &&
<div>
<select className="transfer-select" value={transferMap[customer?.id]} onChange={(e) => setTransferValue(customer?.id, e.target.value)}>
<option value=""></option>
{ site !== 1 && <option value="1">Gaithersburg - 1</option>}
{ site !== 2 && <option value="2">Beltsville - 2</option>}
{ site !== 3 && <option value="3">Frederick - 3</option>}
</select>
{ transferMap[customer?.id] && <button className="btn btn-primary btn-sm me-2" onClick={() => transferCustomer(customer?.id)}>Confirm</button> }
</div>
}
</td>
</tr>
})
}
</tbody>
</table>
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="pagination-controls" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '15px 0',
marginTop: '10px'
}}>
<div style={{ color: '#666', fontSize: '13px' }}>
Showing {startIndex + 1} - {Math.min(endIndex, filteredCustomers.length)} of {filteredCustomers.length} customers
</div>
<div style={{ display: 'flex', gap: '5px', alignItems: 'center' }}>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(1)}
disabled={currentPage === 1}
style={{ padding: '4px 8px' }}
>
First
</button>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
style={{ padding: '4px 8px' }}
>
Prev
</button>
{getPageNumbers().map((page, index) => (
page === '...' ? (
<span key={`ellipsis-${index}`} style={{ padding: '4px 8px' }}>...</span>
) : (
<button
key={page}
className={`btn btn-sm ${currentPage === page ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => goToPage(page)}
style={{ padding: '4px 10px', minWidth: '35px' }}
>
{page}
</button>
)
))}
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
style={{ padding: '4px 8px' }}
>
Next
</button>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => goToPage(totalPages)}
disabled={currentPage === totalPages}
style={{ padding: '4px 8px' }}
>
Last
</button>
</div>
</div>
)}
</div>
</div>;
const customFilterMenu = React.forwardRef(
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
return (
<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">Health Condition</div>
<select value={healthConditionFilter} onChange={(e) => setHealthConditionFilter(e.target.value)}>
<option value=""></option>
<option value="diabetes">Diabetes</option>
<option value="1-1">1-1</option>
<option value="rounding list">Rounding List</option>
<option value="MOLST/POA/Advanced Directive">MOLST/POA/Advanced Directive</option>
</select>
</div>
</div>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Payment Status</div>
<select value={paymentStatusFilter} onChange={(e) => setPaymentStatusFilter(e.target.value)}>
<option value=""></option>
<option value="paid">Paid</option>
<option value="overdue">Overdue</option>
</select>
</div>
</div>
<div className="app-main-content-fields-section margin-sm dropdown-container">
<div className="me-4">
<div className="field-label">Service Requirement</div>
<select value={serviceRequirementFilter} onChange={(e) => setServiceRequirementFilter(e.target.value)}>
<option value=""></option>
<option value="wheelchair">Wheelchair</option>
<option value="special care">Special Care</option>
</select>
</div>
</div>
<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>
</div>
</div>
<div className="list row">
<div className="col-md-12">
<button className="btn btn-default btn-sm float-right" onClick={() => cleanFilterAndClose()}> Cancel </button>
<button className="btn btn-primary btn-sm float-right" onClick={() => FilterAndClose()}> Filter </button>
</div>
</div>
</div>
);
},
const additionalButtons = (
<>
<button className="btn btn-primary me-2" onClick={() => goToCreateNew()}>
<Plus size={16}></Plus>Add New Customer
</button>
<Export
columns={columns}
data={customers.map((customer) => ({
...customer,
address: customer?.address1 || customer?.address2 || customer?.address3 || customer?.address4 || customer?.address5,
phone: customer?.phone || customer?.home_phone || customer?.mobile_phone,
tags: customer?.tags?.join(', ')
}))}
filename="customers"
/>
</>
);
return (
<>
{showSpinner && <div className="spinner-overlay">
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>}
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Customer Information
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
All Customers
{/* <button className="btn btn-primary btn-sm" onClick={() => {goToCreateNew()}}>Create New Customer</button>
<button className="btn btn-link btn-sm" onClick={() => {redirectToAdmin()}}>Back</button> */}
</h4>
</div>
</div>
<div className="app-main-content-list-container list-page">
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeCustomers" id="customers-tab" onSelect={(k) => showArchive(k)}>
<Tab eventKey="activeCustomers" title="Active Customers">
{table}
</Tab>
<Tab eventKey="archivedCustomers" title="Discharge Customers">
{table}
</Tab>
</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)} />
<Dropdown
key={'filter-customers'}
id="filter-customers"
className="me-2"
show={showFilterDropdown}
onToggle={() => setShowFilterDropdown(!showFilterDropdown)}
autoClose={false}
>
<Dropdown.Toggle variant="primary">
<Filter size={16} className="me-2"></Filter>Filter
</Dropdown.Toggle>
<Dropdown.Menu as={customFilterMenu}/>
</Dropdown>
<ManageTable columns={columns} onColumnsChange={handleColumnsChange} />
<button className="btn btn-primary me-2" onClick={() => goToCreateNew()}><Plus size={16}></Plus>Add New Customer</button>
<Export
columns={columns}
data={filteredCustomers.map((customer) => ({
...customer,
address: customer?.address1 || customer?.address2 || customer?.address3 || customer?.address4 || customer?.address5,
phone: customer?.phone || customer?.home_phone || customer?.mobile_phone,
tags: customer?.tags?.join(', ')
}))}
filename="customers"
/>
</div>
</div>
</div>
</>
)
<DashboardCustomersList
showBreadcrumb={true}
title="All Customers"
additionalButtons={additionalButtons}
/>
);
};
export default CustomersList;
export default CustomersList;

View File

@@ -35,11 +35,19 @@
.dashboard-event-item {
padding: 8px;
border-radius: 4px;
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
word-wrap: break-word;
overflow-wrap: break-word;
}
.dashboard-event-title {
font-size: 0.85rem;
font-weight: 500;
word-wrap: break-word;
overflow-wrap: break-word;
}
.dashboard-event-time {
@@ -50,4 +58,25 @@
.dashboard-event-description {
font-size: 0.75rem;
color: #666;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Ensure right sidebar content stays within bounds */
.dashboard-right-sidebar .column-card {
width: 100%;
max-width: 100%;
overflow: hidden;
}
.dashboard-right-sidebar .column-card > div {
width: 100%;
max-width: 100%;
}
/* Override min-width for event tiles in dashboard to ensure they fit */
.dashboard-right-sidebar .event-list-item-container {
min-width: 0;
width: 100%;
max-width: 100%;
}

View File

@@ -7,7 +7,7 @@ import { CUSTOMER_TYPE, ManageTable } from "../../shared";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown } from "react-bootstrap";
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
const DashboardCustomersList = () => {
const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, title = null }) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const site = EventsService.site;
@@ -20,6 +20,7 @@ const DashboardCustomersList = () => {
const [selectedItems, setSelectedItems] = useState([]);
const [filteredCustomers, setFilteredCustomers] = useState(customers);
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
const [showManageTableDropdown, setShowManageTableDropdown] = useState(false);
const [healthConditionFilter, setHealthConditionFilter] = useState('');
const [paymentStatusFilter, setPaymentStatusFilter] = useState('');
const [serviceRequirementFilter, setServiceRequirementFilter] = useState('');
@@ -393,7 +394,20 @@ const DashboardCustomersList = () => {
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>}
<div className="app-main-content-list-container">
{showBreadcrumb && (
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/">General</Breadcrumb.Item>
<Breadcrumb.Item active>
Customer Information
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>{title || 'All Customers'}</h4>
</div>
</div>
)}
<div className={`app-main-content-list-container ${showBreadcrumb ? 'list-page' : ''}`}>
<div className="app-main-content-list-func-container">
<Tabs defaultActiveKey="activeCustomers" id="customers-tab" onSelect={(k) => showArchive(k)}>
<Tab eventKey="activeCustomers" title="Active Customers">
@@ -410,7 +424,12 @@ const DashboardCustomersList = () => {
id="filter-customers"
className="me-2"
show={showFilterDropdown}
onToggle={() => setShowFilterDropdown(!showFilterDropdown)}
onToggle={(isOpen) => {
if (isOpen) {
setShowManageTableDropdown(false);
}
setShowFilterDropdown(isOpen);
}}
autoClose={false}
>
<Dropdown.Toggle variant="primary">
@@ -418,8 +437,18 @@ const DashboardCustomersList = () => {
</Dropdown.Toggle>
<Dropdown.Menu as={customFilterMenu}/>
</Dropdown>
<ManageTable columns={columns} onColumnsChange={handleColumnsChange} />
{/* Removed Create New Customer button and Export functionality */}
<ManageTable
columns={columns}
onColumnsChange={handleColumnsChange}
show={showManageTableDropdown}
onToggle={(isOpen) => {
if (isOpen) {
setShowFilterDropdown(false);
}
setShowManageTableDropdown(isOpen);
}}
/>
{additionalButtons}
</div>
</div>
</div>

View File

@@ -90,8 +90,7 @@ const RoutesSection = ({transRoutes, copyList, sectionName, drivers, vehicles, c
{`${sectionName}: `} <span className="route-stats">{
(sectionName.includes('Inbound') ||
sectionName.includes('Outbound')) &&
(`${seniors?.length} Scheduled ${seniors?.filter(
item => ![PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT, PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT].includes(item?.customer_route_status))?.length} ${ sectionName.includes('Inbound') ? 'Checked In': 'Checked Out'} (${seniors.filter(item => [CUSTOMER_TYPE.MEMBER, CUSTOMER_TYPE.SELF_PAY].includes(item.customer_type) && ![PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT, PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT].includes(item?.customer_route_status))?.length} Members ${seniors.filter(item=> [CUSTOMER_TYPE.VISITOR].includes(item?.customer_type) && ![PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT, PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT].includes(item?.customer_route_status))?.length} Visitors)`)}</span>
(`${seniors?.length} Scheduled (${seniors.filter(item => [CUSTOMER_TYPE.MEMBER, CUSTOMER_TYPE.SELF_PAY].includes(item.customer_type) && ![PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT, PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT].includes(item?.customer_route_status))?.length} Members ${seniors.filter(item=> [CUSTOMER_TYPE.VISITOR].includes(item?.customer_type) && ![PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT, PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT].includes(item?.customer_route_status))?.length} Visitors)`)}</span>
</h6>
{ canAddNew && (
<small className="me-4" onClick={() => { if (routeType) {redirect(routeType)} else {redirect()}}}>

View File

@@ -2,9 +2,13 @@ import React, { useState } from "react";
import { Dropdown } from "react-bootstrap";
import { Columns } from "react-bootstrap-icons";
const ManageTable = ({ columns, onColumnsChange }) => {
const [showManageTableDropdown, setShowManageTableDropdown] = useState(false);
const ManageTable = ({ columns, onColumnsChange, show, onToggle }) => {
const [internalShow, setInternalShow] = useState(false);
const [tempColumns, setTempColumns] = useState(columns);
// Use external control if provided, otherwise use internal state
const showManageTableDropdown = show !== undefined ? show : internalShow;
const handleToggle = onToggle || (() => setInternalShow(!internalShow));
const handleColumnToggle = (columnKey) => {
const updatedColumns = tempColumns.map(col =>
@@ -15,12 +19,20 @@ const ManageTable = ({ columns, onColumnsChange }) => {
const handleDone = () => {
onColumnsChange(tempColumns);
setShowManageTableDropdown(false);
if (onToggle) {
onToggle(false);
} else {
setInternalShow(false);
}
};
const handleCancel = () => {
setTempColumns(columns);
setShowManageTableDropdown(false);
if (onToggle) {
onToggle(false);
} else {
setInternalShow(false);
}
};
const customManageTableMenu = React.forwardRef(
@@ -69,7 +81,7 @@ const ManageTable = ({ columns, onColumnsChange }) => {
id="manage-table"
className="me-2"
show={showManageTableDropdown}
onToggle={() => setShowManageTableDropdown(!showManageTableDropdown)}
onToggle={handleToggle}
autoClose={false}
>
<Dropdown.Toggle variant="primary">