diff --git a/public/locale/en.json b/public/locale/en.json index a8211b379a4..980ac3f3f1d 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -979,6 +979,7 @@ "etiology_identified": "Etiology identified", "evening_slots": "Evening Slots", "events": "Events", + "exception": "Exception", "exception_created": "Exception created successfully", "exception_deleted": "Exception deleted", "exceptions": "Exceptions", @@ -1878,6 +1879,8 @@ "session_capacity": "Session Capacity", "session_expired": "Session Expired", "session_expired_msg": "It appears that your session has expired. This could be due to inactivity. Please login again to continue.", + "session_slots_info": "{{slots}} slots of {{minutes}} mins.", + "session_slots_info_striked": "{{intended_slots}} slots {{actual_slots}} slots of {{minutes}} mins.", "session_title": "Session Title", "session_title_placeholder": "IP Rounds", "session_type": "Session Type", diff --git a/src/components/Users/UserAvailabilityTab.tsx b/src/components/Users/UserAvailabilityTab.tsx index e1f83cfdb66..6ac227bec67 100644 --- a/src/components/Users/UserAvailabilityTab.tsx +++ b/src/components/Users/UserAvailabilityTab.tsx @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { useQueryParams } from "raviger"; import { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; @@ -14,23 +15,37 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import Loading from "@/components/Common/Loading"; import useSlug from "@/hooks/useSlug"; import query from "@/Utils/request/query"; -import { formatTimeShort } from "@/Utils/utils"; +import { + dateQueryString, + formatTimeShort, + humanizeStrings, +} from "@/Utils/utils"; import ScheduleExceptions from "@/pages/Scheduling/ScheduleExceptions"; import ScheduleTemplates from "@/pages/Scheduling/ScheduleTemplates"; import CreateScheduleExceptionSheet from "@/pages/Scheduling/components/CreateScheduleExceptionSheet"; import CreateScheduleTemplateSheet from "@/pages/Scheduling/components/CreateScheduleTemplateSheet"; import { + computeAppointmentSlots, filterAvailabilitiesByDayOfWeek, getSlotsPerSession, isDateInRange, } from "@/pages/Scheduling/utils"; -import { AvailabilityDateTime } from "@/types/scheduling/schedule"; +import { + AvailabilityDateTime, + ScheduleException, + ScheduleTemplate, +} from "@/types/scheduling/schedule"; import scheduleApis from "@/types/scheduling/scheduleApis"; import { UserBase } from "@/types/user/user"; @@ -39,12 +54,16 @@ type Props = { }; type AvailabilityTabQueryParams = { - view?: "schedule" | "exceptions"; + tab?: "schedule" | "exceptions" | null; + sheet?: "create_template" | "add_exception" | null; + valid_from?: string | null; + valid_to?: string | null; }; export default function UserAvailabilityTab({ userData: user }: Props) { + const { t } = useTranslation(); const [qParams, setQParams] = useQueryParams(); - const view = qParams.view || "schedule"; + const view = qParams.tab || "schedule"; const [month, setMonth] = useState(new Date()); const facilityId = useSlug("facility"); @@ -144,72 +163,12 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
- -
-

- {date.toLocaleDateString("default", { - day: "numeric", - month: "short", - year: "numeric", - })} -

- -
- - - {templates.map((template) => ( -
-
- -

- {template.name} -

-
- -
- {template.availabilities.map( - ({ - id, - name, - slot_type, - availability, - slot_size_in_minutes, - }) => ( -
-

{name}

-

- {slot_type} - | - - {/* TODO: handle multiple days of week */} - {formatAvailabilityTime(availability)} - -

- {slot_type === "appointment" && ( -

- {Math.floor( - getSlotsPerSession( - availability[0].start_time, - availability[0].end_time, - slot_size_in_minutes, - ) ?? 0, - )}{" "} - slots of {slot_size_in_minutes} mins. -

- )} -
- ), - )} -
-
- ))} -
-
+ ); }} @@ -220,20 +179,20 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
{view === "schedule" && ( @@ -284,6 +243,198 @@ export default function UserAvailabilityTab({ userData: user }: Props) { ); } +function DayDetailsPopover({ + date, + templates, + unavailableExceptions, + setQParams, +}: { + date: Date; + templates: ScheduleTemplate[]; + unavailableExceptions: ScheduleException[]; + setQParams: (params: AvailabilityTabQueryParams) => void; +}) { + const { t } = useTranslation(); + + return ( + +
+

+ {date.toLocaleDateString("default", { + day: "numeric", + month: "short", + year: "numeric", + })} +

+ +
+ + + {templates.map((template) => ( +
+
+ +

{template.name}

+
+ +
+ {template.availabilities.map((availability) => ( + + ))} +
+
+ ))} + + {unavailableExceptions.length > 0 && ( +
+ {unavailableExceptions.map((exception) => ( +
+
+
+

+ {t("exception")}: {exception.reason} +

+

+ {formatTimeShort(exception.start_time)} + - + {formatTimeShort(exception.end_time)} +

+
+
+ ))} +
+ )} + + + ); +} + +function ScheduleTemplateAvailabilityItem({ + availability, + unavailableExceptions, + date, +}: { + availability: ScheduleTemplate["availabilities"][0]; + unavailableExceptions: ScheduleException[]; + date: Date; +}) { + const { t } = useTranslation(); + + if (availability.slot_type !== "appointment") { + return ( +
+

{availability.name}

+

+ + {t(`SCHEDULE_AVAILABILITY_TYPE__${availability.slot_type}`)} + + | + + {/* TODO: handle multiple days of week */} + {formatAvailabilityTime(availability.availability)} + +

+
+ ); + } + + const intendedSlots = getSlotsPerSession( + availability.availability[0].start_time, + availability.availability[0].end_time, + availability.slot_size_in_minutes, + ); + + const computedSlots = computeAppointmentSlots( + availability, + unavailableExceptions, + date, + ); + + const availableSlots = computedSlots.filter( + (slot) => slot.isAvailable, + ).length; + + const exceptions = [ + ...new Set(computedSlots.flatMap((slot) => slot.exceptions)), + ]; + const hasExceptions = exceptions.length > 0; + + return ( +
+

{availability.name}

+

+ + {t(`SCHEDULE_AVAILABILITY_TYPE__${availability.slot_type}`)} + + | + + {formatAvailabilityTime(availability.availability)} + +

+ {availability.slot_type === "appointment" && ( +

+ {availableSlots === intendedSlots ? ( + t("session_slots_info", { + slots: availableSlots, + minutes: availability.slot_size_in_minutes, + }) + ) : ( + + + + , + }} + values={{ + intended_slots: intendedSlots, + actual_slots: availableSlots, + minutes: availability.slot_size_in_minutes, + }} + /> + + + {hasExceptions && ( + +

+ {t("exceptions")}:{" "} + {humanizeStrings(exceptions.map((e) => e.reason))} +

+ + )} + + )} +

+ )} +
+ ); +} + const diagonalStripes = { backgroundImage: `repeating-linear-gradient( -45deg, diff --git a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx index cb9e93a8090..1433d94ecb0 100644 --- a/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx +++ b/src/pages/Scheduling/components/CreateScheduleExceptionSheet.tsx @@ -1,7 +1,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { isBefore, parse } from "date-fns"; -import { useEffect, useState } from "react"; +import { isAfter, isBefore, parse } from "date-fns"; +import { useQueryParams } from "raviger"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -41,12 +42,23 @@ interface Props { trigger?: React.ReactNode; } +type QueryParams = { + sheet?: "add_exception" | null; + valid_from?: string | null; + valid_to?: string | null; +}; + export default function CreateScheduleExceptionSheet({ 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(); + const setQParams = (p: QueryParams) => _setQParams(p, { replace: false }); const formSchema = z .object({ @@ -77,19 +89,10 @@ export default function CreateScheduleExceptionSheet({ path: ["start_time"], // This will show the error on the start_time field }, ) - .refine( - (data) => { - return isBefore(data.valid_from, data.valid_to); - }, - { - message: t("from_date_must_be_before_to_date"), - path: ["valid_from"], // This will show the error on the valid_from field - }, - ); - - const queryClient = useQueryClient(); - - const [open, setOpen] = useState(false); + .refine((data) => !isAfter(data.valid_from, data.valid_to), { + message: t("from_date_must_be_before_to_date"), + path: ["valid_from"], // This will show the error on the valid_from field + }); const form = useForm>({ resolver: zodResolver(formSchema), @@ -103,13 +106,25 @@ export default function CreateScheduleExceptionSheet({ }, }); + useEffect(() => { + if (qParams.valid_from) { + form.setValue("valid_from", new Date(qParams.valid_from)); + } + }, [qParams.valid_from, form]); + + useEffect(() => { + if (qParams.valid_to) { + form.setValue("valid_to", new Date(qParams.valid_to)); + } + }, [qParams.valid_to, form]); + const { mutate: createException, isPending } = useMutation({ mutationFn: mutate(scheduleApis.exceptions.create, { pathParams: { facility_id: facilityId }, }), onSuccess: () => { toast.success(t("exception_created")); - setOpen(false); + setQParams({ sheet: null, valid_from: null, valid_to: null }); form.reset(); queryClient.invalidateQueries({ queryKey: ["user-schedule-exceptions", { facilityId, userId }], @@ -127,7 +142,7 @@ export default function CreateScheduleExceptionSheet({ form.resetField("start_time"); form.resetField("end_time"); } - }, [unavailableAllDay]); + }, [unavailableAllDay, form]); function onSubmit(data: z.infer) { createException({ @@ -141,7 +156,16 @@ export default function CreateScheduleExceptionSheet({ } return ( - + + setQParams({ + sheet: open ? "add_exception" : null, + valid_from: null, + valid_to: null, + }) + } + > {trigger ?? (