coronasafe/care_fe

View on GitHub
src/pages/Scheduling/components/CreateScheduleTemplateSheet.tsx

Summary

Maintainability
F
1 wk
Test Coverage
File `CreateScheduleTemplateSheet.tsx` has 674 lines of code (exceeds 250 allowed). Consider refactoring.
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { isAfter, isBefore, parse } from "date-fns";
import { useQueryParams } from "raviger";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { toast } from "sonner";
import * as z from "zod";
 
import Callout from "@/CAREUI/display/Callout";
import CareIcon from "@/CAREUI/icons/CareIcon";
import WeekdayCheckbox, {
DayOfWeek,
} from "@/CAREUI/interactive/WeekdayCheckbox";
 
import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
 
import useBreakpoints from "@/hooks/useBreakpoints";
 
import mutate from "@/Utils/request/mutate";
import { Time } from "@/Utils/types";
import { dateQueryString } from "@/Utils/utils";
import {
calculateSlotDuration,
getSlotsPerSession,
getTokenDuration,
} from "@/pages/Scheduling/utils";
import { ScheduleAvailabilityCreateRequest } from "@/types/scheduling/schedule";
import scheduleApis from "@/types/scheduling/scheduleApi";
 
interface Props {
facilityId: string;
userId: string;
trigger?: React.ReactNode;
}
 
type QueryParams = {
sheet?: "create_template" | null;
};
 
Function `CreateScheduleTemplateSheet` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.
export default function CreateScheduleTemplateSheet({
facilityId,
userId,
trigger,
}: Props) {
const { t } = useTranslation();
const queryClient = useQueryClient();
 
// Voluntarily masking the setQParams function to merge with other query params if any (since path is not unique within the user availability tab)
const [qParams, _setQParams] = useQueryParams<QueryParams>();
const setQParams = (p: QueryParams) => _setQParams(p, { overwrite: false });
 
const weekdayFormat = useBreakpoints({
default: "alphabet",
md: "short",
} as const);
 
const formSchema = z
.object({
name: z.string().min(1, t("field_required")),
valid_from: z.date({
required_error: t("field_required"),
}),
valid_to: z.date({
required_error: t("field_required"),
}),
Identical blocks of code found in 2 locations. Consider refactoring.
weekdays: z
.array(z.number() as unknown as z.ZodType<DayOfWeek>)
.min(1, t("schedule_weekdays_min_error")),
availabilities: z
.array(
Similar blocks of code found in 2 locations. Consider refactoring.
z
.discriminatedUnion("slot_type", [
// Schema for appointment type
z.object({
slot_type: z.literal("appointment"),
name: z.string().min(1, t("field_required")),
reason: z.string(),
Similar blocks of code found in 8 locations. Consider refactoring.
start_time: z
.string()
.min(1, t("field_required")) as unknown as z.ZodType<Time>,
Similar blocks of code found in 8 locations. Consider refactoring.
end_time: z
.string()
.min(1, t("field_required")) as unknown as z.ZodType<Time>,
slot_size_in_minutes: z
.number()
.min(1, t("number_min_error", { min: 0 })),
tokens_per_slot: z
.number()
.min(1, t("number_min_error", { min: 0 })),
is_auto_fill: z.boolean().optional(),
num_of_slots: z
.number()
.min(1, t("number_min_error", { min: 0 })),
}),
// Schema for open and closed types
z.object({
slot_type: z.enum(["open", "closed"]),
name: z.string().min(1, t("field_required")),
reason: z.string(),
Similar blocks of code found in 8 locations. Consider refactoring.
start_time: z
.string()
.min(1, t("field_required")) as unknown as z.ZodType<Time>,
Similar blocks of code found in 8 locations. Consider refactoring.
end_time: z
.string()
.min(1, t("field_required")) as unknown as z.ZodType<Time>,
slot_size_in_minutes: z.literal(null),
tokens_per_slot: z.literal(null),
}),
])
.refine(
(data) => {
// Validate each availability's time range
const startTime = parse(data.start_time, "HH:mm", new Date());
const endTime = parse(data.end_time, "HH:mm", new Date());
return isBefore(startTime, endTime);
},
{
message: t("start_time_must_be_before_end_time"),
path: ["start_time"], // This will show error at the start_time field
},
),
)
.min(1, t("schedule_sessions_min_error")),
})
.refine((data) => !isAfter(data.valid_from, data.valid_to), {
message: t("from_date_must_be_before_to_date"),
path: ["valid_from"],
});
 
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
valid_from: undefined,
valid_to: undefined,
weekdays: [],
availabilities: [
{
name: "",
slot_type: "appointment",
reason: "",
start_time: undefined,
end_time: undefined,
tokens_per_slot: null as unknown as undefined,
slot_size_in_minutes: null as unknown as undefined,
is_auto_fill: false,
num_of_slots: 1,
},
],
},
});
 
