Files
worldshine-redesign/client/src/components/events/EventsList.js
Lixian Zhou e3c818eacd
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 29s
fix
2026-03-12 15:20:46 -04:00

590 lines
27 KiB
JavaScript

import React, {useState, useEffect} from "react";
import { useNavigate } from "react-router-dom";
import { AuthService, EventsService, CustomerService, DriverService, ResourceService } from "../../services";
import DatePicker from "react-datepicker";
import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Button, Modal } from "react-bootstrap";
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
import TimePicker from 'react-time-picker';
import 'react-time-picker/dist/TimePicker.css';
import { ManageTable, Export } from "../../shared/components";
const EventsList = () => {
const navigate = useNavigate();
const [events, setEvents] = useState([]);
const [customers, setCustomers] = useState([]);
const [resources, setResources] = useState([]);
const [selectedDate, setSelectedDate] = useState(new Date());
// Multi-column sorting: array of {key, order} objects
// First item has highest priority, subsequent items are secondary sorts
const [sortingRules, setSortingRules] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
const [showTransportationModal, setShowTransportationModal] = useState(false);
const [driver, setDriver] = useState(null);
const [driverOptions, setDriverOptions] = useState([]);
const [transportStartTime, setTransportStartTime] = useState(null);
const [transportOptionsList, setTransportationOptionsList] = useState([]);
const [transportSelected, setTransportSelected] = useState(null);
const [showDeletedItems, setShowDeletedItems] = useState(false);
const [columns, setColumns] = useState([
{
key: 'customer',
label:'Customer Name',
show: true
},
{
key: 'member_type',
label: 'Customer Type',
show: true
},
{
key: 'translation',
label: 'Language Support',
show: true
},
{
key: 'transMethod',
label: 'Transportation Support',
show: true
},
{
key: 'transportation',
label: 'Driver',
show: true
},
{
key: 'startTime',
label: 'Appointment Time',
show: true
},
{
key: 'eyes_on',
label: 'Eyes-On',
show: true
},
{
key: 'newPatient',
label: 'New Patient',
show: true
},
{
key: 'needId',
label: 'ID Needed',
show: true
},
{
key: 'doctor',
label: 'Provider',
show: true
},
{
key: 'phone',
label: 'Provider Phone Number',
show: true
},
{
key: 'address',
label: 'Provider Address',
show: true
},
{
key: 'fasting',
label: 'Fasting Required',
show: true
}
]);
const checkDisability = (customers, event) => {
const currentCustomer = customers?.find(c => c?.id === event?.data?.customer || c?.name === event?.data?.client_name || c?.name === event?.target_name);
return currentCustomer?.disability || event?.data?.disability?.toLowerCase() === 'yes' || false;
};
// Default sort: 1) driver name ascending, 2) start time early to late, 3) address ascending, 4) language (empty first)
const applyDefaultSort = (eventsArray) => {
return [...eventsArray].sort((a, b) => {
// 1. First sort by driver (transportation) name ascending
const driverA = (a.transportation || '').toLowerCase();
const driverB = (b.transportation || '').toLowerCase();
if (driverA !== driverB) {
return driverA.localeCompare(driverB);
}
// 2. Then sort by start time (early to late)
const timeA = a.start_time ? new Date(a.start_time).getTime() : 0;
const timeB = b.start_time ? new Date(b.start_time).getTime() : 0;
if (timeA !== timeB) {
return timeA - timeB;
}
// 3. Then sort by resource address ascending
const addressA = (a.address || '').toLowerCase();
const addressB = (b.address || '').toLowerCase();
if (addressA !== addressB) {
return addressA.localeCompare(addressB);
}
// 4. Finally sort by language (empty values first, then values with content)
const langA = (a.translation || '').trim();
const langB = (b.translation || '').trim();
if (langA === '' && langB !== '') return -1; // Empty comes first
if (langA !== '' && langB === '') return 1; // Empty comes first
return langA.localeCompare(langB); // Both empty or both have values - sort alphabetically
});
};
useEffect(() => {
if (!AuthService.canViewMedicalEvents()) {
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);
})
DriverService.getAllActiveDrivers().then((data) => {
console.log('drivers', data.data);
setDriverOptions(data.data);
})
ResourceService.getAll().then(data => {
setResources(data.data);
})
}, []);
useEffect(() => {
if (customers?.length > 0 && resources?.length>0) {
EventsService.getAllEvents({ date: EventsService.formatDate(selectedDate) }).then((data) => {
const processedEvents = data.data.filter((item) => {
item.customer = item?.data?.customer ? (customers.find(c=>c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || '');
item.doctor = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || '');
item.phone = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || '');
item.address = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.address || item?.data?.resource_address || '') : (item?.data?.resource_address || '');
item.translation = item?.data?.interpreter || '';
item.newPatient = item?.data?.new_patient || '';
item.needId = item?.data?.need_id || '';
item.disability = item?.data?.disability || '';
item.member_type = item?.data?.customer ? (customers.find(c => c.id === item?.data?.customer)?.type || '') : '';
item.eyes_on = checkDisability(customers, item) ? 'Yes' : 'No';
item.startTime = item?.start_time? `${new Date(item?.start_time).toLocaleDateString()} ${new Date(item?.start_time).toLocaleTimeString()}` : '' ;
item.fasting = item?.data?.fasting || '';
item.chinese_name = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.name_cn : (customers?.find(c=> c?.name === item?.data?.client_name || c?.name === item?.target_name )?.name_cn || '');
item.transportation = item?.link_event_name || '';
item.dob = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.birth_date : (item?.data?.client_birth_date || '');
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed);
// Apply default sort (driver name → start time → address)
const sortedEvents = applyDefaultSort(processedEvents);
setEvents(sortedEvents);
setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'))
})
}
}, [selectedDate, resources, customers]);
// Apply multi-column sorting
// After all custom sorting rules, always sort by language (empty first)
const applyMultiColumnSort = (eventsArray, rules) => {
if (rules.length === 0) {
return applyDefaultSort(eventsArray);
}
return [...eventsArray].sort((a, b) => {
// First apply all custom sorting rules
for (const rule of rules) {
const valA = (a[rule.key] || '').toString().toLowerCase();
const valB = (b[rule.key] || '').toString().toLowerCase();
const comparison = valA.localeCompare(valB);
if (comparison !== 0) {
return rule.order === 'desc' ? -comparison : comparison;
}
}
// After all rules, always sort by language (empty values first, then values with content)
const langA = (a.translation || '').trim();
const langB = (b.translation || '').trim();
if (langA === '' && langB !== '') return -1; // Empty comes first
if (langA !== '' && langB === '') return 1; // Empty comes first
return langA.localeCompare(langB); // Both empty or both have values - sort alphabetically
});
};
useEffect(() => {
if (events.length === 0) return;
const sortedEvents = applyMultiColumnSort(events, sortingRules);
setEvents(sortedEvents);
}, [sortingRules]);
const redirectToAdmin = () => {
navigate(`/medical`)
}
const goToEdit = (id) => {
if (!AuthService.canEditMedicalEvents()) return;
navigate(`/medical/events/edit/${id}`)
}
const goToCreateNew = () => {
if (!AuthService.canEditMedicalEvents()) return;
navigate(`/medical/events`)
}
const goToEventsCalendar = () => {
navigate(`/medical/events/calendar`)
}
const goToMultipleList = () => {
navigate(`/medical/events/multiple-list`)
}
const goToResourceList = () => {
navigate(`/medical/resources/list`)
}
const goToView = (id) => {
navigate(`/medical/events/${id}`)
}
const goToNextDay = () => {
setSelectedDate(new Date(selectedDate.setDate(selectedDate.getDate() + 1)));
}
const goToPreviousDay = () => {
setSelectedDate(new Date(selectedDate.setDate(selectedDate.getDate() - 1)));
}
const showDeleted = (value) => {
setShowDeletedItems(value === 'archivedEvents');
// Recover all filters
// setKeyword('');
setSortingRules([]);
setSelectedItems([]);
}
// Get the sort indicator for a column (shows priority number if multi-column)
const getSortingImg = (key) => {
const ruleIndex = sortingRules.findIndex(rule => rule.key === key);
if (ruleIndex === -1) {
return 'default';
}
return sortingRules[ruleIndex].order === 'asc' ? 'up_arrow' : 'down_arrow';
}
// Get the sort priority number for display (1, 2, 3, etc.)
const getSortPriority = (key) => {
const ruleIndex = sortingRules.findIndex(rule => rule.key === key);
return ruleIndex === -1 ? null : ruleIndex + 1;
}
// Multi-column sort: clicking a column adds it to sort rules or toggles its order
// Behavior: asc → desc → remove from sorting
const sortTableWithField = (key) => {
const existingIndex = sortingRules.findIndex(rule => rule.key === key);
if (existingIndex === -1) {
// Column not in sort rules - add it with 'asc' order
setSortingRules([...sortingRules, { key, order: 'asc' }]);
} else {
const existingRule = sortingRules[existingIndex];
if (existingRule.order === 'asc') {
// Toggle to desc
const newRules = [...sortingRules];
newRules[existingIndex] = { key, order: 'desc' };
setSortingRules(newRules);
} else {
// Already desc - remove from sorting rules
const newRules = sortingRules.filter((_, index) => index !== existingIndex);
setSortingRules(newRules);
}
}
}
const toggleSelectedAllItems = () => {
if (selectedItems.length !== events.length || selectedItems.length === 0) {
const newSelectedItems = [...events].map((event) => event.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 checkSelectAll = () => {
return selectedItems.length === events.length && selectedItems.length > 0;
}
const disableAssignTransportationButton = () => {
return (!transportSelected || transportSelected === '') || ((transportSelected === 'create_new') && (!driver || !transportStartTime || driver === '' || transportStartTime === ''));
}
const closePanel = () => {
setShowTransportationModal(false);
setTransportStartTime(null);
setDriver(null);
setTransportSelected(null);
}
const assignDriver = () => {
// if select create new event, then create a new transportation event first
if (transportSelected === 'create_new') {
const dateString = new Date().toLocaleDateString();
const startDateTime = new Date(`${dateString} ${transportStartTime}`);
const transportationParameter = {
title: `${driverOptions.find((item) => item.id === driver)?.name} ${startDateTime.toLocaleTimeString()}`,
description: 'transportatoin for med events',
type: 'transportation',
source_type: 'resource',
source_uuid: '',
target_type: 'staff',
target_uuid: driver,
start_time: startDateTime,
stop_time: startDateTime,
status: 'active',
create_by: JSON.parse(localStorage.getItem('user'))?.name,
create_date: new Date(),
edit_by: JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date(),
edit_history: [{
employee: JSON.parse(localStorage.getItem('user'))?.name,
date: new Date()
}]
};
EventsService.createNewEvent(transportationParameter).then(data => {
const trans = data.data;
EventsService.assignTransportationToEvents({
transportationId: trans.id,
transportationName: trans.title,
eventIds: selectedItems
}).then(() => {
setSelectedItems([]);
EventsService.getAllEvents({ date: EventsService.formatDate(selectedDate) }).then((data) => {
const results = [...data.data];
const eventsResults = results.filter((item) => {
item.customer = item?.data?.customer ? (customers.find(c=>c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || '');
item.doctor = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || '');
item.phone = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || '');
item.address = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.address || item?.data?.resource_address || '') : (item?.data?.resource_address || '');
item.translation = item?.data?.interpreter || '';
item.newPatient = item?.data?.new_patient || '';
item.needId = item?.data?.need_id || '';
item.disability = item?.data?.disability || '';
item.member_type = item?.data?.customer ? (customers.find(c => c.id === item?.data?.customer)?.type || '') : '';
item.eyes_on = checkDisability(customers, item) ? 'Yes' : 'No';
item.startTime = item?.start_time? `${new Date(item?.start_time).toLocaleDateString()} ${new Date(item?.start_time).toLocaleTimeString()}` : '' ;
item.fasting = item?.data?.fasting || '';
item.chinese_name = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.name_cn : (customers?.find(c=> c?.name === item?.data?.client_name || c?.name === item?.target_name )?.name_cn || '');
item.transportation = item?.link_event_name || '';
item.dob = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.birth_date : (item?.data?.client_birth_date || '');
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed);
// Apply current sorting rules (or default if none)
const sortedEvents = applyMultiColumnSort(eventsResults, sortingRules);
setEvents(sortedEvents);
setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'));
closePanel();
})
});
}).catch((err) => console.log('Transportation Event Creation failed'))
} else {
if (transportSelected && transportSelected !== '') {
EventsService.assignTransportationToEvents({
transportationId: transportSelected,
transportationName: transportOptionsList.find((item) => item.id === transportSelected)?.title,
eventIds: selectedItems
}).then(() => {
setSelectedItems([]);
EventsService.getAllEvents({ date: EventsService.formatDate(selectedDate) }).then((data) => {
const results = [...data.data];
const eventsResults = results.filter((item) => {
item.customer = item?.data?.customer ? (customers.find(c=>c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || '');
item.doctor = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || '');
item.phone = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || '');
item.address = item?.data?.resource ? (resources.find(r=> r.id === item?.data?.resource)?.address || item?.data?.resource_address || '') : (item?.data?.resource_address || '');
item.translation = item?.data?.interpreter || '';
item.newPatient = item?.data?.new_patient || '';
item.needId = item?.data?.need_id || '';
item.disability = item?.data?.disability || '';
item.member_type = item?.data?.customer ? (customers.find(c => c.id === item?.data?.customer)?.type || '') : '';
item.eyes_on = checkDisability(customers, item) ? 'Yes' : 'No';
item.startTime = item?.start_time? `${EventsService.formatDate(new Date(item?.start_time))} ${new Date(item?.start_time).toLocaleTimeString('en-US', { hour: '2-digit', minute: 'numeric', hour12: true })}` : '' ;
item.fasting = item?.data?.fasting || '';
item.chinese_name = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.name_cn : (customers?.find(c=> c?.name === item?.data?.client_name || c?.name === item?.target_name )?.name_cn || '');
item.transportation = item?.link_event_name || '';
item.dob = item?.data?.customer ? customers.find(c => c.id === item?.data?.customer)?.birth_date : (item?.data?.client_birth_date || '');
item.transMethod = item?.data?.trans_method;
return item;
}).filter(item => item.type === 'medical' && item.confirmed);
// Apply current sorting rules (or default if none)
const sortedEvents = applyMultiColumnSort(eventsResults, sortingRules);
setEvents(sortedEvents);
// setTransportationOptionsList(data.data.filter((item) => item.type === 'transportation' && item.status === 'active'));
closePanel();
})
});
}
};
};
const table = (statusParam) => <div className="list row mb-4">
<div className="col-md-12">
<table className="personnel-info-table">
<thead>
<tr>
<th className="th-checkbox"><input type="checkbox" checked={checkSelectAll()} onClick={() => toggleSelectedAllItems()}></input></th>
<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)} style={{ cursor: 'pointer' }}>
{getSortPriority(column.key) && <small style={{ marginRight: '2px', fontSize: '10px', color: '#666' }}>{getSortPriority(column.key)}</small>}
<img src={`/images/${getSortingImg(column.key)}.png`}></img>
</span>
</th>)
}
</tr>
</thead>
<tbody>
{
events && events.filter(event => event.status === statusParam).map((medicalEvent, index) => <tr key={medicalEvent.id}>
<td className="td-checkbox"><input type="checkbox" checked={selectedItems.includes(medicalEvent.id)} onClick={()=>toggleItem(medicalEvent?.id)}/></td>
<td className="td-index">{index + 1}</td>
{columns.find(col => col.key === 'customer')?.show && <td>{`${medicalEvent?.customer || ''}${medicalEvent?.chinese_name ? ` (${medicalEvent.chinese_name})` : ''}`}</td>}
{columns.find(col => col.key === 'member_type')?.show && <td>{medicalEvent?.member_type}</td>}
{columns.find(col => col.key === 'translation')?.show && <td>{medicalEvent?.translation}</td>}
{columns.find(col => col.key === 'transMethod')?.show && <td>{medicalEvent?.transMethod}</td>}
{columns.find(col => col.key === 'transportation')?.show && <td>{medicalEvent?.transportation}</td>}
{columns.find(col => col.key === 'startTime')?.show && <td>{medicalEvent?.startTime}</td>}
{columns.find(col => col.key === 'eyes_on')?.show && <td>{medicalEvent?.eyes_on}</td>}
{columns.find(col => col.key === 'newPatient')?.show && <td>{medicalEvent?.newPatient}</td>}
{columns.find(col => col.key === 'fasting')?.show && <td>{medicalEvent?.fasting}</td>}
{columns.find(col => col.key === 'needId')?.show && <td>{medicalEvent?.needId}</td>}
{columns.find(col => col.key === 'doctor')?.show && <td>{medicalEvent?.doctor}</td>}
{columns.find(col => col.key === 'phone')?.show && <td>{medicalEvent?.phone}</td>}
{columns.find(col => col.key === 'address')?.show && <td>{medicalEvent?.address}</td>}
</tr>)
}
</tbody>
</table>
</div>
</div>;
return (
<>
<div className="list row mb-4">
<Breadcrumb>
<Breadcrumb.Item href="/medical/index">Medical</Breadcrumb.Item>
<Breadcrumb.Item active>
Appointment Information
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>
Medical Event One-Day List (With Driver Assignment)
{/* <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="activeEvents" id="requests-tab" onSelect={(k) => showDeleted(k)}>
<Tab eventKey="activeEvents" title="Active Appointments">
<div className="app-main-content-fields-section with-function">
<Button className="me-2" variant="outline-primary" size="sm" onClick={() => goToPreviousDay()} > {'<'} </Button>
<DatePicker className="me-2" selected={selectedDate} onChange={(v) => setSelectedDate(v)} />
<Button className="me-2 ms-2" variant="outline-primary" size="sm" onClick={() => goToNextDay()}> {'>'} </Button>
{AuthService.canEditMedicalEvents() && <Button className="me-2" variant="primary" size="sm" disabled={selectedItems.length === 0} onClick={() => { setTransportSelected('create_new'); setShowTransportationModal(true); }}> + Show Assign Transportation Panel</Button>}
</div>
{table('active')}
</Tab>
<Tab eventKey="archivedEvents" title="Archived Appointments">
<div className="app-main-content-fields-section with-function">
<Button className="me-2" variant="outline-primary" size="sm" onClick={() => goToPreviousDay()} > {'<'} </Button>
<DatePicker className="me-2" selected={selectedDate} onChange={(v) => setSelectedDate(v)} />
<Button className="me-2 ms-2" variant="outline-primary" size="sm" onClick={() => goToNextDay()}> {'>'} </Button>
</div>
{table('inactive')}
</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)} /> */}
{/* <button className="btn btn-primary me-2"><Filter size={16} className="me-2"></Filter>Filter</button> */}
<ManageTable columns={columns} onColumnsChange={setColumns} />
{AuthService.canEditMedicalEvents() && <button className="btn btn-primary me-2" onClick={() => goToCreateNew()}><Plus size={16}></Plus>Add New Medical Appointment</button>}
<Export
columns={columns}
data={events.filter(event => event.status === (showDeletedItems ? 'inactive' : 'active'))}
filename="events"
/>
</div>
</div>
</div>
<Modal show={showTransportationModal} onHide={() => closePanel()}>
<Modal.Header closeButton>
<Modal.Title>Assign Transportations Panel</Modal.Title>
</Modal.Header>
<Modal.Body>
<>
{transportSelected === 'create_new' && (
<>
<hr/>
<div>
Start Time:
<div>
<TimePicker disableClock={true} value={transportStartTime} onChange={setTransportStartTime} />
</div>
</div>
</>)
}
{transportSelected === 'create_new' && (
<>
<hr/>
<div>
Driver:
<div>
<select value={driver} onChange={(e)=>{setDriver(e.currentTarget.value)}}>
<option value=""></option>
{
driverOptions.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)
}
</select>
</div>
</div>
</>)
}
</>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => closePanel()}>
Close
</Button>
<Button disabled={disableAssignTransportationButton()} variant="primary" onClick={() => assignDriver()}>
Assign Transportation
</Button>
</Modal.Footer>
</Modal>
</>
)
};
export default EventsList;