Files
worldshine-redesign/client/src/components/dashboard/DashboardCustomersList.js
2026-03-03 16:37:48 -05:00

489 lines
18 KiB
JavaScript

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 { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown, Modal } from "react-bootstrap";
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, title = null }) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const site = EventsService.site;
const [customers, setCustomers] = useState([]);
const [keyword, setKeyword] = useState('');
const [showInactive, setShowInactive] = useState(false);
const [transferMap, setTransferMap] = 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 [showManageTableDropdown, setShowManageTableDropdown] = useState(false);
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [tagsFilter, setTagsFilter] = useState([]);
const [availableLabels, setAvailableLabels] = useState([]);
const [showAvatarModal, setShowAvatarModal] = useState(false);
const [avatarData, setAvatarData] = useState(null);
const [avatarCustomerName, setAvatarCustomerName] = useState('');
const [avatarLoading, setAvatarLoading] = useState(false);
const [customerAvatars, setCustomerAvatars] = useState({});
const isAllCustomersPage = title === 'All Customers';
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: 'tags',
label: 'Tags',
show: true
}
]);
useEffect(() => {
if (!AuthService.canViewCustomers()) {
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`);
}
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);
})
}, []);
// Load avatars for customers - same approach as ViewCustomer
useEffect(() => {
if (customers.length === 0) return;
customers.forEach((customer) => {
if (customerAvatars[customer.id] !== undefined) return; // Already loaded or attempted
// Mark as loading to prevent duplicate requests
setCustomerAvatars(prev => ({ ...prev, [customer.id]: false }));
CustomerService.getAvatar(customer.id)
.then(result => {
if (result?.data) {
// Store raw data exactly like ViewCustomer does
setCustomerAvatars(prev => ({ ...prev, [customer.id]: result.data }));
} else {
setCustomerAvatars(prev => ({ ...prev, [customer.id]: null }));
}
})
.catch(() => {
setCustomerAvatars(prev => ({ ...prev, [customer.id]: null }));
});
});
}, [customers]);
useEffect(() => {
let filtered = customers;
// Basic keyword filter
if (keyword) {
filtered = filtered.filter((item) => item?.name.toLowerCase().includes(keyword.toLowerCase()));
}
// Active/Inactive filter
if (showInactive) {
// Discharged Customers tab: show customers with type discharged, transferred, or deceased
filtered = filtered.filter(item => item.type === CUSTOMER_TYPE.TRANSFERRED || item.type === CUSTOMER_TYPE.DECEASED || item.type === CUSTOMER_TYPE.DISCHARGED);
} else {
// Active Customers tab: show customers who are active AND not discharged/transferred/deceased
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));
});
}
setFilteredCustomers(filtered);
}, [keyword, customers, showInactive, tagsFilter])
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) => {
if (sorting.key === key) {
setSorting({key: key, order: sorting.order === 'asc' ? 'desc' : 'asc'});
} else {
setSorting({key: key, order: 'asc'});
}
}
const toggleSelectedAllItems = () => {
if (selectedItems.length === filteredCustomers.length) {
setSelectedItems([]);
} else {
setSelectedItems(filteredCustomers.map(item => item.id));
}
}
const toggleItem = (id) => {
if (selectedItems.includes(id)) {
setSelectedItems(selectedItems.filter(item => item !== id));
} else {
setSelectedItems([...selectedItems, id]);
}
}
const showArchive = (value) => {
setShowInactive(value === 'archivedCustomers');
}
const checkSelectAll = () => {
return selectedItems.length === filteredCustomers.length && filteredCustomers.length > 0;
}
const cleanFilterAndClose = () => {
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}`);
}
const goToCreateNew = () => {
navigate(`/customers`);
}
const setTransferValue = (customerId, site) => {
setTransferMap(prev => ({
...prev,
[customerId]: site
}));
}
const goToView = (id) => {
navigate(`/customers/${id}`);
}
const showProfilePicture = (customer) => {
setAvatarCustomerName(customer?.name || '');
setAvatarData(null);
setAvatarLoading(true);
setShowAvatarModal(true);
CustomerService.getAvatar(customer?.id).then((data) => {
setAvatarData(data.data);
setAvatarLoading(false);
}).catch(() => {
setAvatarLoading(false);
});
}
const closeAvatarModal = () => {
setShowAvatarModal(false);
setAvatarData(null);
setAvatarCustomerName('');
}
const table = <div className="list row mb-4">
<div className="col-md-12" style={{ overflow: 'auto'}}>
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-index">No.</th>
{
columns.filter(col => col.show).map((column, index) => <th className="sortable-header" key={index}>
{column.label} <span className="float-right" onClick={() => sortTableWithField(column.key)}><img src={`/images/${getSortingImg(column.key)}.png`}></img></span>
</th>)
}
</tr>
</thead>
<tbody>
{
filteredCustomers.map((customer, index) => <tr key={customer.id}>
<td className="td-index">{index + 1}</td>
{columns.find(col => col.key === 'name')?.show && <td style={{ verticalAlign: 'middle' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{AuthService.canViewCustomers() && (
customerAvatars[customer.id] && customerAvatars[customer.id] !== false ? (
<img
src={`data:image/png;base64, ${customerAvatars[customer.id]}`}
alt={customer?.name}
onClick={() => showProfilePicture(customer)}
style={{
width: '64px',
height: '64px',
borderRadius: '8px',
objectFit: 'cover',
cursor: 'pointer',
flexShrink: 0
}}
/>
) : (
<PersonSquare
onClick={() => showProfilePicture(customer)}
size={64}
className="clickable"
style={{ flexShrink: 0, color: '#ccc' }}
/>
)
)}
{AuthService.canAddOrEditCustomers() && <PencilSquare size={16} className="clickable" onClick={() => isAllCustomersPage ? goToEdit(customer?.id) : goToView(customer?.id)} style={{ flexShrink: 0 }}></PencilSquare>}
<span className="clickable" style={{ color: '#0066B1', textDecoration: 'underline', cursor: 'pointer' }} onClick={() => goToView(customer?.id)}>{customer?.name}</span>
</div>
</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 === 'tags')?.show && <td>{customer?.tags?.join(', ')}</td>}
</tr>)
}
</tbody>
</table>
</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">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>
);
},
);
return (
<>
{showSpinner && <div className="spinner-overlay">
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>}
{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">
{table}
</Tab>
<Tab eventKey="archivedCustomers" title="Discharged 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={(isOpen) => {
if (isOpen) {
setShowManageTableDropdown(false);
setShowExportDropdown(false);
}
setShowFilterDropdown(isOpen);
}}
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}
show={showManageTableDropdown}
onToggle={(isOpen) => {
if (isOpen) {
setShowFilterDropdown(false);
setShowExportDropdown(false);
}
setShowManageTableDropdown(isOpen);
}}
/>
{typeof additionalButtons === 'function'
? additionalButtons({
showExportDropdown,
onExportToggle: (isOpen) => {
if (isOpen) {
setShowFilterDropdown(false);
setShowManageTableDropdown(false);
}
setShowExportDropdown(isOpen);
}
})
: additionalButtons
}
</div>
</div>
</div>
<Modal show={showAvatarModal} onHide={closeAvatarModal} centered>
<Modal.Header closeButton>
<Modal.Title style={{ fontSize: '16px' }}>{avatarCustomerName}</Modal.Title>
</Modal.Header>
<Modal.Body style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
{avatarLoading && <Spinner animation="border" variant="primary" />}
{!avatarLoading && avatarData && (
<img
src={`data:image/png;base64, ${avatarData}`}
alt={avatarCustomerName}
style={{ maxWidth: '100%', maxHeight: '400px', objectFit: 'contain' }}
/>
)}
{!avatarLoading && !avatarData && (
<div style={{ textAlign: 'center', color: '#999' }}>No profile picture available</div>
)}
</Modal.Body>
</Modal>
</>
)
};
export default DashboardCustomersList;