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

346 lines
13 KiB
TypeScript

import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/PatientBooking";
import {useDisclosure} from "@mantine/hooks";
import {useEffect, useMemo, useState} from "react";
import {BookingCategory, DashboardPageType} from "~/utils/hms_enums.ts";
import {useNavigate, useOutletContext} from "react-router";
import {
type OutletContextType,
type PatientBookingInfo,
type DoctorTeamInfo,
SORT_SYMBOLS
} from "~/utils/models.ts";
import {Accordion, ActionIcon, Button, Card, Group, Pagination, Stack, Table, Text, Badge} from "@mantine/core";
import {apiGetPatientBookingList, get_team_info} from "~/utils/hms_api.ts";
import {showErrorMessage, timestampToDate} from "~/utils/utils.ts";
import {iconMStyle, marginLeftRight, marginRightBottom, marginRound, marginTopBottom, textCenter} from "~/styles.ts";
import PencilIcon from "mdi-react/PencilIcon";
import DeleteIcon from "mdi-react/DeleteIcon";
import InfoIcon from "mdi-react/InfoIcon";
import {
confirmDeleteBooking,
confirmEditOrCreateBooking,
confirmViewTeamMembers
} from "~/components/subs/confirms.tsx";
import AddIcon from "mdi-react/AddIcon";
import {TruncatedText} from "~/components/subs/TruncatedText.tsx";
import { ResponsiveTableContainer } from "~/components/subs/ResponsiveTableContainer";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Patient Booking" },
{ name: "description", content: "Dashboard Patient Booking" },
];
}
export default function Component() {
const navigate = useNavigate();
const [refreshingBookingList, setRefreshingBookingList] = useState<boolean>(false);
const [bookingInfo, setBookingInfo] = useState<{appointments: PatientBookingInfo[], total_pages: number}>({appointments: [], total_pages: 1});
const [teamInfoMap, setTeamInfoMap] = useState<Map<number, DoctorTeamInfo>>(new Map());
const [loadingTeams, setLoadingTeams] = useState<Set<number>>(new Set());
const [sortKey, setSortKey] = useState<string>("");
const [sortDesc, setSortDesc] = useState<boolean>(true);
const [currPage, setCurrPage] = useState<number>(1);
const { loginUserInfo, refreshMyInfo } = useOutletContext<OutletContextType>();
useEffect(() => {
refreshBookingList();
}, []);
useEffect(() => {
refreshBookingList();
}, [currPage]);
const refreshBookingList = () => {
setRefreshingBookingList(true);
apiGetPatientBookingList(currPage).then(res => {
if (!res.success) {
showErrorMessage(res.message, "Failed to get appointments list");
return;
}
setBookingInfo(res.data);
// Fetch team info for all appointments with assigned teams
const teamsToFetch = res.data.appointments
.filter(app => app.assigned_team !== null)
.map(app => app.assigned_team as number);
// Remove duplicates
const uniqueTeams = [...new Set(teamsToFetch)];
// Fetch team info for each unique team ID
uniqueTeams.forEach(teamId => {
fetchTeamInfo(teamId);
});
})
.catch(err => {})
.finally(() => { setRefreshingBookingList(false); });
};
const fetchTeamInfo = (teamId: number) => {
if (teamId === null || teamInfoMap.has(teamId) || loadingTeams.has(teamId)) {
return;
}
setLoadingTeams(prev => new Set([...prev, teamId]));
get_team_info(teamId).then(res => {
if (res.success) {
setTeamInfoMap(prev => {
const newMap = new Map(prev);
newMap.set(teamId, res.data.team);
return newMap;
});
}
})
.catch(err => {})
.finally(() => {
setLoadingTeams(prev => {
const newSet = new Set(prev);
newSet.delete(teamId);
return newSet;
});
});
};
const handleViewTeam = (teamId: number) => {
const teamInfo = teamInfoMap.get(teamId);
if (teamInfo) {
confirmViewTeamMembers(teamInfo);
} else {
// If team info is not fetched yet, fetch it and then show
get_team_info(teamId).then(res => {
if (res.success) {
setTeamInfoMap(prev => {
const newMap = new Map(prev);
newMap.set(teamId, res.data.team);
return newMap;
});
confirmViewTeamMembers({
...res.data.team,
members: res.data.members
});
} else {
showErrorMessage(res.message, "Failed to fetch team information");
}
}).catch(err => {
showErrorMessage("Failed to fetch team information", "Error");
});
}
};
const handleSort = (key: string) => {
if (sortKey === key) {
setSortDesc(!sortDesc);
} else {
setSortKey(key);
setSortDesc(false); // Default ascending
}
};
const sortedBookings = useMemo(() => {
let data = [...bookingInfo.appointments];
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;
}
if (typeof valA === 'boolean' && typeof valB === 'boolean') {
return sortDesc ? (valB ? 1 : -1) : (valA ? 1 : -1);
}
return 0;
});
return data;
}, [bookingInfo.appointments, sortKey, sortDesc]);
const handleCreateBooking = () => {
confirmEditOrCreateBooking(null, refreshBookingList);
};
const handleEditBooking = (booking: PatientBookingInfo) => {
confirmEditOrCreateBooking(booking, refreshBookingList);
};
const handleDeleteBooking = (bookingId: number, category: string) => {
confirmDeleteBooking(bookingId, category, refreshBookingList);
};
const getCategoryDisplay = (category: string) => {
const key = Object.entries(BookingCategory).find(([_, value]) => value === category)?.[0];
return key ? key.replace(/_/g, ' ') : category;
};
const getStatusBadge = (appointment: PatientBookingInfo) => {
if (appointment.discharged) {
return <Badge color="purple">Discharged</Badge>;
} else if (appointment.admitted) {
return <Badge color="blue">Admitted</Badge>;
} else if (appointment.approved) {
if (appointment.assigned_team === null) {
return <Badge color="red">Rejected</Badge>;
} else {
return <Badge color="green">Approved</Badge>;
}
} else {
return <Badge color="yellow">Pending</Badge>;
}
};
const getTeamDisplay = (booking: PatientBookingInfo) => {
if (!booking.approved) {
return <Text size="sm" c="dimmed">Pending approval</Text>;
}
if (booking.assigned_team === null) {
return <Text size="sm" c="dimmed">Not assigned</Text>;
}
const teamInfo = teamInfoMap.get(booking.assigned_team);
if (!teamInfo) {
return (
<Group>
<Text>Team ID: {booking.assigned_team}</Text>
<ActionIcon
onClick={() => handleViewTeam(booking.assigned_team as number)}
title="View team details"
>
<InfoIcon style={iconMStyle} />
</ActionIcon>
</Group>
);
}
return (
<Group>
<Text>{teamInfo.department.replace(/_/g, ' ')}</Text>
<ActionIcon
onClick={() => handleViewTeam(booking.assigned_team as number)}
title="View team details"
>
<InfoIcon style={iconMStyle} />
</ActionIcon>
</Group>
);
};
const rows = sortedBookings.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 - Booking #${booking.id}`}
/>
</Table.Td>
<Table.Td>{getStatusBadge(booking)}</Table.Td>
<Table.Td>
{booking.feedback ?
<TruncatedText
text={booking.feedback}
title={`Feedback - Booking #${booking.id}`}
/> :
'-'
}
</Table.Td>
<Table.Td>{getTeamDisplay(booking)}</Table.Td>
<Table.Td>
<Group>
{!booking.approved && (
<>
<ActionIcon onClick={() => handleEditBooking(booking)}>
<PencilIcon style={iconMStyle}/>
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDeleteBooking(booking.id, booking.category)}>
<DeleteIcon style={iconMStyle}/>
</ActionIcon>
</>
)}
{booking.approved && (
<Text size="sm" c="dimmed">No actions available</Text>
)}
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Stack>
<Group justify="space-between" align="center" style={marginLeftRight}>
<Text size="1.5em" fw={700}>My Appointments</Text>
<Button
leftSection={<AddIcon style={iconMStyle}/>}
onClick={handleCreateBooking}
>Book Appointment</Button>
</Group>
<Card padding="lg" radius="md" withBorder style={marginTopBottom}>
<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("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("approved")}>
Status{" "}
{sortKey === "approved" && SORT_SYMBOLS[sortDesc ? "desc" : "asc"]}
</Table.Th>
<Table.Th>
Feedback
</Table.Th>
<Table.Th>
Assigned Team
</Table.Th>
<Table.Th>
Actions
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ResponsiveTableContainer>
<Pagination
withEdges
total={bookingInfo.total_pages}
value={currPage}
onChange={setCurrPage}
mt="sm"
style={{justifyItems: "flex-end", ...marginRightBottom}}
/>
</Card.Section>
</Card>
</Stack>
);
}