411 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|