const { mutate: createTemplate, isPending } = useMutation({
mutationFn: mutate(scheduleApis.templates.create, {
pathParams: { facility_id: facilityId },
}),
onSuccess: () => {
toast.success("Schedule template created successfully");
setQParams({ sheet: null });
form.reset();
queryClient.invalidateQueries({
queryKey: ["user-schedule-templates", { facilityId, userId }],
});
},
});
 
async function onSubmit(values: z.infer<typeof formSchema>) {
createTemplate({
valid_from: dateQueryString(values.valid_from),
valid_to: dateQueryString(values.valid_to),
name: values.name,
user: userId,
availabilities: values.availabilities.map(
(availability) =>
({
name: availability.name,
slot_type: availability.slot_type,
slot_size_in_minutes: availability.slot_size_in_minutes,
tokens_per_slot: availability.tokens_per_slot,
reason: availability.reason,
Similar blocks of code found in 2 locations. Consider refactoring.
availability: values.weekdays.map((day) => ({
day_of_week: day,
start_time: availability.start_time,
end_time: availability.end_time,
})),
}) as ScheduleAvailabilityCreateRequest,
),
});
}
 
Function `timeAllocationCallout` has 27 lines of code (exceeds 25 allowed). Consider refactoring.
const timeAllocationCallout = (index: number) => {
const startTime = form.watch(`availabilities.${index}.start_time`);
const endTime = form.watch(`availabilities.${index}.end_time`);
const slotSizeInMinutes = form.watch(
`availabilities.${index}.slot_size_in_minutes`,
);
const tokensPerSlot = form.watch(`availabilities.${index}.tokens_per_slot`);
 
if (!startTime || !endTime || !slotSizeInMinutes || !tokensPerSlot) {
return null;
}
 
const slotsPerSession = getSlotsPerSession(
startTime,
endTime,
slotSizeInMinutes,
);
const tokenDuration = getTokenDuration(slotSizeInMinutes, tokensPerSlot);
 
if (!slotsPerSession || !tokenDuration) return null;
 
Similar blocks of code found in 2 locations. Consider refactoring.
return (
<Callout variant="alert" badge="Info">
<Trans
i18nKey="schedule_slots_allocation_callout"
values={{
slots: Math.floor(slotsPerSession),
token_duration: tokenDuration.toFixed(1).replace(".0", ""),
}}
/>
</Callout>
);
};
 
const updateSlotDuration = (index: number) => {
const isAutoFill = form.watch(`availabilities.${index}.is_auto_fill`);
if (isAutoFill) {
const duration = calculateSlotDuration(
form.watch(`availabilities.${index}.start_time`),
form.watch(`availabilities.${index}.end_time`),
form.watch(`availabilities.${index}.num_of_slots`),
);
form.setValue(`availabilities.${index}.slot_size_in_minutes`, duration);
}
};
 
