111 lines
4.5 KiB
JavaScript
111 lines
4.5 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Button, Card, CardBody, Col, Container, Row, Table } from 'react-bootstrap';
|
|
import PageBreadcrumb from '@/components/PageBreadcrumb';
|
|
import { api } from '@/lib/api';
|
|
|
|
export default function ReportsPage() {
|
|
const [patients, setPatients] = useState([]);
|
|
const [utilization, setUtilization] = useState([]);
|
|
const [productivity, setProductivity] = useState({ sessionsRun: 0, attendance: 0, possibleAttendance: 0, reassignments: [] });
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
api('/api/reports/patients'),
|
|
api('/api/reports/utilization'),
|
|
api('/api/reports/productivity'),
|
|
]).then(([a, b, c]) => {
|
|
setPatients(a.items || []);
|
|
setUtilization(b.items || []);
|
|
setProductivity(c);
|
|
}).catch((err) => setError(err.message));
|
|
}, []);
|
|
|
|
const download = (type) => {
|
|
fetch(`/api/reports/export?type=${type}`, { credentials: 'include' })
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `${type}-report.csv`;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
})
|
|
.catch((err) => setError(err.message));
|
|
};
|
|
|
|
return (
|
|
<Container fluid>
|
|
<PageBreadcrumb title="Reports" subtitle="Program" />
|
|
{error ? <div className="alert alert-danger">{error}</div> : null}
|
|
<div className="d-flex gap-2 mb-3">
|
|
<Button variant="outline-primary" onClick={() => download('patients')}>Export patients CSV</Button>
|
|
<Button variant="outline-primary" onClick={() => download('utilization')}>Export utilization CSV</Button>
|
|
<Button variant="outline-primary" onClick={() => download('vitals')}>Export vitals CSV</Button>
|
|
</div>
|
|
<Row className="g-3">
|
|
<Col lg={7}>
|
|
<Card>
|
|
<CardBody>
|
|
<h5 className="mb-3">Patient progress</h5>
|
|
<Table responsive size="sm" className="mb-0">
|
|
<thead><tr><th>Patient</th><th>Cohort</th><th>PRT</th><th>First session</th><th>Completed</th><th>Assigned</th><th>Attendance</th></tr></thead>
|
|
<tbody>
|
|
{patients.map((item) => (
|
|
<tr key={item.id}>
|
|
<td>{item.name}<div className="small text-muted">{item.diseaseType}</div></td>
|
|
<td>{item.cohort?.name || '—'}</td>
|
|
<td>{item.prt ? `${item.prt.firstName} ${item.prt.lastName}` : '—'}</td>
|
|
<td>{item.firstSession || '—'}</td>
|
|
<td>{item.completed}</td>
|
|
<td>{item.assigned}</td>
|
|
<td>{item.completion}</td>
|
|
</tr>
|
|
))}
|
|
{!patients.length ? <tr><td colSpan={7} className="text-muted">No confirmed patients.</td></tr> : null}
|
|
</tbody>
|
|
</Table>
|
|
</CardBody>
|
|
</Card>
|
|
</Col>
|
|
<Col lg={5}>
|
|
<Card className="mb-3">
|
|
<CardBody>
|
|
<h5 className="mb-3">Utilization</h5>
|
|
<Table responsive size="sm" className="mb-0">
|
|
<thead><tr><th>PRT</th><th>Cohorts</th><th>Idle</th><th>Cancelled</th></tr></thead>
|
|
<tbody>
|
|
{utilization.map((item) => (
|
|
<tr key={item.id}>
|
|
<td>{item.name}</td>
|
|
<td>{item.used}/{item.max}</td>
|
|
<td>{item.idle}</td>
|
|
<td>{item.cancelled}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</Table>
|
|
</CardBody>
|
|
</Card>
|
|
<Card>
|
|
<CardBody>
|
|
<h5 className="mb-2">Productivity</h5>
|
|
<p className="mb-1">Sessions run: {productivity.sessionsRun}</p>
|
|
<p className="mb-3">Attendance: {productivity.attendance}/{productivity.possibleAttendance}</p>
|
|
<h6>Recent reassignments</h6>
|
|
{(productivity.reassignments || []).length ? (
|
|
<ul className="mb-0">
|
|
{productivity.reassignments.map((item) => (
|
|
<li key={item.id}>{item.date}: {item.cohort} {item.from} → {item.to || 'open'} ({item.reason})</li>
|
|
))}
|
|
</ul>
|
|
) : <p className="text-muted mb-0">None yet.</p>}
|
|
</CardBody>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</Container>
|
|
);
|
|
}
|