2025-04-30 17:28:58 +01:00

685 lines
28 KiB
TypeScript

import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/Home";
import {useEffect, useState} from "react";
import {useNavigate, useOutletContext} from "react-router";
import {
type DoctorDataWithPermission,
type DoctorTeamInfo,
type OutletContextType,
type PatientBookingInfo,
type PatientData,
type TreatmentInfoWithDoctorInfo,
type WardInfo
} from "~/utils/models.ts";
import {Badge, Button, Card, Grid, Group, Modal, PasswordInput, Stack, Table, Text} from "@mantine/core";
import {
apiChangeMyPassword,
apiGetPatientBookingList,
apiGetTreatmentRecords,
apiPatientGetCurrentWard
} from "~/utils/hms_api.ts";
import {showErrorMessage, showInfoMessage} from "~/utils/utils.ts";
import {iconMStyle, marginLeftRight, marginRound, marginTop} from "~/styles.ts";
import {useForm} from "@mantine/form";
import {BookingCategory, DashboardPageType, DoctorGrade, UserType} from "~/utils/hms_enums.ts";
import {confirmEditDoctor, confirmEditPatient} from "~/components/subs/confirms.tsx";
import EditIcon from "mdi-react/EditIcon";
import LockResetIcon from "mdi-react/LockResetIcon";
import ArrowRightIcon from "mdi-react/ArrowRightIcon";
import {TruncatedText} from "~/components/subs/TruncatedText.tsx";
import {PatientInfoDisplay} from "~/components/subs/PatientInfoDisplay.tsx";
import {TeamInfoDisplay} from "~/components/subs/TeamInfoDisplay.tsx";
import {DoctorInfoDisplay} from "~/components/subs/DoctorInfoDisplay.tsx";
import DoctorTeamsSimple from "~/components/subs/DoctorTeamsSimple.tsx";
import { ResponsiveTableContainer } from "~/components/subs/ResponsiveTableContainer";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Home" },
{ name: "description", content: "Dashboard Home" },
];
}
interface ChangePasswordModalProps {
opened: boolean;
onClose: () => void;
onSuccess: () => void;
}
function ChangePasswordModal({ opened, onClose, onSuccess }: ChangePasswordModalProps) {
const form = useForm({
initialValues: {
old_password: '',
new_password: '',
confirm_password: '',
},
validate: {
old_password: (value) => (!value ? 'Current password is required' : null),
new_password: (value) => (!value ? 'New password is required' : value.length < 6 ? 'Password must be at least 6 characters' : null),
confirm_password: (value, values) => (value !== values.new_password ? 'Passwords do not match' : null),
},
});
const handleSubmit = (values: typeof form.values) => {
apiChangeMyPassword(values.old_password, values.new_password)
.then(res => {
if (res.success) {
showInfoMessage('', 'Password changed successfully', 3000);
form.reset();
onClose();
onSuccess();
} else {
showErrorMessage(res.message, 'Failed to change password');
}
})
.catch(err => {
showErrorMessage(err.toString(), 'Failed to change password');
});
};
return (
<Modal opened={opened} onClose={onClose} title="Change Password" centered>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<PasswordInput
label="Current Password"
placeholder="Enter your current password"
withAsterisk
{...form.getInputProps('old_password')}
/>
<PasswordInput
label="New Password"
placeholder="Enter new password"
withAsterisk
{...form.getInputProps('new_password')}
/>
<PasswordInput
label="Confirm New Password"
placeholder="Confirm new password"
withAsterisk
{...form.getInputProps('confirm_password')}
/>
<Group justify="flex-end" mt="md">
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button type="submit">Change Password</Button>
</Group>
</Stack>
</form>
</Modal>
);
}
interface PatientHomeProps {
patientData: PatientData;
refreshUserInfo: () => void;
}
function PatientHome({ patientData, refreshUserInfo }: PatientHomeProps) {
const navigate = useNavigate();
const [changePasswordOpened, setChangePasswordOpened] = useState(false);
const [currentWard, setCurrentWard] = useState<WardInfo | null>(null);
const [loadingWard, setLoadingWard] = useState(false);
const [bookings, setBookings] = useState<PatientBookingInfo[]>([]);
const [loadingBookings, setLoadingBookings] = useState(false);
const [treatments, setTreatments] = useState<TreatmentInfoWithDoctorInfo[]>([]);
const [loadingTreatments, setLoadingTreatments] = useState(false);
const { changePage } = useOutletContext<OutletContextType>();
useEffect(() => {
fetchCurrentWard();
fetchRecentBookings();
fetchRecentTreatments();
}, []);
const fetchCurrentWard = () => {
setLoadingWard(true);
apiPatientGetCurrentWard()
.then(res => {
if (res.success && res.data.ward) {
setCurrentWard(res.data.ward);
} else {
setCurrentWard(null);
}
})
.catch(err => {
showErrorMessage(err.toString(), 'Error');
})
.finally(() => {
setLoadingWard(false);
});
};
const fetchRecentBookings = () => {
setLoadingBookings(true);
apiGetPatientBookingList(1)
.then(res => {
if (res.success) {
const recentBookings = [...res.data.appointments]
.sort((a, b) => b.id - a.id)
.slice(0, 5);
setBookings(recentBookings);
} else {
showErrorMessage(res.message, 'Failed to load appointments');
}
})
.catch(err => {
showErrorMessage(err.toString(), 'Error');
})
.finally(() => {
setLoadingBookings(false);
});
};
const fetchRecentTreatments = () => {
setLoadingTreatments(true);
apiGetTreatmentRecords(1, patientData.id, null, null)
.then(res => {
if (res.success) {
// Get only the 10 most recent treatments
const recentTreatments = [...res.data.treatments]
.sort((a, b) => b.treated_at - a.treated_at)
.slice(0, 10);
setTreatments(recentTreatments);
} else {
showErrorMessage(res.message, 'Failed to load treatments');
}
})
.catch(err => {
showErrorMessage(err.toString(), 'Error');
})
.finally(() => {
setLoadingTreatments(false);
});
};
const handleEditInfo = () => {
confirmEditPatient(patientData, () => {
refreshUserInfo();
});
};
const getStatusBadge = (booking: PatientBookingInfo) => {
if (booking.discharged) {
return <Badge color="purple">Discharged</Badge>;
} else if (booking.admitted) {
return <Badge color="blue">Admitted</Badge>;
} else if (booking.approved) {
if (booking.assigned_team === null) {
return <Badge color="red">Rejected</Badge>;
} else {
return <Badge color="green">Approved</Badge>;
}
} else {
return <Badge color="yellow">Pending</Badge>;
}
};
const getCategoryDisplay = (category: string) => {
const key = Object.entries(BookingCategory).find(([_, value]) => value === category)?.[0];
return key ? key.replace(/_/g, ' ') : category;
};
const bookingRows = bookings.map((booking) => (
<Table.Tr key={booking.id}>
<Table.Td>{booking.id}</Table.Td>
<Table.Td>{getCategoryDisplay(booking.category)}</Table.Td>
<Table.Td>{new Date(booking.appointment_time * 1000).toLocaleString('en-GB').replace(',', '')}</Table.Td>
<Table.Td>
<TruncatedText
text={booking.description}
title={`Description - Appointment #${booking.id}`}
/>
</Table.Td>
<Table.Td>{getStatusBadge(booking)}</Table.Td>
<Table.Td>
{booking.assigned_team ? (
<TeamInfoDisplay teamId={booking.assigned_team} />
) : (
<Text size="sm" c="dimmed">Not Assigned</Text>
)}
</Table.Td>
<Table.Td>
{booking.feedback ? (
<TruncatedText
text={booking.feedback}
title={`Feedback - Appointment #${booking.id}`}
/>
) : (
<Text>-</Text>
)}
</Table.Td>
</Table.Tr>
));
const treatmentRows = treatments.map((treatment) => (
<Table.Tr key={treatment.id}>
<Table.Td>{treatment.id}</Table.Td>
<Table.Td>
<DoctorInfoDisplay doctorId={treatment.doctor_id} />
</Table.Td>
<Table.Td>
{treatment.team ? (
<TeamInfoDisplay teamId={treatment.team.id} />
) : (
<Text size="sm" c="dimmed">Not Assigned</Text>
)}
</Table.Td>
<Table.Td>
<TruncatedText
text={treatment.treat_info}
title={`Treatment Info - Record #${treatment.id}`}
/>
</Table.Td>
<Table.Td>{new Date(treatment.treated_at * 1000).toLocaleString('en-GB').replace(',', '')}</Table.Td>
</Table.Tr>
));
return (
<Stack>
<Card padding="lg" radius="md" withBorder>
<Card.Section withBorder>
<Group justify="space-between" style={marginRound}>
<Stack gap="xs">
<Text size="xl" fw={700}>Hi, {patientData.title} {patientData.name}</Text>
<Text size="sm" c="dimmed">Welcome to the Hospital Management System</Text>
</Stack>
<Group>
<Button
leftSection={<EditIcon style={iconMStyle} />}
onClick={handleEditInfo}
>
Edit Profile
</Button>
<Button
leftSection={<LockResetIcon style={iconMStyle} />}
onClick={() => setChangePasswordOpened(true)}
>
Change Password
</Button>
</Group>
</Group>
</Card.Section>
<Card.Section>
<Grid style={marginRound}>
<Grid.Col span={{ base: 12, md: 6 }}>
<Stack gap="sm">
<Group>
<Text fw={500} w={100}>Full Name:</Text>
<Text>{patientData.title} {patientData.name}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Gender:</Text>
<Text>{patientData.gender}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Birth Date:</Text>
<Text>{patientData.birth_date}</Text>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Stack gap="sm">
<Group>
<Text fw={500} w={100}>Email:</Text>
<Text>{patientData.email}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Phone:</Text>
<Text>{patientData.phone}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Address:</Text>
<Text>{patientData.address}, {patientData.postcode}</Text>
</Group>
</Stack>
</Grid.Col>
</Grid>
</Card.Section>
</Card>
<Card padding="lg" radius="md" withBorder>
<Card.Section withBorder>
<Group style={{...marginLeftRight, ...marginTop}}>
<Text size="lg" fw={700} mb="md">Current Ward</Text>
</Group>
</Card.Section>
<Card.Section>
{loadingWard ? (
<Group style={marginRound}>
<Text>Loading...</Text>
</Group>
) : currentWard ? (
<Grid style={marginRound}>
<Grid.Col span={6}>
<Stack gap="sm">
<Group>
<Text fw={500} w={120}>Ward ID:</Text>
<Text>{currentWard.id}</Text>
</Group>
<Group>
<Text fw={500} w={120}>Ward Name:</Text>
<Text>{currentWard.name}</Text>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={6}>
<Stack gap="sm">
<Group>
<Text fw={500} w={120}>Ward Type:</Text>
<Text>{currentWard.type.replace(/_/g, ' ')}</Text>
</Group>
<Group>
<Text fw={500} w={120}>Occupancy:</Text>
<Text>{currentWard.current_occupancy} / {currentWard.total_capacity}</Text>
</Group>
</Stack>
</Grid.Col>
</Grid>
) : (
<Group style={marginRound}>
<Text c="dimmed">You are not currently admitted to any ward.</Text>
</Group>
)}
</Card.Section>
</Card>
<Card padding="lg" radius="md" withBorder>
<Group justify="space-between" mb="md">
<Text size="lg" fw={700}>Recent Appointments</Text>
<Button
rightSection={<ArrowRightIcon style={iconMStyle} />}
variant="subtle"
onClick={() => changePage(DashboardPageType.PatientBooking)}
>
View All
</Button>
</Group>
<Card.Section withBorder>
<Group style={marginRound}>
{loadingBookings ? (
<Text>Loading...</Text>
) : bookings.length > 0 ? (
<ResponsiveTableContainer minWidth={600}>
<Table striped highlightOnHover withColumnBorders withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Category</Table.Th>
<Table.Th>Appointment Time</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Team</Table.Th>
<Table.Th>Feedback</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{bookingRows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
) : (
<Text c="dimmed">No appointments found.</Text>
)}
</Group>
</Card.Section>
</Card>
<Card padding="lg" radius="md" withBorder>
<Group justify="space-between" mb="md">
<Text size="lg" fw={700}>Recent Treatments</Text>
<Button
rightSection={<ArrowRightIcon style={iconMStyle} />}
variant="subtle"
onClick={() => changePage(DashboardPageType.TreatmentRecord, `/dashboard/treatment_record/${patientData.id}`)}
>
View All
</Button>
</Group>
<Card.Section withBorder>
<Group style={marginRound}>
{loadingTreatments ? (
<Text>Loading...</Text>
) : treatments.length > 0 ? (
<ResponsiveTableContainer minWidth={600}>
<Table striped highlightOnHover withColumnBorders withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Doctor</Table.Th>
<Table.Th>Medical Team</Table.Th>
<Table.Th>Treatment Info</Table.Th>
<Table.Th>Treatment Date</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{treatmentRows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
) : (
<Text c="dimmed">No treatment records found.</Text>
)}
</Group>
</Card.Section>
</Card>
<ChangePasswordModal
opened={changePasswordOpened}
onClose={() => setChangePasswordOpened(false)}
onSuccess={() => {navigate("/dashboard/account/login")}}
/>
</Stack>
);
}
interface DoctorHomeProps {
doctorData: DoctorDataWithPermission;
doctorTeams: DoctorTeamInfo[];
refreshUserInfo: () => void;
}
function DoctorHome({ doctorData, doctorTeams, refreshUserInfo }: DoctorHomeProps) {
const [changePasswordOpened, setChangePasswordOpened] = useState(false);
const [assignedPatients, setAssignedPatients] = useState<any[]>([]);
const [loadingPatients, setLoadingPatients] = useState(false);
const { changePage } = useOutletContext<OutletContextType>();
const navigate = useNavigate();
useEffect(() => {
fetchAssignedPatients();
}, []);
const fetchAssignedPatients = () => {
setLoadingPatients(true);
apiGetTreatmentRecords(1, null, doctorData.id, null)
.then(res => {
if (res.success) {
const uniquePatientIds = new Set();
const patientTreatments = res.data.treatments
.filter(treatment => {
if (!uniquePatientIds.has(treatment.patient_id)) {
uniquePatientIds.add(treatment.patient_id);
return true;
}
return false;
})
.slice(0, 15); // Get only the first 15 unique patients
setAssignedPatients(patientTreatments);
} else {
showErrorMessage(res.message, 'Failed to load patient assignments');
}
})
.catch(err => {
showErrorMessage(err.toString(), 'Error');
})
.finally(() => {
setLoadingPatients(false);
});
};
const handleEditInfo = () => {
confirmEditDoctor(doctorData, () => {
refreshUserInfo();
});
};
const patientRows = assignedPatients.map((treatment) => (
<Table.Tr key={treatment.id}>
<Table.Td>
<PatientInfoDisplay patientId={treatment.patient_id} />
</Table.Td>
<Table.Td>
<TruncatedText
text={treatment.treat_info}
title={`Treatment Info - Record #${treatment.id}`}
/>
</Table.Td>
<Table.Td>{new Date(treatment.treated_at * 1000).toLocaleString('en-GB').replace(',', '')}</Table.Td>
</Table.Tr>
));
return (
<Stack>
<Card padding="lg" radius="md" withBorder>
<Card.Section withBorder>
<Group justify="space-between" style={marginRound}>
<Stack gap="xs">
<Text size="xl" fw={700}>Hi, {doctorData.title} {doctorData.name}</Text>
<Text size="sm" c="dimmed">Welcome to the Hospital Management System</Text>
</Stack>
<Group>
<Button
leftSection={<EditIcon style={iconMStyle} />}
onClick={handleEditInfo}
>
Edit Profile
</Button>
<Button
leftSection={<LockResetIcon style={iconMStyle} />}
onClick={() => setChangePasswordOpened(true)}
>
Change Password
</Button>
</Group>
</Group>
</Card.Section>
<Card.Section>
<Grid style={marginRound}>
<Grid.Col span={{ base: 12, md: 6 }}>
<Stack gap="sm">
<Group>
<Text fw={500} w={100}>Full Name:</Text>
<Text>{doctorData.title} {doctorData.name}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Grade:</Text>
<Text>{DoctorGrade[doctorData.grade]}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Gender:</Text>
<Text>{doctorData.gender}</Text>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Stack gap="sm">
<Group>
<Text fw={500} w={100}>Birth Date:</Text>
<Text>{doctorData.birth_date}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Email:</Text>
<Text>{doctorData.email}</Text>
</Group>
<Group>
<Text fw={500} w={100}>Phone:</Text>
<Text>{doctorData.phone}</Text>
</Group>
</Stack>
</Grid.Col>
</Grid>
</Card.Section>
</Card>
<DoctorTeamsSimple doctorTeams={doctorTeams} />
<Card padding="lg" radius="md" withBorder>
<Group justify="space-between" mb="md">
<Text size="lg" fw={700}>Recent Patients</Text>
<Button
rightSection={<ArrowRightIcon style={iconMStyle} />}
variant="subtle"
onClick={() => changePage(DashboardPageType.DoctorTreatment)}
>
View All
</Button>
</Group>
<Card.Section withBorder>
<Group style={marginRound}>
{loadingPatients ? (
<Text>Loading...</Text>
) : assignedPatients.length > 0 ? (
<ResponsiveTableContainer minWidth={600}>
<Table striped highlightOnHover withColumnBorders withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Patient</Table.Th>
<Table.Th>Last Treatment</Table.Th>
<Table.Th>Treatment Date</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{patientRows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
) : (
<Text c="dimmed">No patients assigned yet.</Text>
)}
</Group>
</Card.Section>
</Card>
<ChangePasswordModal
opened={changePasswordOpened}
onClose={() => setChangePasswordOpened(false)}
onSuccess={() => {navigate("/dashboard/account/login")}}
/>
</Stack>
);
}
export default function Component() {
const { loginUserInfo, refreshMyInfo } = useOutletContext<OutletContextType>();
if (!loginUserInfo) {
return (
<Stack>
<Text size="1.5em" fw={700} style={marginLeftRight}>Dashboard</Text>
<Text>Loading user information...</Text>
</Stack>
);
}
const isDoctor = loginUserInfo.user_type === UserType.DOCTOR;
return (
<Stack>
{isDoctor && loginUserInfo.doctor_data ? (
<DoctorHome
doctorData={loginUserInfo.doctor_data}
doctorTeams={loginUserInfo.doctor_teams || []}
refreshUserInfo={refreshMyInfo}
/>
) : loginUserInfo.patient_data ? (
<PatientHome
patientData={loginUserInfo.patient_data}
refreshUserInfo={refreshMyInfo}
/>
) : (
<Text>Unable to load user profile information.</Text>
)}
</Stack>
);
}