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

411 lines
16 KiB
TypeScript

import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/DoctorTreatment";
import {useEffect, useMemo, useState} from "react";
import {BookingCategory, UserPermissionLevel} from "~/utils/hms_enums.ts";
import {useNavigate, useOutletContext} from "react-router";
import {
type DoctorTeamInfo,
type OutletContextType,
type PatientBookingInfoWithAdmission,
SORT_SYMBOLS,
WardTypes
} from "~/utils/models.ts";
import {Accordion, ActionIcon, Badge, Button, Card, Group, Modal, Select, Stack, Table, Text, Textarea} from "@mantine/core";
import {apiDoctorTreat, apiGetDoctorAppointments, apiGetTeamList, apiGetWardList} from "~/utils/hms_api.ts";
import {showErrorMessage, showInfoMessage} from "~/utils/utils.ts";
import {iconMStyle, marginLeftRight, marginTopBottom} from "~/styles.ts";
import {confirmCheckWardPatients} from "~/components/subs/confirms.tsx";
import {TruncatedText} from "~/components/subs/TruncatedText.tsx";
import InfoIcon from "mdi-react/InfoIcon";
import NoteEditIcon from "mdi-react/NoteEditIcon";
import {useForm} from "@mantine/form";
import {PatientInfoDisplay} from "~/components/subs/PatientInfoDisplay.tsx";
import FilterIcon from "mdi-react/FilterIcon";
import {ResponsiveTableContainer} from "~/components/subs/ResponsiveTableContainer.tsx";
import {TeamInfoDisplay} from "~/components/subs/TeamInfoDisplay.tsx";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Doctor Treatment" },
{ name: "description", content: "Record treatment information for patients" },
];
}
function TreatmentModal({
opened,
onClose,
appointmentId,
patientName,
patientId,
onSuccess
}: {
opened: boolean;
onClose: () => void;
appointmentId: number;
patientName: string;
patientId: number;
onSuccess: () => void;
}) {
const [loading, setLoading] = useState(false);
const form = useForm({
initialValues: {
treatment_info: ''
},
validate: {
treatment_info: (value) => (!value ? 'Treatment information is required' : null)
}
});
const handleSubmit = (values: typeof form.values) => {
setLoading(true);
// Current timestamp in seconds
const currentTime = Math.floor(Date.now() / 1000);
apiDoctorTreat(
appointmentId,
values.treatment_info,
currentTime
).then(res => {
if (res.success) {
showInfoMessage("Treatment record saved successfully", "Success");
onSuccess();
onClose();
} else {
showErrorMessage(res.message, "Failed to save treatment record");
}
})
.catch(err => {
showErrorMessage("Failed to save treatment record", "Error");
})
.finally(() => {
setLoading(false);
});
};
return (
<Modal
opened={opened}
onClose={onClose}
title="Record Treatment"
size="lg"
centered
>
<Stack gap="md">
<Group>
<Text fw={500}>Patient:</Text>
<PatientInfoDisplay patientId={patientId} showIcon={false} />
</Group>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Textarea
label="Treatment Information"
description="Please provide detailed information about the treatment provided"
placeholder="Enter treatment details, medications, observations, etc."
required
minRows={5}
maxRows={10}
{...form.getInputProps('treatment_info')}
/>
<Group justify="flex-end" mt="md">
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button type="submit" loading={loading}>Save Treatment Record</Button>
</Group>
</Stack>
</form>
</Stack>
</Modal>
);
}
export default function Component() {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [appointments, setAppointments] = useState<PatientBookingInfoWithAdmission[]>([]);
const [wardMap, setWardMap] = useState<Map<number, string>>(new Map());
// Treatment modal state
const [treatmentModalOpen, setTreatmentModalOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<PatientBookingInfoWithAdmission | null>(null);
const [sortKey, setSortKey] = useState<string>("appointment_time");
const [sortDesc, setSortDesc] = useState<boolean>(true);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [teamList, setTeamList] = useState<DoctorTeamInfo[]>([]);
const { loginUserInfo } = useOutletContext<OutletContextType>();
useEffect(() => {
fetchAppointments();
fetchWards();
refreshTeamList()
}, []);
const refreshTeamList = () => {
apiGetTeamList(-1).then(res => {
if (res.success) {
setTeamList(res.data.teams)
}
else {
showErrorMessage(res.message, "Failed to fetch teams");
}
}).catch(err => showErrorMessage(err.toString(), "Failed to fetch teams"));
}
const getMemberNameFromTeam = (team: DoctorTeamInfo, doctorId: number) => {
const member = team.members.find(member => member.id === doctorId);
return member ? `${member.title} ${member.name}`.trim() : `Doctor ID: ${doctorId}`;
}
const fetchAppointments = (target_team_id?: number) => {
setLoading(true);
apiGetDoctorAppointments(target_team_id)
.then(res => {
if (res.success) {
setAppointments(res.data.appointments);
} else {
showErrorMessage(res.message, "Failed to fetch appointments");
}
})
.catch(err => {
showErrorMessage("Failed to fetch appointments", "Error");
})
.finally(() => {
setLoading(false);
});
};
const fetchWards = () => {
apiGetWardList(1, true)
.then(res => {
if (res.success) {
const wardNameMap = new Map<number, string>();
res.data.wards.forEach(ward => {
wardNameMap.set(ward.id, `${ward.name} (${WardTypes[ward.type]})`);
});
setWardMap(wardNameMap);
}
})
.catch(err => {
showErrorMessage("Failed to fetch wards information", "Error");
});
};
const handleSort = (key: string) => {
if (sortKey === key) {
setSortDesc(!sortDesc);
} else {
setSortKey(key);
setSortDesc(false); // Default ascending
}
};
const openTreatmentModal = (appointment: PatientBookingInfoWithAdmission) => {
setSelectedAppointment(appointment);
setTreatmentModalOpen(true);
};
const closeTreatmentModal = () => {
setTreatmentModalOpen(false);
setSelectedAppointment(null);
};
const handleViewWard = (wardId: number) => {
confirmCheckWardPatients(wardId, fetchAppointments);
};
const getCategoryDisplay = (category: string) => {
const key = Object.entries(BookingCategory).find(([_, value]) => value === category)?.[0];
return key ? key.replace(/_/g, ' ') : category;
};
const getStatusBadge = (appointment: PatientBookingInfoWithAdmission) => {
if (appointment.discharged) {
return <Badge color="purple">Discharged</Badge>;
} else if (appointment.admitted) {
return <Badge color="blue">Admitted</Badge>;
} else {
return <Badge color="yellow">Pending</Badge>;
}
};
const sortedAppointments = useMemo(() => {
let data = [...appointments];
// Apply sorting
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;
}, [appointments, sortKey, sortDesc]);
const rows = sortedAppointments.map((appointment) => (
<Table.Tr key={appointment.id}>
<Table.Td>{appointment.id}</Table.Td>
<Table.Td>
<PatientInfoDisplay patientId={appointment.patient_id} />
</Table.Td>
<Table.Td>{getCategoryDisplay(appointment.category)}</Table.Td>
<Table.Td>{new Date(appointment.appointment_time * 1000).toLocaleString('en-GB').replace(',', '')}</Table.Td>
<Table.Td>
<TruncatedText
text={appointment.description}
title={`Description - Appointment #${appointment.id}`}
/>
</Table.Td>
<Table.Td>{getStatusBadge(appointment)}</Table.Td>
<Table.Td>
<Group gap="xs">
<Text>{wardMap.get(appointment.admission.ward_id) || `Ward ID: ${appointment.admission.ward_id}`}</Text>
<ActionIcon
size="sm"
onClick={() => handleViewWard(appointment.admission.ward_id)}
title="View ward details"
>
<InfoIcon style={iconMStyle} />
</ActionIcon>
</Group>
</Table.Td>
<Table.Td>
<TeamInfoDisplay teamId={appointment.assigned_team || -1} />
</Table.Td>
<Table.Td>
<Group>
<ActionIcon
color="blue"
onClick={() => openTreatmentModal(appointment)}
title="Record treatment"
>
<NoteEditIcon style={iconMStyle}/>
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
));
const handleTeamChange = (value: string | null) => {
setSelectedTeamId(value);
const teamId = value === null ? undefined : parseInt(value);
fetchAppointments(teamId);
};
return (
<Stack>
<Group justify="space-between" align="center" style={marginLeftRight}>
<Text size="1.5em" fw={700}>Patient Treatment</Text>
<Button
onClick={() => fetchAppointments(selectedTeamId ? parseInt(selectedTeamId) : undefined)}
loading={loading}
variant="outline"
>Refresh</Button>
</Group>
<Card padding="lg" radius="md" withBorder style={marginTopBottom}>
{ loginUserInfo && loginUserInfo.user_permission >= UserPermissionLevel.ADMIN &&
<Card.Section withBorder>
<Accordion variant="filled" chevronPosition="left" defaultValue="advanced-filter">
<Accordion.Item value="advanced-filter">
<Accordion.Control>
<Group>
<Text>Select a Team</Text>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Group grow>
<Select
label="Select Team"
placeholder="Select a medical team"
data={[
{ value: "-1", label: "All Teams" },
...(teamList.map(team => ({
value: team.id.toString(),
label: `Team ${team.id}: ${team.department.replace(/_/g, ' ')} (${getMemberNameFromTeam(team, team.consultant_id)})`
})) || [])
]}
value={selectedTeamId}
onChange={handleTeamChange}
searchable
clearable
/>
</Group>
</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")}>
ID{" "}
{sortKey === "id" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("patient_id")}>
Patient{" "}
{sortKey === "patient_id" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("category")}>
Category{" "}
{sortKey === "category" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th onClick={() => handleSort("appointment_time")}>
Appointment Time{" "}
{sortKey === "appointment_time" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th>
Description
</Table.Th>
<Table.Th onClick={() => handleSort("admitted")}>
Status{" "}
{sortKey === "admitted" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th>
Ward
</Table.Th>
<Table.Th>
Medical Team
</Table.Th>
<Table.Th>
Actions
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
</Card.Section>
</Card>
{selectedAppointment && (
<TreatmentModal
opened={treatmentModalOpen}
onClose={closeTreatmentModal}
appointmentId={selectedAppointment.id}
patientName={`Patient ID: ${selectedAppointment.patient_id}`}
patientId={selectedAppointment.patient_id}
onSuccess={fetchAppointments}
/>
)}
</Stack>
);
}