HMS_Group5/HMS_Frontend/app/pages/dashboard/PatientsManagement.tsx
2025-04-30 17:28:58 +01:00

433 lines
18 KiB
TypeScript

import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/PatientsManagement";
import {useEffect, useMemo, useState} from "react";
import {DashboardPageType, UserType} from "~/utils/hms_enums.ts";
import {useNavigate, useOutletContext} from "react-router";
import {
type DoctorTeamInfo,
type OutletContextType,
type PatientDataWithWardAndAdmission,
SORT_SYMBOLS,
type WardInfo
} from "~/utils/models.ts";
import {Accordion, ActionIcon, Button, Card, Group, Pagination, Stack, Table, Text, Tooltip} from "@mantine/core";
import {apiGetPatientsList, apiGetTeamList, apiGetWardList} from "~/utils/hms_api.ts";
import {showErrorMessage} from "~/utils/utils.ts";
import {iconMStyle, marginLeftRight, marginRightBottom, marginTopBottom} from "~/styles.ts";
import PencilIcon from "mdi-react/PencilIcon";
import LogoutVariantIcon from "mdi-react/LogoutVariantIcon";
import AddIcon from "mdi-react/AddIcon";
import InfoIcon from "mdi-react/InfoIcon";
import {
confirmAdminAddUser,
confirmCheckWardPatients,
confirmEditPatient,
confirmPatientDischarge,
confirmResetPassword
} from "~/components/subs/confirms.tsx";
import {PatientManagementFilter} from "~/components/subs/PatientManagementFilter.tsx";
import {PatientInfoDisplay} from "~/components/subs/PatientInfoDisplay.tsx";
import {TeamInfoDisplay} from "~/components/subs/TeamInfoDisplay.tsx";
import LockResetIcon from "mdi-react/LockResetIcon";
import {modals} from "@mantine/modals";
import HistoryIcon from "mdi-react/HistoryIcon";
import FilterIcon from "mdi-react/FilterIcon";
import {ResponsiveTableContainer} from "~/components/subs/ResponsiveTableContainer.tsx";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Patients Management" },
{ name: "description", content: "Manage hospital patients" },
];
}
export default function Component() {
const navigate = useNavigate();
const [refreshingPatientList, setRefreshingPatientList] = useState<boolean>(false);
const [patientInfo, setPatientInfo] = useState<{patients: PatientDataWithWardAndAdmission[], total_pages: number}>({patients: [], total_pages: 1});
// Lists for filters
const [teams, setTeams] = useState<DoctorTeamInfo[]>([]);
const [wards, setWards] = useState<WardInfo[]>([]);
const [loadingTeams, setLoadingTeams] = useState(false);
const [loadingWards, setLoadingWards] = useState(false);
const [sortKey, setSortKey] = useState<string>("");
const [sortDesc, setSortDesc] = useState<boolean>(true);
const [currPage, setCurrPage] = useState<number>(1);
// Filter states
const [nameSearch, setNameSearch] = useState<string>("");
const [filterGenders, setFilterGenders] = useState<string[]>([]);
const [filterHasTeam, setFilterHasTeam] = useState<number>(-1);
const [filterIsAdmitted, setFilterIsAdmitted] = useState<number>(-1);
const [filterTeams, setFilterTeams] = useState<number[]>([]);
const [filterWards, setFilterWards] = useState<number[]>([]);
const { changePage } = useOutletContext<OutletContextType>();
useEffect(() => {
refreshPatientList();
loadTeamsList();
loadWardsList();
}, []);
useEffect(() => {
refreshPatientList();
}, [currPage]);
const loadTeamsList = () => {
setLoadingTeams(true);
apiGetTeamList(-1) // Get all teams
.then(res => {
if (res.success) {
setTeams(res.data.teams);
} else {
showErrorMessage(res.message, "Failed to load teams");
}
})
.catch(err => {})
.finally(() => setLoadingTeams(false));
};
const loadWardsList = () => {
setLoadingWards(true);
apiGetWardList(-1, true) // Get all wards
.then(res => {
if (res.success) {
setWards(res.data.wards);
} else {
showErrorMessage(res.message, "Failed to load wards");
}
})
.catch(err => {})
.finally(() => setLoadingWards(false));
};
const updateFilter = (
name: string,
genders: string[],
hasTeam: string,
isAdmitted: string,
teams: number[],
wards: number[]
) => {
setNameSearch(name);
setFilterGenders(genders);
setFilterHasTeam(Number(hasTeam));
setFilterIsAdmitted(Number(isAdmitted));
setFilterTeams(teams);
setFilterWards(wards);
};
const refreshPatientList = () => {
setRefreshingPatientList(true);
apiGetPatientsList(currPage).then(res => {
if (!res.success) {
showErrorMessage(res.message, "Failed to get patient list");
return;
}
setPatientInfo(res.data);
})
.catch(err => {})
.finally(() => { setRefreshingPatientList(false); });
};
const handleSort = (key: string) => {
if (sortKey === key) {
setSortDesc(!sortDesc);
} else {
setSortKey(key);
setSortDesc(false); // Default ascending
}
};
const sortedPatients = useMemo<PatientDataWithWardAndAdmission[]>(() => {
let data = [...patientInfo.patients];
// Apply all filters
data = data.filter((p) => {
// Name search
const matchesName = !nameSearch || (
(p.name?.toLowerCase().includes(nameSearch.toLowerCase()) ||
p.title?.toLowerCase().includes(nameSearch.toLowerCase()))
);
// Gender filter
const okGender = filterGenders.length === 0 || filterGenders.includes(p.gender);
// Team assignment status filter
const okTeam = filterHasTeam === -1 ||
(filterHasTeam === 1 ? p.admission?.team_id != null : p.admission?.team_id == null);
// Admission status filter
const okAdmitted = filterIsAdmitted === -1 ||
(filterIsAdmitted === 1 ? p.admission != null : p.admission == null);
// Specific teams filter
const okSpecificTeams = filterTeams.length === 0 ||
(p.admission?.team_id != null && filterTeams.includes(p.admission.team_id));
// Specific wards filter
const okSpecificWards = filterWards.length === 0 ||
(p.ward?.id != null && filterWards.includes(p.ward.id));
return matchesName && okGender && okTeam && okAdmitted && okSpecificTeams && okSpecificWards;
});
if (!sortKey) return data;
data.sort((a, b) => {
const valA = a[sortKey];
const valB = b[sortKey];
if (typeof valA === 'string' && typeof valB === 'string') {
const cmp = valA.localeCompare(valB);
return sortDesc ? -cmp : cmp;
}
if (typeof valA === 'number' && typeof valB === 'number') {
return sortDesc ? valB - valA : valA - valB;
}
return 0;
});
return data;
}, [
patientInfo.patients,
sortKey,
sortDesc,
nameSearch,
filterGenders,
filterHasTeam,
filterIsAdmitted,
filterTeams,
filterWards
]);
// Convert teams and wards to options format for MultiSelect
const teamOptions = useMemo(() => {
return teams.map(team => ({
value: team.id.toString(),
label: team.department.replace(/_/g, ' ')
}));
}, [teams]);
const wardOptions = useMemo(() => {
return wards.map(ward => ({
value: ward.id.toString(),
label: `${ward.name} (${ward.current_occupancy}/${ward.total_capacity})`
}));
}, [wards]);
const handleViewPatientInfo = (patient: PatientDataWithWardAndAdmission) => {
modals.open({
title: "Patient Information",
centered: true,
size: "md",
children: (
<Stack>
<Group>
<Text fw={500} w={100}>Name:</Text>
<Text>{`${patient.title} ${patient.name}`}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Gender:</Text>
<Text>{patient.gender}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Birth Date:</Text>
<Text>{patient.birth_date}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Email:</Text>
<Text>{patient.email}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Phone:</Text>
<Text>{patient.phone}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Address:</Text>
<Text>{patient.address}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Postcode:</Text>
<Text>{patient.postcode}</Text>
</Group>
<Group justify="flex-end" mt="md">
<Button onClick={() => modals.closeAll()}>Close</Button>
</Group>
</Stack>
)
});
};
const handleEditPatient = (patient: PatientDataWithWardAndAdmission) => {
confirmEditPatient(patient, refreshPatientList);
};
const handleViewWard = (wardId: number) => {
confirmCheckWardPatients(wardId, refreshPatientList);
};
const handlePatientDischarge = (admissionId: number, patientName: string) => {
confirmPatientDischarge(admissionId, patientName, refreshPatientList);
};
const rows = sortedPatients.map((patient) => (
<Table.Tr key={patient.id}>
<Table.Td>{patient.id}</Table.Td>
<Table.Td>
<PatientInfoDisplay patientId={patient.id} />
</Table.Td>
<Table.Td>{patient.gender}</Table.Td>
<Table.Td>{patient.birth_date}</Table.Td>
<Table.Td>{patient.email}</Table.Td>
<Table.Td>{patient.phone}</Table.Td>
<Table.Td>
{patient.admission?.team_id ? (
<TeamInfoDisplay teamId={patient.admission.team_id} />
) : (
<Text size="sm" c="dimmed">Not Assigned</Text>
)}
</Table.Td>
<Table.Td>
{patient.ward ? (
<Group gap="xs">
<Text>{patient.ward.name}</Text>
<ActionIcon
size="sm"
onClick={() => handleViewWard(patient.ward!.id)}
title="View ward details"
>
<InfoIcon style={iconMStyle} />
</ActionIcon>
</Group>
) : (
<Text size="sm" c="dimmed">Not Admitted</Text>
)}
</Table.Td>
<Table.Td>
<Group>
<Tooltip label="Treatment Record" withArrow>
<ActionIcon onClick={() => { changePage(DashboardPageType.TreatmentRecord, `/dashboard/treatment_record/${patient.id}`) }}>
<HistoryIcon style={iconMStyle}/>
</ActionIcon>
</Tooltip>
<ActionIcon onClick={() => handleViewPatientInfo(patient)} title="View patient info">
<InfoIcon style={iconMStyle}/>
</ActionIcon>
<ActionIcon onClick={() => handleEditPatient(patient)} title="Edit patient info">
<PencilIcon style={iconMStyle}/>
</ActionIcon>
<Tooltip label="Reset Password" withArrow>
<ActionIcon onClick={() => { confirmResetPassword(patient.id, UserType.PATIENT) }}>
<LockResetIcon style={iconMStyle}/>
</ActionIcon>
</Tooltip>
<Tooltip label={patient.admission ? "Discharge patient" : "Not admitted"}>
<ActionIcon
color="red"
onClick={() => patient.admission && handlePatientDischarge(patient.admission.id, `${patient.title} ${patient.name}`)}
disabled={!patient.admission}
title="Discharge patient"
>
<LogoutVariantIcon style={iconMStyle}/>
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Stack>
<Group justify="space-between" align="center" style={marginLeftRight}>
<Text size="1.5em" fw={700}>Patients Management</Text>
<Button
leftSection={<AddIcon style={iconMStyle}/>}
onClick={() => { confirmAdminAddUser(0, refreshPatientList); }}
>Add Patient</Button>
</Group>
<Card padding="lg" radius="md" withBorder style={marginTopBottom}>
<Card.Section withBorder>
<Accordion variant="filled" chevronPosition="left" defaultValue="advanced-filter">
<Accordion.Item value="advanced-filter">
<Accordion.Control>
<Group>
<FilterIcon style={iconMStyle} />
<Text>Filters</Text>
</Group>
</Accordion.Control>
<Accordion.Panel>
<PatientManagementFilter
onChange={updateFilter}
teamOptions={teamOptions}
wardOptions={wardOptions}
/>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Card.Section>
<Card.Section>
<ResponsiveTableContainer minWidth={600}>
<Table striped highlightOnHover withColumnBorders withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th onClick={() => handleSort("id")}>
Patient ID{" "}
{sortKey === "id" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("name")}>
Name{" "}
{sortKey === "name" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("gender")}>
Gender{" "}
{sortKey === "gender" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("birth_date")}>
Birth Date{" "}
{sortKey === "birth_date" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("email")}>
Email{" "}
{sortKey === "email" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("phone")}>
Phone{" "}
{sortKey === "phone" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th>
Medical Team
</Table.Th>
<Table.Th>
Ward
</Table.Th>
<Table.Th>
Operations
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
<Pagination
withEdges
total={patientInfo.total_pages}
value={currPage}
onChange={setCurrPage}
mt="sm"
style={{justifyItems: "flex-end", ...marginRightBottom}}
/>
</Card.Section>
</Card>
</Stack>
);
}