return (
<Sheet
open={qParams.sheet === "create_template"}
onOpenChange={(open) =>
setQParams({ sheet: open ? "create_template" : null })
}
>
<SheetTrigger asChild>
{trigger ?? (
<Button variant="primary" disabled={isPending}>
{t("create_template")}
</Button>
)}
</SheetTrigger>
<SheetContent className="flex min-w-full flex-col bg-gray-100 sm:min-w-fit ">
Similar blocks of code found in 2 locations. Consider refactoring.
<SheetHeader>
<SheetTitle>{t("create_schedule_template")}</SheetTitle>
<SheetDescription className="sr-only">
{t("create_schedule_template")}
</SheetDescription>
</SheetHeader>
 
<div className="-mx-6 mb-16 overflow-auto px-6 pb-16 pt-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
Similar blocks of code found in 2 locations. Consider refactoring.
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel aria-required>
{t("schedule_template_name")}
</FormLabel>
<FormControl>
<Input
placeholder={t("schedule_template_name_placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
 
Similar blocks of code found in 2 locations. Consider refactoring.
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="valid_from"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel aria-required>{t("valid_from")}</FormLabel>
<DatePicker
date={field.value}
onChange={(date) => field.onChange(date)}
/>
<FormMessage />
</FormItem>
)}
/>
 
<FormField
control={form.control}
name="valid_to"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel aria-required>{t("valid_to")}</FormLabel>
<DatePicker
date={field.value}
onChange={(date) => field.onChange(date)}
/>
<FormMessage />
</FormItem>
)}
/>
</div>
 
