Skip to content

Commit

Permalink
User Availabilities Exception related fixes (#9977)
Browse files Browse the repository at this point in the history
  • Loading branch information
rithviknishad authored Jan 16, 2025
1 parent 4bd274a commit 0ca5940
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 109 deletions.
3 changes: 3 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,7 @@
"etiology_identified": "Etiology identified",
"evening_slots": "Evening Slots",
"events": "Events",
"exception": "Exception",
"exception_created": "Exception created successfully",
"exception_deleted": "Exception deleted",
"exceptions": "Exceptions",
Expand Down Expand Up @@ -1885,6 +1886,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": "<s>{{intended_slots}} slots</s> {{actual_slots}} slots of {{minutes}} mins.",
"session_title": "Session Title",
"session_title_placeholder": "IP Rounds",
"session_type": "Session Type",
Expand Down
299 changes: 225 additions & 74 deletions src/components/Users/UserAvailabilityTab.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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";

Expand All @@ -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<AvailabilityTabQueryParams>();
const view = qParams.view || "schedule";
const view = qParams.tab || "schedule";
const [month, setMonth] = useState(new Date());

const facilityId = useSlug("facility");
Expand Down Expand Up @@ -144,72 +163,12 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
<div />
</div>
</PopoverTrigger>
<PopoverContent className=" p-6" align="center" sideOffset={5}>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">
{date.toLocaleDateString("default", {
day: "numeric",
month: "short",
year: "numeric",
})}
</p>
<Button variant="outline" size="sm">
Add Exception
</Button>
</div>

<ScrollArea className="h-[22rem]">
{templates.map((template) => (
<div key={template.id} className="border-t pt-3 mt-3">
<div className="flex items-center">
<ColoredIndicator
className="mr-2 size-3 rounded"
id={template.id}
/>
<h3 className="text-lg font-semibold">
{template.name}
</h3>
</div>

<div className="pl-5 py-2 space-y-4">
{template.availabilities.map(
({
id,
name,
slot_type,
availability,
slot_size_in_minutes,
}) => (
<div key={id}>
<h4 className="font-medium text-base">{name}</h4>
<p className="text-sm text-gray-600">
<span>{slot_type}</span>
<span className="px-2 text-gray-300">|</span>
<span className="text-sm">
{/* TODO: handle multiple days of week */}
{formatAvailabilityTime(availability)}
</span>
</p>
{slot_type === "appointment" && (
<p className="text-sm text-gray-600">
{Math.floor(
getSlotsPerSession(
availability[0].start_time,
availability[0].end_time,
slot_size_in_minutes,
) ?? 0,
)}{" "}
slots of {slot_size_in_minutes} mins.
</p>
)}
</div>
),
)}
</div>
</div>
))}
</ScrollArea>
</PopoverContent>
<DayDetailsPopover
date={date}
templates={templates}
unavailableExceptions={unavailableExceptions}
setQParams={setQParams}
/>
</Popover>
);
}}
Expand All @@ -220,20 +179,20 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
<div className="flex bg-gray-100 rounded-lg p-1 gap-1 max-w-min">
<Button
variant={view === "schedule" ? "outline" : "ghost"}
onClick={() => setQParams({ view: "schedule" })}
onClick={() => setQParams({ tab: "schedule" })}
className={cn(view === "schedule" && "shadow", "hover:bg-white")}
>
Schedule
{t("schedule")}
</Button>
<Button
variant={view === "exceptions" ? "outline" : "ghost"}
onClick={() => setQParams({ view: "exceptions" })}
onClick={() => setQParams({ tab: "exceptions" })}
className={cn(
view === "exceptions" && "shadow",
"hover:bg-white",
)}
>
Exceptions
{t("exceptions")}
</Button>
</div>
{view === "schedule" && (
Expand Down Expand Up @@ -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 (
<PopoverContent className="p-6" align="center" sideOffset={5}>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600">
{date.toLocaleDateString("default", {
day: "numeric",
month: "short",
year: "numeric",
})}
</p>
<Button
variant="outline"
size="sm"
onClick={() =>
setQParams({
tab: "exceptions",
sheet: "add_exception",
valid_from: dateQueryString(date),
valid_to: dateQueryString(date),
})
}
>
{t("add_exception")}
</Button>
</div>

<ScrollArea className="h-[22rem]">
{templates.map((template) => (
<div key={template.id} className="border-t pt-3 mt-3">
<div className="flex items-center">
<ColoredIndicator
className="mr-2 size-3 rounded"
id={template.id}
/>
<h3 className="text-lg font-semibold">{template.name}</h3>
</div>

<div className="pl-5 py-2 space-y-4">
{template.availabilities.map((availability) => (
<ScheduleTemplateAvailabilityItem
key={availability.id}
availability={availability}
unavailableExceptions={unavailableExceptions}
date={date}
/>
))}
</div>
</div>
))}

{unavailableExceptions.length > 0 && (
<div className="space-y-3 mt-2">
{unavailableExceptions.map((exception) => (
<div key={exception.id} className="flex items-start">
<div
className="mr-2 mt-1 w-3 h-8 rounded bg-yellow-200"
style={diagonalStripes}
/>
<div>
<p className="text-sm text-black font-medium">
{t("exception")}: {exception.reason}
</p>
<p className="text-sm text-gray-600">
<span>{formatTimeShort(exception.start_time)}</span>
<span className="px-2 text-gray-300">-</span>
<span>{formatTimeShort(exception.end_time)}</span>
</p>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</PopoverContent>
);
}

function ScheduleTemplateAvailabilityItem({
availability,
unavailableExceptions,
date,
}: {
availability: ScheduleTemplate["availabilities"][0];
unavailableExceptions: ScheduleException[];
date: Date;
}) {
const { t } = useTranslation();

if (availability.slot_type !== "appointment") {
return (
<div key={availability.id}>
<h4 className="font-medium text-base">{availability.name}</h4>
<p className="text-sm text-gray-600">
<span>
{t(`SCHEDULE_AVAILABILITY_TYPE__${availability.slot_type}`)}
</span>
<span className="px-2 text-gray-300">|</span>
<span className="text-sm">
{/* TODO: handle multiple days of week */}
{formatAvailabilityTime(availability.availability)}
</span>
</p>
</div>
);
}

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 (
<div key={availability.id}>
<h4 className="font-medium text-base">{availability.name}</h4>
<p className="text-sm text-gray-600">
<span>
{t(`SCHEDULE_AVAILABILITY_TYPE__${availability.slot_type}`)}
</span>
<span className="px-2 text-gray-300">|</span>
<span className="text-sm">
{formatAvailabilityTime(availability.availability)}
</span>
</p>
{availability.slot_type === "appointment" && (
<p className="text-sm text-gray-600">
{availableSlots === intendedSlots ? (
t("session_slots_info", {
slots: availableSlots,
minutes: availability.slot_size_in_minutes,
})
) : (
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-pointer underline">
<Trans
i18nKey="session_slots_info_striked"
components={{
s: <s />,
}}
values={{
intended_slots: intendedSlots,
actual_slots: availableSlots,
minutes: availability.slot_size_in_minutes,
}}
/>
</span>
</TooltipTrigger>
{hasExceptions && (
<TooltipContent className="max-w-xs" side="bottom">
<p className="font-medium mb-1">
{t("exceptions")}:{" "}
{humanizeStrings(exceptions.map((e) => e.reason))}
</p>
</TooltipContent>
)}
</Tooltip>
)}
</p>
)}
</div>
);
}

const diagonalStripes = {
backgroundImage: `repeating-linear-gradient(
-45deg,
Expand Down
Loading

0 comments on commit 0ca5940

Please sign in to comment.