All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 31s
344 lines
16 KiB
JavaScript
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;
|