<div>
<FormLabel className="text-lg font-semibold">
{t("weekly_schedule")}
</FormLabel>
<span className="block text-sm">
{t("schedule_weekdays_description")}
</span>
<div className="py-2">
<FormField
control={form.control}
name="weekdays"
render={({ field }) => (
<FormItem>
<FormControl>
<WeekdayCheckbox
value={field.value}
onChange={field.onChange}
format={weekdayFormat}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
 
<div className="space-y-4">
{form.watch("availabilities")?.map((_, index) => (
<div
key={index}
className="flex flex-col rounded-lg bg-white p-4 shadow-sm"
>
<div className="flex items-center justify-between pb-6">
<div className="flex items-center gap-2">
<CareIcon
icon="l-clock"
className="text-lg text-blue-600"
/>
<span className="font-semibold">
{form.watch(`availabilities.${index}.name`)}
</span>
</div>
<Button
type="button"
variant="secondary"
size="sm"
className="text-gray-600 hover:text-gray-900"
onClick={() => {
const availabilities =
form.getValues("availabilities");
availabilities.splice(index, 1);
form.setValue("availabilities", availabilities);
}}
>
<CareIcon icon="l-trash" className="text-base" />
<span className="ml-2">{t("remove")}</span>
</Button>
</div>
 
<div className="flex flex-col gap-x-6 gap-y-4">
<div className="items-stretch">
<FormField
control={form.control}
name={`availabilities.${index}.name`}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel aria-required>
{t("session_title")}
</FormLabel>
<FormControl>
<Input
placeholder={t("session_title_placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
 
{/* <FormField
control={form.control}
name={`availabilities.${index}.slot_type`}
render={({ field }) => (
<FormItem className="col-span-2 md:col-span-1">
<FormLabel aria-required>{t("session_type")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue asChild>
<span>
{t(
`SCHEDULE_AVAILABILITY_TYPE__${field.value}`,
)}
</span>
</SelectValue>
</SelectTrigger>
</FormControl>
<SelectContent>
{["appointment", "open", "closed"].map(
(type) => (
<SelectItem key={type} value={type}>
<p>
{t(
`SCHEDULE_AVAILABILITY_TYPE__${type}`,
)}
</p>
<p className="text-xs text-gray-500">
{t(
`SCHEDULE_AVAILABILITY_TYPE_DESCRIPTION__${type}`,
)}
</p>
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/> */}
<div className="flex flex-wrap">
Similar blocks of code found in 2 locations. Consider refactoring.
<FormField
control={form.control}
name={`availabilities.${index}.start_time`}
render={({ field }) => (
<FormItem className="flex flex-col w-full">
<FormLabel aria-required>
{t("start_time")}
</FormLabel>
<FormControl>
<Input
type="time"
{...field}
onChange={(e) => {
field.onChange(e);
updateSlotDuration(index);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
 
Similar blocks of code found in 2 locations. Consider refactoring.
<FormField
control={form.control}
name={`availabilities.${index}.end_time`}
render={({ field }) => (
<FormItem className="flex flex-col w-full mt-2">
<FormLabel aria-required>
{t("end_time")}
</FormLabel>
<FormControl>
<Input
type="time"
{...field}
onChange={(e) => {
field.onChange(e);
updateSlotDuration(index);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
 
{form.watch(`availabilities.${index}.slot_type`) ===
"appointment" && (
<>
<div className="flex flex-wrap mt-0 pt-2 gap-2">
<div className="w-full gap-x-2 grid grid-cols-[auto_1fr_auto] mb-2 bg-gray-50 p-3 rounded-lg">
<CareIcon
icon="l-bolt"
className="text-lg text-blue-600"
/>
<Label
htmlFor={`auto-fill-${index}`}
className="text-sm font-medium cursor-pointer col-start-2"
>
{t("auto_fill_slot_duration")}
</Label>
<Switch
className="col-start-3"
id={`auto-fill-${index}`}
checked={form.watch(
`availabilities.${index}.is_auto_fill`,
)}
onCheckedChange={(checked) => {
form.setValue(
`availabilities.${index}.is_auto_fill`,
checked,
);
if (checked) {
updateSlotDuration(index);
}
}}
/>
{form.watch(
`availabilities.${index}.is_auto_fill`,
) && (
<div className="row-start-2 col-start-2 col-span-2">
<FormField
control={form.control}
name={`availabilities.${index}.num_of_slots`}
render={({ field }) => (
<FormItem className="flex flex-col mt-2 space-y-0">
<Label className="text-sm font-light">
{t("number_of_slots")}
</Label>
<FormControl>
<Input
type="number"
min={1}
defaultValue={1}
{...field}
className="shadow-none"
onChange={(e) => {
field.onChange(
e.target.valueAsNumber,
);
updateSlotDuration(index);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
 
<FormField
control={form.control}
name={`availabilities.${index}.slot_size_in_minutes`}
render={({ field }) => (
<FormItem className="flex grow flex-col">
<FormLabel
aria-required
className="whitespace-nowrap "
>
{t("schedule_slot_size_label")}
</FormLabel>
<FormControl>
<Input
type="number"
min={0}
placeholder="e.g. 10"
{...field}
value={field.value ?? ""}
onChange={(e) => {
field.onChange(e.target.valueAsNumber);
}}
disabled={form.watch(
`availabilities.${index}.is_auto_fill`,
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
 
<FormField
control={form.control}
name={`availabilities.${index}.tokens_per_slot`}
render={({ field }) => (
<FormItem className="flex flex-col grow">
<FormLabel
aria-required
className="whitespace-nowrap"
>
{t("patients_per_slot")}
</FormLabel>
Similar blocks of code found in 2 locations. Consider refactoring.
<FormControl>
<Input
type="number"
min={0}
placeholder="e.g. 1"
{...field}
value={field.value ?? ""}
onChange={(e) =>
field.onChange(e.target.valueAsNumber)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="col-span-2 mb-2">
{timeAllocationCallout(index)}
</div>
</>
)}
</div>
 
<div>
<FormField
control={form.control}
name={`availabilities.${index}.reason`}
Similar blocks of code found in 2 locations. Consider refactoring.
render={({ field }) => (
<FormItem>
<FormLabel>{t("remarks")}</FormLabel>
<FormControl>
<Textarea
placeholder={t("remarks_placeholder")}
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
))}
{form.formState.errors.availabilities && (
<FormMessage>
{form.formState.errors.availabilities.root?.message}
</FormMessage>
)}
</div>
 
<Button
type="button"
variant="outline_primary"
onClick={() => {
const availabilities = form.getValues("availabilities");
form.setValue("availabilities", [
...availabilities,
{
name: "",
slot_type: "appointment",
reason: "",
start_time: "00:00",
end_time: "00:00",
tokens_per_slot: null as unknown as number,
slot_size_in_minutes: null as unknown as number,
is_auto_fill: false,
num_of_slots: 1,
},
]);
}}
>
<CareIcon icon="l-plus" className="text-lg" />
<span>{t("add_another_session")}</span>
</Button>
 
<SheetFooter className="absolute inset-x-0 bottom-0 border-t border-gray-200 bg-white p-6">
<SheetClose asChild>
<Button variant="outline" type="button" disabled={isPending}>
{t("cancel")}
</Button>
</SheetClose>
 
Similar blocks of code found in 3 locations. Consider refactoring.
<Button variant="primary" type="submit" disabled={isPending}>
{isPending ? t("saving") : t("save")}
</Button>
</SheetFooter>
</form>
</Form>
</div>
</SheetContent>
</Sheet>
);
}