coronasafe/care_fe

View on GitHub
src/pages/Appointments/AppointmentsPage.tsx

Summary

Maintainability
F
6 days
Test Coverage
File `AppointmentsPage.tsx` has 1034 lines of code (exceeds 250 allowed). Consider refactoring.
import careConfig from "@careConfig";
import { CaretDownIcon, CheckIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
addDays,
format,
formatDate,
isPast,
isToday,
isTomorrow,
isYesterday,
subDays,
} from "date-fns";
import { Edit3Icon } from "lucide-react";
import { Link, navigate } from "raviger";
import { useEffect } from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
 
import { cn } from "@/lib/utils";
 
import CareIcon from "@/CAREUI/icons/CareIcon";
 
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { DateRangePicker } from "@/components/ui/date-range-picker";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 
import Loading from "@/components/Common/Loading";
import Page from "@/components/Common/Page";
import { TableSkeleton } from "@/components/Common/SkeletonLoading";
 
import useAppHistory from "@/hooks/useAppHistory";
import useAuthUser from "@/hooks/useAuthUser";
import useFilters, { FilterState } from "@/hooks/useFilters";
 
import { getPermissions } from "@/common/Permissions";
 
import routes from "@/Utils/request/api";
import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import { useView } from "@/Utils/useView";
import {
dateQueryString,
formatDateTime,
formatName,
formatPatientAge,
} from "@/Utils/utils";
import { usePermissions } from "@/context/PermissionContext";
import { PractitionerSelector } from "@/pages/Appointments/components/PractitionerSelector";
import {
formatSlotTimeRange,
groupSlotsByAvailability,
} from "@/pages/Appointments/utils";
import { getFakeTokenNumber } from "@/pages/Scheduling/utils";
import {
Appointment,
AppointmentStatuses,
TokenSlot,
} from "@/types/scheduling/schedule";
import scheduleApis from "@/types/scheduling/scheduleApi";
 
interface DateRangeDisplayProps {
dateFrom: string | null;
dateTo: string | null;
}
 
Similar blocks of code found in 3 locations. Consider refactoring.
function AppointmentsEmptyState() {
const { t } = useTranslation();
return (
<Card className="flex flex-col items-center justify-center p-8 text-center border-dashed">
<div className="rounded-full bg-primary/10 p-3 mb-4">
<CareIcon icon="l-calendar-slash" className="size-6 text-primary" />
</div>
<h3 className="text-lg font-semibold mb-1">{t("no_appointments")}</h3>
<p className="text-sm text-gray-500 mb-4">
{t("adjust_appointments_filters")}
</p>
</Card>
);
}
 
