Files
worldshine-redesign/client/src/components/dashboard/Dashboard.js
Lixian Zhou adfad31741
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 31s
fix
2026-03-11 11:24:08 -04:00

344 lines
16 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Breadcrumb, BreadcrumbItem, Card, Row, Col, Dropdown, Spinner } from 'react-bootstrap';
import { AuthService, EventsService, CustomerService, TransRoutesService, ResourceService } from '../../services';
import DashboardCustomersList from './DashboardCustomersList';
import { CUSTOMER_TYPE, PERSONAL_ROUTE_STATUS } from '../../shared';
import moment from 'moment';
import './Dashboard.css';
const Dashboard = () => {
const [todayAttendance, setTodayAttendance] = useState(0);
const [todayMedicalAppointments, setTodayMedicalAppointments] = useState(0);
const [membersCount, setMembersCount] = useState(0);
const [visitorsCount, setVisitorsCount] = useState(0);
const [medicalPercentage, setMedicalPercentage] = useState(0);
const [medicalTrend, setMedicalTrend] = useState(''); // 'increase' or 'decrease'
const [events, setEvents] = useState([]);
const [allEvents, setAllEvents] = useState([]);
const [groupedEvents, setGroupedEvents] = useState(new Map());
const [selectedEventType, setSelectedEventType] = useState('medical');
const [customers, setCustomers] = useState([]);
const [resources, setResources] = useState([]);
const [showSpinner, setShowSpinner] = useState(false);
const eventTypes = [
{ value: 'medical', label: 'Medical Appointments' },
{ value: 'activity', label: 'Activities' },
{ value: 'incident', label: 'Attendance' },
{ value: 'meal_plan', label: 'Meal Plan' },
{ value: 'reminder', label: 'Important Dates' }
];
// Fetch today's events for the calendar list
const fetchTodayEvents = async () => {
try {
const today = new Date();
const fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
const toDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const eventsData = await EventsService.getAllEvents({
from: EventsService.formatDate(fromDate),
to: EventsService.formatDate(toDate)
});
setAllEvents(eventsData.data);
let processedEvents = [];
// Filter and map events based on selected type
if (selectedEventType === 'medical') {
if (customers?.length > 0 && resources.length > 0) {
const originalEvents = [...eventsData.data];
processedEvents = originalEvents
?.filter(item => item.type === 'medical')
?.map((item) => {
const customerField = item?.data?.customer ? (customers?.find(c => c.id === item?.data?.customer)?.name || item?.data?.client_name || '') : (item?.data?.client_name || '');
const doctorField = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.name || item?.data?.resource_name || '') : (item?.data?.resource_name || '');
item.event_id = item.id;
item.customer = customerField;
item.doctor = doctorField;
item.phone = item?.data?.resource ? ((resources?.find(r => r.id === item?.data?.resource))?.phone || item?.data?.resource_phone || '') : (item?.data?.resource_phone || '');
item.contact = item?.data?.resource? ((resources?.find(r => r.id === item?.data?.resource))?.contact || item?.data?.resource_contact || '') : (item?.data?.resource_contact || '');
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.startTime = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : '';
item.endTime = item?.start_time? `${moment(new Date(item?.end_time)).format('YYYY-MM-DD HH:mm')}` : '';
item.fasting = item?.data?.fasting || '';
item.transportation = item?.link_event_name || '';
item.title = `${customerField}, provider: ${doctorField}`;
item.start = item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`;
item.end = item?.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`);
item.color = item?.color;
item._options = { additionalClasses: [`event-${item?.color || 'primary'}`]};
return item;
})
?.filter(item => item.status === 'active');
}
} else {
const originalEvents = [...eventsData.data];
processedEvents = originalEvents
?.filter(item => item.type === selectedEventType)
?.map(item => ({
...item,
title: item?.title,
start: item?.start_time? `${moment(new Date(item?.start_time)).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`,
end: item?.stop_time? `${moment(new Date(item?.stop_time)).format('YYYY-MM-DD HH:mm')}` : (item?.start_time? `${moment(item?.start_time).format('YYYY-MM-DD HH:mm')}` : `${moment().format('YYYY-MM-DD HH:mm')}`),
_options: { additionalClasses: [`event-${item?.color || 'primary'}`]}
}))
?.filter(item => item.status === 'active');
}
const sortedEvents = [...(processedEvents || [])].sort((a, b) => {
const aTime = moment(a?.start_time).valueOf();
const bTime = moment(b?.start_time).valueOf();
return aTime - bTime;
});
setEvents(sortedEvents);
// Group events by date
const eventsDateMap = new Map();
sortedEvents?.forEach(eventItem => {
const dateString = moment(eventItem.start_time).format('MMM Do, YYYY');
if (eventsDateMap.has(dateString)) {
eventsDateMap.set(dateString, [...eventsDateMap.get(dateString), eventItem]);
} else {
eventsDateMap.set(dateString, [eventItem]);
}
});
eventsDateMap.forEach((items, key) => {
const sortedItems = [...items].sort((a, b) => {
const aTime = moment(a?.start_time).valueOf();
const bTime = moment(b?.start_time).valueOf();
return aTime - bTime;
});
eventsDateMap.set(key, sortedItems);
});
setGroupedEvents(eventsDateMap);
} catch (error) {
console.error('Error fetching events:', error);
}
};
// Fetch today's attendance data
const fetchTodayAttendance = async () => {
try {
const today = EventsService.formatDate(new Date());
const routesData = await TransRoutesService.getAll(today);
const inboundRoutes = routesData.data.filter(route => route.type === 'inbound');
let totalAttendance = 0;
let members = 0;
let visitors = 0;
// Get all customers from inbound routes who are not absent
const allCustomers = TransRoutesService.getAllCustomersFromRoutes(inboundRoutes, []);
allCustomers.forEach(customer => {
// Check if customer is not Unexpected Absent or Scheduled Absent
if (customer.customer_route_status !== PERSONAL_ROUTE_STATUS.UNEXPECTED_ABSENT &&
customer.customer_route_status !== PERSONAL_ROUTE_STATUS.SCHEDULED_ABSENT) {
totalAttendance++;
// Count by customer type
if (customer.customer_type === CUSTOMER_TYPE.MEMBER || customer.customer_type === CUSTOMER_TYPE.SELF_PAY) {
members++;
} else if (customer.customer_type === CUSTOMER_TYPE.VISITOR) {
visitors++;
}
}
});
setTodayAttendance(totalAttendance);
setMembersCount(members);
setVisitorsCount(visitors);
} catch (error) {
console.error('Error fetching attendance:', error);
}
};
// Fetch today's and yesterday's medical appointments
const fetchMedicalAppointments = async () => {
try {
const today = EventsService.formatDate(new Date());
const yesterday = EventsService.formatDate(new Date(Date.now() - 24 * 60 * 60 * 1000));
// Get today's medical events
const todayEventsData = await EventsService.getAllEvents({ date: today });
const todayMedicalEvents = todayEventsData.data.filter(event => event.type === 'medical');
// Get yesterday's medical events
const yesterdayEventsData = await EventsService.getAllEvents({ date: yesterday });
const yesterdayMedicalEvents = yesterdayEventsData.data.filter(event => event.type === 'medical');
setTodayMedicalAppointments(todayMedicalEvents.length);
// Calculate percentage change
if (yesterdayMedicalEvents.length > 0) {
const percentageChange = ((todayMedicalEvents.length - yesterdayMedicalEvents.length) / yesterdayMedicalEvents.length) * 100;
setMedicalPercentage(Math.abs(percentageChange));
setMedicalTrend(percentageChange >= 0 ? 'increase' : 'decrease');
} else if (todayMedicalEvents.length > 0) {
setMedicalPercentage(100);
setMedicalTrend('increase');
} else {
setMedicalPercentage(0);
setMedicalTrend('');
}
} catch (error) {
console.error('Error fetching medical appointments:', error);
}
};
useEffect(() => {
setShowSpinner(true);
Promise.all([
fetchTodayAttendance(),
fetchMedicalAppointments(),
CustomerService.getAllCustomers().then((data) => {
setCustomers(data.data);
}),
ResourceService.getAll().then((data) => {
setResources(data.data);
})
]).finally(() => {
setShowSpinner(false);
});
}, []);
// Separate useEffect for events that depends on selectedEventType, customers, and resources
useEffect(() => {
if (customers?.length > 0 && resources?.length > 0) {
fetchTodayEvents();
}
}, [selectedEventType, customers, resources]);
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>Dashboard</Breadcrumb.Item>
<Breadcrumb.Item active>
Dashboard
</Breadcrumb.Item>
</Breadcrumb>
<div className="col-md-12 text-primary">
<h4>Dashboard</h4>
</div>
</div>
<div className="app-main-content-list-container">
<div className="row">
{/* Main Section - 3/4 width */}
<div className="col-md-9">
{/* Top Cards */}
<div className="row mb-4 dashboard-top-cards">
<div className="col-md-6">
<Card className="h-100 dashboard-card">
<Card.Body>
<Card.Title className="dashboard-card-title">Today's Attendance</Card.Title>
<Card.Text className="h2 text-primary">
{todayAttendance}
</Card.Text>
<Card.Text className="text-muted">
{membersCount} Members • {visitorsCount} Visitors
</Card.Text>
</Card.Body>
</Card>
</div>
<div className="col-md-6">
<Card className="h-100 dashboard-card">
<Card.Body>
<Card.Title className="dashboard-card-title">Today's Medical Appointments</Card.Title>
<Card.Text className="h2 text-primary">
{todayMedicalAppointments}
</Card.Text>
<Card.Text className="text-muted">
{medicalTrend && (
<span className={medicalTrend === 'increase' ? 'text-success' : 'text-danger'}>
{medicalTrend === 'increase' ? '↗' : '↘'} {medicalPercentage.toFixed(1)}% {medicalTrend === 'increase' ? 'increase' : 'decrease'} from yesterday
</span>
)}
{!medicalTrend && 'No change from yesterday'}
</Card.Text>
</Card.Body>
</Card>
</div>
</div>
{/* Customer List Section */}
{AuthService.canViewCustomers() && (
<div className="row">
<div className="col-12">
<DashboardCustomersList />
</div>
</div>
)}
</div>
{/* Right Side Bar - 1/4 width */}
<div className="col-md-3 dashboard-right-sidebar">
<div className="column-container dashboard-column-container">
<div className="column-card">
<div className="d-flex justify-content-between align-items-center mb-3">
<h6 className="text-primary mb-0">List</h6>
<select
value={selectedEventType}
onChange={(e) => setSelectedEventType(e.target.value)}
className="form-select form-select-sm dashboard-event-selector"
>
{eventTypes.map((eventType) => (
<option key={eventType.value} value={eventType.value}>
{eventType.label}
</option>
))}
</select>
</div>
<div>
{Array.from(groupedEvents?.keys())?.map((key) => {
return (
<div key={key}>
<h6 className="text-primary me-2 dashboard-event-date">{key}</h6>
{groupedEvents.get(key).map((eventItem, index) => (
<div key={index} className={`event-${eventItem.color || 'primary'} mb-3 event-list-item-container dashboard-event-item`}>
<div className="event-item-flex">
<div className="sx__month-agenda-event__title dashboard-event-title">
{selectedEventType === 'medical' ? eventItem.customer || eventItem.title : eventItem.title}
</div>
<div className="sx__event-modal__time dashboard-event-time">
{`${moment(eventItem?.start_time).format('hh:mm A')} ${eventItem?.end_time ? `- ${moment(eventItem?.end_time).format('hh:mm A')}` : ''}`}
</div>
</div>
<div className="sx__event-modal__time with-padding dashboard-event-description">
{selectedEventType === 'medical' ? `Provider: ${eventItem?.doctor || 'N/A'}` : eventItem?.description || 'No description'}
</div>
</div>
))}
</div>
);
})}
{groupedEvents.size === 0 && (
<div className="text-muted text-center py-3">
{selectedEventType === 'incident' ? 'No attendance notes available' : `No ${(eventTypes.find(t => t.value === selectedEventType)?.label || selectedEventType).toLowerCase()} available`}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default Dashboard;