Function `DateRangeDisplay` has a Cognitive Complexity of 18 (exceeds 5 allowed). Consider refactoring.
function DateRangeDisplay({ dateFrom, dateTo }: DateRangeDisplayProps) {
const { t } = useTranslation();
 
if (!dateFrom && !dateTo) {
return (
<span className="text-gray-500">{t("showing_all_appointments")}</span>
);
}
 
const today = new Date();
 
// Case 1: Today only or Yesterday only
if (
(dateFrom === dateQueryString(today) &&
dateTo === dateQueryString(today)) ||
(dateFrom === dateQueryString(subDays(today, 1)) &&
dateTo === dateQueryString(subDays(today, 1)))
) {
<>
{dateFrom === dateQueryString(today) ? (
Similar blocks of code found in 2 locations. Consider refactoring.
<>
<span className="text-black">{t("today")}</span>
<span className="pl-1 text-gray-500">
({formatDate(dateFrom, "dd MMM yyyy")})
</span>
</>
) : (
Similar blocks of code found in 2 locations. Consider refactoring.
<>
<span className="text-black">{t("yesterday")}</span>
<span className="pl-1 text-gray-500">
({formatDate(dateFrom, "dd MMM yyyy")})
</span>
</>
)}
</>;
}
 
// Case 2: Pre-defined ranges
const ranges = [
{
label: t("last_week_short"),
from: subDays(today, 7),
to: today,
},
{
label: t("next_week_short"),
from: today,
to: addDays(today, 7),
},
{
label: t("next_month"),
from: today,
to: addDays(today, 30),
},
];
 
const matchingRange = ranges.find(
(range) =>
dateFrom &&
dateTo &&
dateQueryString(range.from) === dateFrom &&
dateQueryString(range.to) === dateTo,
);
 
if (matchingRange && dateFrom && dateTo) {
return (
<>
<span className="text-black">{matchingRange.label}</span>
<span className="pl-1 text-gray-500">
({formatDate(dateFrom, "dd MMM yyyy")} -{" "}
{formatDate(dateTo, "dd MMM yyyy")})
</span>
</>
);
}
 
// Case 3: Same date with relative labels
if (dateFrom && dateFrom === dateTo) {
const date = new Date(dateFrom);
let relativeDay = null;
 
if (isToday(date)) {
relativeDay = t("today");
} else if (isTomorrow(date)) {
relativeDay = t("tomorrow");
} else if (isYesterday(date)) {
relativeDay = t("yesterday");
}
 
if (relativeDay) {
return (
<>
<span className="text-black">{relativeDay}</span>
<span className="pl-1 text-gray-500">
({formatDate(dateFrom, "dd MMM yyyy")})
</span>
</>
);
}
 
return (
<>
<span className="capitalize text-gray-500">{t("on")} </span>
<span className="pl-1 text-black ">
{formatDate(dateFrom, "dd MMM yyyy")}
</span>
</>
);
}
 
// Case 4: Single date (before or after)
Similar blocks of code found in 2 locations. Consider refactoring.
if (dateFrom && !dateTo) {
Avoid too many `return` statements within this function.
return (
<>
<span className="capitalize text-gray-500">{t("after")} </span>
<span className="pl-1 text-black">
{formatDate(dateFrom, "dd MMM yyyy")}
</span>
</>
);
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
if (!dateFrom && dateTo) {
Avoid too many `return` statements within this function.
return (
<>
<span className=" capitalize text-gray-500">{t("before")} </span>
<span className="pl-1 text-black">
{formatDate(dateTo, "dd MMM yyyy")}
</span>
</>
);
}
 
// Case 5: Date range
Avoid too many `return` statements within this function.
return (
<span className="text-black">
{formatDate(dateFrom!, "dd MMM yyyy")} -{" "}
{formatDate(dateTo!, "dd MMM yyyy")}
</span>
);
}
 
Function `AppointmentsPage` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
export default function AppointmentsPage({
facilityId,
}: {
facilityId: string;
}) {
const { t } = useTranslation();
const authUser = useAuthUser();
const { qParams, updateQuery, resultsPerPage, Pagination } = useFilters({
limit: 15,
});
 
const [activeTab, setActiveTab] = useView("appointments", "board");
 
const { hasPermission } = usePermissions();
const { goBack } = useAppHistory();
 
Similar blocks of code found in 4 locations. Consider refactoring.
const { data: facilityData, isLoading: isFacilityLoading } = useQuery({
queryKey: ["facility", facilityId],
queryFn: query(routes.getPermittedFacility, {
pathParams: { id: facilityId },
}),
});
 
const { canViewAppointments } = getPermissions(
hasPermission,
facilityData?.permissions ?? [],
);
 
Similar blocks of code found in 3 locations. Consider refactoring.
const schedulableUsersQuery = useQuery({
queryKey: ["practitioners", facilityId],
queryFn: query(scheduleApis.appointments.availableUsers, {
pathParams: { facility_id: facilityId },
}),
});
 
const resources = schedulableUsersQuery.data?.users;
const practitioner = resources?.find(
(r) => r.username === qParams.practitioner,
);
 
useEffect(() => {
// trigger this effect only when there are no query params already applied, and once the query is loaded
if (Object.keys(qParams).length !== 0 || schedulableUsersQuery.isLoading) {
return;
}
 
// Sets the practitioner filter to the current user if they are in the list of
// schedulable users and no practitioner was selected.
if (
!qParams.practitioner &&
schedulableUsersQuery.data?.users.some(
(r) => r.username === authUser.username,
)
) {
qParams.practitioner = authUser.username;
}
 
// Set default date range if no dates are present
if (!qParams.date_from && !qParams.date_to) {
const today = new Date();
const defaultDays = careConfig.appointments.defaultDateFilter;
 
if (defaultDays === 0) {
// Today only
qParams.date_from = dateQueryString(today);
qParams.date_to = dateQueryString(today);
} else {
// Past or future days based on configuration
const fromDate = defaultDays > 0 ? today : addDays(today, defaultDays);
const toDate = defaultDays > 0 ? addDays(today, defaultDays) : today;
qParams.date_from = dateQueryString(fromDate);
qParams.date_to = dateQueryString(toDate);
}
}
 
// Only update if there are changes
if (Object.keys(qParams).length > 0) {
updateQuery({
...qParams,
});
}
}, [schedulableUsersQuery.isLoading]);
 
// Enabled only if filtered by a practitioner and a single day
const slotsFilterEnabled =
!!qParams.date_from &&
!!practitioner &&
(qParams.date_from === qParams.date_to || !qParams.date_to);
 
const slotsQuery = useQuery({
queryKey: ["slots", facilityId, qParams.practitioner, qParams.date_from],
queryFn: query(scheduleApis.slots.getSlotsForDay, {
pathParams: { facility_id: facilityId },
body: {
// voluntarily coalesce to empty string since we know query would be
// enabled only if practitioner and date_from are present
user: practitioner?.id ?? "",
day: qParams.date_from ?? "",
},
}),
enabled: slotsFilterEnabled,
});
 
const slots = slotsQuery.data?.results?.filter((s) => s.allocated > 0);
const slot = slots?.find((s) => s.id === qParams.slot);
 
useEffect(() => {
if (!canViewAppointments && !isFacilityLoading) {
toast.error(t("no_permission_to_view_page"));
goBack("/");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canViewAppointments, isFacilityLoading]);
 
if (schedulableUsersQuery.isLoading) {
return <Loading />;
}
 
return (
<Page
title={t("appointments")}
options={
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as "board" | "list")}
>
<TabsList>
Similar blocks of code found in 4 locations. Consider refactoring.
<TabsTrigger value="board">
<CareIcon icon="l-kanban" />
<span>{t("board")}</span>
</TabsTrigger>
Similar blocks of code found in 4 locations. Consider refactoring.
<TabsTrigger value="list">
<CareIcon icon="l-list-ul" />
<span>{t("list")}</span>
</TabsTrigger>
</TabsList>
</Tabs>
}
>
<div className="mt-4 py-4 flex flex-col lg:flex-row gap-4 justify-between border-t border-gray-200">
<div className="flex flex-col xl:flex-row gap-4 items-start md:items-start md:w-xs">
<div className="mt-1 w-full">
<Label className="mb-2 text-black">
{t("select_practitioner")}
</Label>
<PractitionerSelector
facilityId={facilityId}
selected={practitioner ?? null}
onSelect={(user) =>
updateQuery({
practitioner: user?.username ?? null,
slot: null,
})
}
clearSelection={t("show_all")}
/>
</div>
 
<div>
<div className="flex items-center gap-1 -mt-2">
<Popover modal>
<PopoverTrigger asChild>
<Button variant="ghost">
<Label>
<DateRangeDisplay
dateFrom={qParams.date_from}
dateTo={qParams.date_to}
/>
</Label>
<Edit3Icon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto" align="start">
<div className="flex flex-col gap-4">
<div className="flex justify-between">
<Button
variant="link"
size="xs"
onClick={() => {
const today = new Date();
updateQuery({
date_from: dateQueryString(subDays(today, 7)),
date_to: dateQueryString(today),
slot: null,
});
}}
>
{t("last_week_short")}
</Button>
 
<Button
variant="link"
size="xs"
onClick={() => {
const today = new Date();
updateQuery({
date_from: dateQueryString(subDays(today, 1)),
date_to: dateQueryString(subDays(today, 1)),
slot: null,
});
}}
>
{t("yesterday")}
</Button>
 
<Button
variant="link"
size="xs"
onClick={() => {
const today = new Date();
updateQuery({
date_from: dateQueryString(today),
date_to: dateQueryString(today),
slot: null,
});
}}
>
{t("today")}
</Button>
 
Similar blocks of code found in 2 locations. Consider refactoring.
<Button
variant="link"
size="xs"
onClick={() => {
const today = new Date();
updateQuery({
date_from: dateQueryString(today),
date_to: dateQueryString(addDays(today, 7)),
slot: null,
});
}}
>
{t("next_week_short")}
</Button>
 
Similar blocks of code found in 2 locations. Consider refactoring.
<Button
variant="link"
size="xs"
onClick={() => {
const today = new Date();
updateQuery({
date_from: dateQueryString(today),
date_to: dateQueryString(addDays(today, 30)),
slot: null,
});
}}
>
{t("next_month")}
</Button>
</div>
 
<DateRangePicker
date={{
from: qParams.date_from
? new Date(qParams.date_from)
: undefined,
to: qParams.date_to
? new Date(qParams.date_to)
: undefined,
}}
onChange={(date) =>
updateQuery({
date_from: date?.from
? dateQueryString(date.from)
: null,
date_to: date?.to ? dateQueryString(date?.to) : null,
slot: null,
})
}
/>
</div>
</PopoverContent>
</Popover>
</div>
 
{slotsFilterEnabled && !!slots?.length && (
<SlotFilter
slots={slots}
selectedSlot={slot}
onSelect={(slot) => {
if (slot === "all") {
updateQuery({ slot: null });
} else {
updateQuery({ slot });
}
}}
/>
)}
</div>
</div>
 
<div className="flex gap-4 items-center">
<Input
className="md:w-xs w-full"
placeholder={t("search")}
value={qParams.search ?? ""}
onChange={(e) => updateQuery({ search: e.target.value })}
/>
</div>
</div>
 
{activeTab === "board" ? (
<ScrollArea>
<div className="flex w-max space-x-4">
{(
[
"booked",
"checked_in",
"in_consultation",
"fulfilled",
"noshow",
] as const
).map((status) => (
<AppointmentColumn
key={status}
status={status}
facilityId={facilityId}
slot={slot?.id}
practitioner={practitioner?.id ?? null}
date_from={qParams.date_from}
date_to={qParams.date_to}
search={qParams.search?.toLowerCase()}
canViewAppointments={canViewAppointments}
/>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
) : (
<AppointmentRow
facilityId={facilityId}
updateQuery={updateQuery}
practitioner={practitioner?.id ?? null}
slot={qParams.slot}
page={qParams.page}
date_from={qParams.date_from}
date_to={qParams.date_to}
search={qParams.search?.toLowerCase()}
canViewAppointments={canViewAppointments}
resultsPerPage={resultsPerPage}
status={qParams.status}
Pagination={Pagination}
/>
)}
</Page>
);
}
 
Function `AppointmentColumn` has 82 lines of code (exceeds 25 allowed). Consider refactoring.
Function `AppointmentColumn` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
function AppointmentColumn(props: {
facilityId: string;
status: Appointment["status"];
practitioner: string | null;
slot?: string | null;
date_from: string | null;
date_to: string | null;
search?: string;
canViewAppointments: boolean;
}) {
const { t } = useTranslation();
 
const { data } = useQuery({
queryKey: [
"appointments",
props.facilityId,
props.status,
props.practitioner,
props.slot,
props.date_from,
props.date_to,
],
queryFn: query(scheduleApis.appointments.list, {
pathParams: { facility_id: props.facilityId },
queryParams: {
status: props.status,
limit: 100,
slot: props.slot,
user: props.practitioner ?? undefined,
date_after: props.date_from,
date_before: props.date_to,
},
}),
enabled: !!props.date_from && !!props.date_to && props.canViewAppointments,
});
 
let appointments = data?.results ?? [];
 
Similar blocks of code found in 2 locations. Consider refactoring.
if (props.search) {
appointments = appointments.filter(({ patient }) =>
patient.name.toLowerCase().includes(props.search!),
);
}
 
return (
<div
className={cn(
"bg-gray-100 py-4 rounded-lg w-[20rem] overflow-y-hidden",
!data && "animate-pulse",
)}
>
<div className="flex px-3 items-center gap-2 mb-4">
<h2 className="font-semibold capitalize text-base px-1">
{t(props.status)}
</h2>
<span className="bg-gray-200 px-2 py-1 rounded-md text-xs font-medium">
{data?.count == null ? (
"..."
) : data.count === appointments.length ? (
data.count
) : (
<Trans
i18nKey="showing_x_of_y"
values={{
x: appointments.length,
y: data.count,
}}
components={{
strong: <span className="font-bold" />,
}}
/>
)}
</span>
</div>
{appointments.length === 0 ? (
<div className="flex justify-center items-center h-[calc(100vh-18rem)]">
<p className="text-gray-500">{t("no_appointments")}</p>
</div>
) : (
<ScrollArea>
<ul className="space-y-3 px-3 pb-4 pt-1 h-[calc(100vh-18rem)]">
{appointments.map((appointment) => (
<li key={appointment.id}>
<Link
href={`/facility/${props.facilityId}/patient/${appointment.patient.id}/appointments/${appointment.id}`}
className="text-inherit"
>
<AppointmentCard appointment={appointment} />
</Link>
</li>
))}
</ul>
</ScrollArea>
)}
</div>
);
}
 
function AppointmentCard({ appointment }: { appointment: Appointment }) {
const { patient } = appointment;
const { t } = useTranslation();
 
return (
<div className="bg-white p-3 rounded shadow-sm group hover:ring-1 hover:ring-primary-700 hover:ring-offset-1 hover:ring-offset-white hover:shadow-md transition-all duration-100 ease-in-out">
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold text-base group-hover:text-primary-700 transition-all duration-200 ease-in-out">
{patient.name}
</h3>
Similar blocks of code found in 2 locations. Consider refactoring.
<p className="text-sm text-gray-700">
{formatPatientAge(patient as any, true)},{" "}
{t(`GENDER__${patient.gender}`)}
</p>
<p className="text-xs text-gray-500 mt-1">
{formatDateTime(
appointment.token_slot.start_datetime,
"ddd, DD MMM YYYY, HH:mm",
)}
</p>
</div>
 
<div className="bg-gray-100 px-2 py-1 rounded text-center">
<p className="text-[10px] uppercase">{t("token")}</p>
{/* TODO: replace this with token number once that's ready... */}
<p className="font-bold text-2xl uppercase">
{getFakeTokenNumber(appointment)}
</p>
</div>
</div>
</div>
);
}
 
Function `AppointmentRow` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.
function AppointmentRow(props: {
facilityId: string;
page: number | null;
practitioner: string | null;
Pagination: ({
totalCount,
noMargin,
}: {
totalCount: number;
noMargin?: boolean;
}) => React.ReactNode;
updateQuery: (filter: FilterState) => void;
resultsPerPage: number;
slot: string | null;
status: string | null;
date_from: string | null;
date_to: string | null;
search?: string;
canViewAppointments: boolean;
}) {
const { t } = useTranslation();
 
const { data, isLoading } = useQuery({
queryKey: [
"appointments",
props.facilityId,
props.status,
props.page,
props.practitioner,
props.slot,
props.date_from,
props.date_to,
],
queryFn: query(scheduleApis.appointments.list, {
pathParams: { facility_id: props.facilityId },
queryParams: {
status: props.status ?? "booked",
slot: props.slot,
user: props.practitioner ?? undefined,
date_after: props.date_from,
date_before: props.date_to,
limit: props.resultsPerPage,
offset: ((props.page ?? 1) - 1) * props.resultsPerPage,
},
}),
enabled: !!props.date_from && !!props.date_to && props.canViewAppointments,
});
 
let appointments = data?.results ?? [];
 
Similar blocks of code found in 2 locations. Consider refactoring.
if (props.search) {
appointments = appointments.filter(({ patient }) =>
patient.name.toLowerCase().includes(props.search!),
);
}
return (
<div className="overflow-x-auto">
<div className={cn(!data && "animate-pulse")}>
<div className="hidden md:flex">
<Tabs
value={props.status ?? "booked"}
className="overflow-x-auto"
onValueChange={(value) => props.updateQuery({ status: value })}
>
<TabsList>
<TabsTrigger value="booked">{t("booked")}</TabsTrigger>
<TabsTrigger value="checked_in">{t("checked_in")}</TabsTrigger>
<TabsTrigger value="in_consultation">
{t("in_consultation")}
</TabsTrigger>
<TabsTrigger value="fulfilled">{t("fulfilled")}</TabsTrigger>
<TabsTrigger value="noshow">{t("noshow")}</TabsTrigger>
</TabsList>
</Tabs>
</div>
 
{/* Status Filter - Mobile */}
<div className="md:hidden">
<Select
value={props.status || "booked"}
onValueChange={(value) => props.updateQuery({ status: value })}
>
<SelectTrigger className="h-8 w-40">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="booked">
<div className="flex items-center">Booked</div>
</SelectItem>
<SelectItem value="checked_in">
<div className="flex items-center">Checked In</div>
</SelectItem>
<SelectItem value="in_consultation">
<div className="flex items-center">In Consultation</div>
</SelectItem>
<SelectItem value="fulfilled">
<div className="flex items-center">Fulfilled</div>
</SelectItem>
<SelectItem value="noshow">
<div className="flex items-center">No Show</div>
</SelectItem>
</SelectContent>
</Select>
</div>
 
{isLoading ? (
<TableSkeleton count={5} />
) : appointments.length === 0 ? (
<AppointmentsEmptyState />
) : (
<Table className="p-2 border-separate border-gray-200 border-spacing-y-3">
<TableHeader>
<TableRow>
<TableHead className="pl-8 font-semibold text-black text-xs">
{t("patient")}
</TableHead>
<TableHead className="font-semibold text-black text-xs">
{t("consulting_doctor")}
</TableHead>
<TableHead className="font-semibold text-black text-xs">
{t("current_status")}
</TableHead>
<TableHead className="font-semibold text-black text-xs">
{t("token_no")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="">
{appointments.map((appointment) => (
<TableRow
key={appointment.id}
className="shadow-sm rounded-lg cursor-pointer group"
onClick={() =>
navigate(
`/facility/${props.facilityId}/patient/${appointment.patient.id}/appointments/${appointment.id}`,
)
}
>
<AppointmentRowItem
appointment={appointment}
facilityId={props.facilityId}
/>
</TableRow>
))}
</TableBody>
</Table>
)}
{props.Pagination({ totalCount: data?.count ?? 0 })}
</div>
</div>
);
}
 
function AppointmentRowItem({
appointment,
facilityId,
}: {
appointment: Appointment;
facilityId: string;
}) {
const { patient } = appointment;
const { t } = useTranslation();
 
return (
<>
<TableCell className="py-6 group-hover:bg-gray-100 bg-white rounded-l-lg">
<span className="flex flex-row items-center gap-2">
<CareIcon
icon="l-draggabledots"
className="size-4 invisible group-hover:visible"
/>
<span className="flex flex-col">
<span className="text-sm font-semibold">{patient.name}</span>
Similar blocks of code found in 2 locations. Consider refactoring.
<span className="text-xs text-gray-500">
{formatPatientAge(patient as any, true)},{" "}
{t(`GENDER__${patient.gender}`)}
</span>
</span>
</span>
</TableCell>
{/* TODO: Replace with relevant information */}
<TableCell className="py-6 group-hover:bg-gray-100 bg-white">
{formatName(appointment.user)}
</TableCell>
<TableCell className="py-6 group-hover:bg-gray-100 bg-white">
<AppointmentStatusDropdown
appointment={appointment}
facilityId={facilityId}
/>
</TableCell>
{/* TODO: replace this with token number once that's ready... */}
<TableCell className="py-6 group-hover:bg-gray-100 bg-white rounded-r-lg">
{getFakeTokenNumber(appointment)}
</TableCell>
</>
);
}
 
Function `AppointmentStatusDropdown` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
const AppointmentStatusDropdown = ({
appointment,
facilityId,
}: {
appointment: Appointment;
facilityId: string;
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const currentStatus = appointment.status;
const hasStarted = isPast(appointment.token_slot.start_datetime);
 
const { mutate: updateAppointment } = useMutation({
mutationFn: mutate(scheduleApis.appointments.update, {
pathParams: {
facility_id: facilityId,
id: appointment.id,
},
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["appointments", facilityId],
});
queryClient.invalidateQueries({
queryKey: ["appointment", appointment.id],
});
},
});
 
// Get available status options based on current status
const getAvailableStatuses = () => {
if (
["fulfilled", "cancelled", "entered_in_error"].includes(currentStatus)
) {
return [currentStatus];
}
 
if (currentStatus === "booked") {
return ["booked", "checked_in", "in_consultation", "noshow", "cancelled"];
}
 
if (currentStatus === "checked_in") {
return ["checked_in", "in_consultation", "noshow", "cancelled"];
}
 
if (currentStatus === "in_consultation") {
return ["in_consultation", "fulfilled", "cancelled"];
}
 
Avoid too many `return` statements within this function.
return AppointmentStatuses;
};
 
return (
<div className="w-32" onClick={(e) => e.stopPropagation()}>
<Select
value={currentStatus}
onValueChange={(value) =>
updateAppointment({ status: value as Appointment["status"] })
}
>
<SelectTrigger>
<CareIcon icon="l-schedule" className="size-4" />
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableStatuses().map((status) => (
<SelectItem
key={status}
value={status}
disabled={
!hasStarted &&
["checked_in", "in_consultation", "fulfilled"].includes(status)
}
>
{t(status)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
 
interface SlotFilterProps {
slots: TokenSlot[];
disableInline?: boolean;
disabled?: boolean;
selectedSlot: TokenSlot | undefined;
onSelect: (slot: string) => void;
}
 
export const SlotFilter = ({
slots,
selectedSlot,
onSelect,
...props
}: SlotFilterProps) => {
const { t } = useTranslation();
 
if (slots.length <= 3 && !props.disableInline) {
return (
<Tabs value={selectedSlot?.id ?? "all"} onValueChange={onSelect}>
<TabsList>
<TabsTrigger
value="all"
className="uppercase"
disabled={props.disabled}
>
{t("all")}
</TabsTrigger>
{slots.map((slot) => (
<TabsTrigger
key={slot.id}
value={slot.id}
disabled={props.disabled}
>
{format(slot.start_datetime, "h:mm a").replace(":00", "")}
{" - "}
{format(slot.end_datetime, "h:mm a").replace(":00", "")}
</TabsTrigger>
))}
</TabsList>
</Tabs>
);
}
 
const slotsByAvailability = groupSlotsByAvailability(slots);
 
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="min-w-60 justify-start"
disabled={props.disabled}
>
{selectedSlot ? (
<div className="flex items-center gap-2">
<span>{formatSlotTimeRange(selectedSlot)}</span>
</div>
) : (
<span>{t("show_all_slots")}</span>
)}
<CaretDownIcon className="ml-auto" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder={t("search")}
className="outline-hidden border-none ring-0 shadow-none"
/>
<CommandList>
<CommandEmpty>{t("no_slots_found")}</CommandEmpty>
<CommandGroup>
<CommandItem
value="all"
onSelect={() => onSelect("all")}
className="cursor-pointer"
>
<span>{t("show_all")}</span>
{selectedSlot === undefined && (
<CheckIcon className="ml-auto" />
)}
</CommandItem>
</CommandGroup>
{slotsByAvailability.map(({ availability, slots }) => (
<>
<CommandSeparator />
<CommandGroup
key={availability.name}
heading={availability.name}
>
{slots.map((slot) => (
<CommandItem
key={slot.id}
value={formatSlotTimeRange(slot)}
onSelect={() => onSelect(slot.id)}
className="cursor-pointer"
>
<span>{formatSlotTimeRange(slot)}</span>
<span className="text-xs text-gray-500 font-medium">
{slot.allocated} / {availability.tokens_per_slot}
</span>
{selectedSlot?.id === slot.id && (
<CheckIcon className="ml-auto" />
)}
</CommandItem>
))}
</CommandGroup>
</>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};