From 6165966de4a7ac8be71b58e74f2366b47959212a Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 6 Jan 2025 21:24:07 +0530 Subject: [PATCH 01/10] improve global error handler --- src/Utils/request/errorHandler.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Utils/request/errorHandler.ts b/src/Utils/request/errorHandler.ts index ef2eba8bfe8..af5570ae29b 100644 --- a/src/Utils/request/errorHandler.ts +++ b/src/Utils/request/errorHandler.ts @@ -70,10 +70,10 @@ function isNotFound(error: HTTPError) { type PydanticError = { type: string; - loc: string[]; + loc?: string[]; msg: string; - input: unknown; - url: string; + input?: unknown; + url?: string; }; function isPydanticError(errors: unknown): errors is PydanticError[] { @@ -87,12 +87,15 @@ function isPydanticError(errors: unknown): errors is PydanticError[] { function handlePydanticErrors(errors: PydanticError[]) { errors.map(({ type, loc, msg }) => { - const title = type + if (!loc) { + toast.error(msg); + return; + } + type = type .replace("_", " ") .replace(/\b\w/g, (char) => char.toUpperCase()); - - toast.error(`${title}: '${loc.join(".")}'`, { - description: msg, + toast.error(msg, { + description: `${type}: '${loc.join(".")}'`, duration: 8000, }); }); From bad911cb9076be45c2a0ddbbb8796d09560c9f6b Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 6 Jan 2025 21:24:31 +0530 Subject: [PATCH 02/10] schedule & appointments: refactor; validations; fixes; new things... --- public/locale/en.json | 6 + .../Appointments/AppointmentDetailsPage.tsx | 39 ++++- .../Appointments/AppointmentsPage.tsx | 135 ++++++++++++------ .../Schedule/ScheduleExceptionForm.tsx | 43 +++--- .../Schedule/ScheduleExceptionsList.tsx | 28 ++-- .../Schedule/ScheduleTemplateForm.tsx | 40 ++---- .../Schedule/ScheduleTemplatesList.tsx | 59 +++++++- src/components/Schedule/api.ts | 7 + src/components/Users/UserAvailabilityTab.tsx | 34 ++--- 9 files changed, 259 insertions(+), 132 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 0d34499cb35..e8c332308d2 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -904,6 +904,7 @@ "etiology_identified": "Etiology identified", "evening_slots": "Evening Slots", "events": "Events", + "exception_created": "Exception created successfully", "exception_deleted": "Exception deleted", "exceptions": "Exceptions", "expand_sidebar": "Expand Sidebar", @@ -1160,6 +1161,7 @@ "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", "mark_all_as_read": "Mark all as Read", + "mark_as_entered_in_error": "Mark as entered in error", "mark_as_fulfilled": "Mark as Fullfilled", "mark_as_noshow": "Mark as no-show", "mark_as_read": "Mark as Read", @@ -1268,6 +1270,7 @@ "no_resource_requests_found": "No requests found", "no_results": "No results", "no_results_found": "No Results Found", + "no_schedule_templates_found": "No schedule templates found for this month.", "no_scheduled_exceptions_found": "No scheduled exceptions found", "no_slots_available": "No slots available", "no_slots_available_for_this_date": "No slots available for this date", @@ -1727,6 +1730,7 @@ "systolic": "Systolic", "tachycardia": "Tachycardia", "target_dosage": "Target Dosage", + "template_deleted": "Template has been deleted", "test_type": "Type of test done", "tested_on": "Tested on", "thank_you_for_choosing": "Thank you for choosing our care service", @@ -1743,6 +1747,7 @@ "today": "Today", "token": "Token", "token_no": "Token No.", + "tomorrow": "Tomorrow", "total_amount": "Total Amount", "total_beds": "Total Beds", "total_patients": "Total Patients", @@ -1922,6 +1927,7 @@ "years_of_experience": "Years of Experience", "years_of_experience_of_the_doctor": "Years of Experience of the Doctor", "yes": "Yes", + "yesterday": "Yesterday", "yet_to_be_decided": "Yet to be decided", "you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.", "zoom_in": "Zoom In", diff --git a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx index 55bba4d2862..65463bb8a28 100644 --- a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx @@ -12,15 +12,13 @@ import { } from "@radix-ui/react-icons"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { differenceInYears, format, isSameDay } from "date-fns"; -import { PrinterIcon } from "lucide-react"; +import { BanIcon, PrinterIcon } from "lucide-react"; import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - import { Badge, BadgeProps } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -175,6 +173,7 @@ export default function AppointmentDetailsPage(props: Props) {
updateAppointment({ status })} onViewPatient={redirectToPatientPage} @@ -357,20 +356,38 @@ const AppointmentDetails = ({ }; interface AppointmentActionsProps { + facilityId: string; appointment: Appointment; onChange: (status: Appointment["status"]) => void; onViewPatient: () => void; } const AppointmentActions = ({ + facilityId, appointment, onChange, onViewPatient, }: AppointmentActionsProps) => { const { t } = useTranslation(); + const queryClient = useQueryClient(); + const currentStatus = appointment.status; const isToday = isSameDay(appointment.token_slot.start_datetime, new Date()); + const { mutate: cancelAppointment } = useMutation({ + mutationFn: mutate(ScheduleAPIs.appointments.cancel, { + pathParams: { + facility_id: facilityId, + id: appointment.id, + }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["appointment", appointment.id], + }); + }, + }); + if (["fulfilled", "cancelled", "entered_in_error"].includes(currentStatus)) { return null; } @@ -449,10 +466,22 @@ const AppointmentActions = ({ )} - +
); }; diff --git a/src/components/Schedule/Appointments/AppointmentsPage.tsx b/src/components/Schedule/Appointments/AppointmentsPage.tsx index bd12619aea3..3ecd7ee6fc6 100644 --- a/src/components/Schedule/Appointments/AppointmentsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentsPage.tsx @@ -1,6 +1,15 @@ -import { CaretDownIcon, CheckIcon, ReloadIcon } from "@radix-ui/react-icons"; +import { CaretDownIcon, CheckIcon } from "@radix-ui/react-icons"; +import { PopoverClose } from "@radix-ui/react-popover"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { format, formatDate, isPast } from "date-fns"; +import { + format, + formatDate, + isPast, + isToday, + isTomorrow, + isYesterday, +} from "date-fns"; +import { Edit3Icon } from "lucide-react"; import { Link, navigate, useQueryParams } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -10,6 +19,7 @@ import { cn } from "@/lib/utils"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; import { Command, CommandEmpty, @@ -19,7 +29,6 @@ import { CommandList, CommandSeparator, } from "@/components/ui/command"; -import { DatePicker } from "@/components/ui/date-picker"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -182,48 +191,51 @@ export default function AppointmentsPage(props: { facilityId?: string }) { : t("no_results")} - - setQParams({ - practitioner: undefined, - slot: undefined, - }) - } - className="cursor-pointer" - > - {t("show_all")} - {qParams.practitioner === undefined && ( - - )} - - {resourcesQuery.data?.users.map((user) => ( + setQParams({ - practitioner: user.id, + practitioner: undefined, slot: undefined, }) } className="cursor-pointer" > -
- - {formatName(user)} - - {user.user_type} - -
- {qParams.practitioner === user.id && ( + {t("show_all")} + {qParams.practitioner === undefined && ( )}
+
+ {resourcesQuery.data?.users.map((user) => ( + + + setQParams({ + practitioner: user.id, + slot: undefined, + }) + } + className="cursor-pointer" + > +
+ + {formatName(user)} + + {user.user_type} + +
+ {qParams.practitioner === user.id && ( + + )} +
+
))}
@@ -233,12 +245,51 @@ export default function AppointmentsPage(props: { facilityId?: string }) {
- +
+ + + + + + + { + setQParams({ + date: dateQueryString(date), + slot: undefined, + }); + }} + initialFocus + /> + + +
setQParams({ search: e.target.value })} /> - + {/*
- + */} diff --git a/src/components/Schedule/ScheduleExceptionForm.tsx b/src/components/Schedule/ScheduleExceptionForm.tsx index 26fded409da..e48ae4f5b51 100644 --- a/src/components/Schedule/ScheduleExceptionForm.tsx +++ b/src/components/Schedule/ScheduleExceptionForm.tsx @@ -1,7 +1,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import * as z from "zod"; @@ -30,12 +31,9 @@ import { import { ScheduleAPIs } from "@/components/Schedule/api"; -import useSlug from "@/hooks/useSlug"; - import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; -import { UserBase } from "@/types/user/user"; const formSchema = z.object({ reason: z.string().min(1, "Reason is required"), @@ -53,13 +51,15 @@ const formSchema = z.object({ type FormValues = z.infer; interface Props { - onRefresh?: () => void; - user: UserBase; + facilityId: string; + userId: string; } -export default function ScheduleExceptionForm({ user, onRefresh }: Props) { +export default function ScheduleExceptionForm({ facilityId, userId }: Props) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); - const facilityId = useSlug("facility"); const form = useForm({ resolver: zodResolver(formSchema), @@ -73,14 +73,18 @@ export default function ScheduleExceptionForm({ user, onRefresh }: Props) { }, }); - const { - mutate: createException, - isPending, - isSuccess, - } = useMutation({ + const { mutate: createException, isPending } = useMutation({ mutationFn: mutate(ScheduleAPIs.exceptions.create, { pathParams: { facility_id: facilityId }, }), + onSuccess: () => { + toast.success(t("exception_created")); + setOpen(false); + form.reset(); + queryClient.invalidateQueries({ + queryKey: ["user-schedule-exceptions", { facilityId, userId }], + }); + }, }); const unavailableAllDay = form.watch("unavailable_all_day"); @@ -95,23 +99,14 @@ export default function ScheduleExceptionForm({ user, onRefresh }: Props) { } }, [unavailableAllDay]); - useEffect(() => { - if (isSuccess) { - toast.success("Exception created successfully"); - setOpen(false); - form.reset(); - onRefresh?.(); - } - }, [isSuccess]); - - async function onSubmit(data: FormValues) { + function onSubmit(data: FormValues) { createException({ reason: data.reason, valid_from: dateQueryString(data.valid_from), valid_to: dateQueryString(data.valid_to), start_time: data.start_time, end_time: data.end_time, - user: user.id, + user: userId, }); } diff --git a/src/components/Schedule/ScheduleExceptionsList.tsx b/src/components/Schedule/ScheduleExceptionsList.tsx index 5b9290a45bd..bbfe734bf84 100644 --- a/src/components/Schedule/ScheduleExceptionsList.tsx +++ b/src/components/Schedule/ScheduleExceptionsList.tsx @@ -14,16 +14,20 @@ import Loading from "@/components/Common/Loading"; import { ScheduleAPIs } from "@/components/Schedule/api"; import { ScheduleException } from "@/components/Schedule/types"; -import useSlug from "@/hooks/useSlug"; - import mutate from "@/Utils/request/mutate"; import { formatTimeShort } from "@/Utils/utils"; interface Props { items?: ScheduleException[]; + facilityId: string; + userId: string; } -export default function ScheduleExceptionsList({ items }: Props) { +export default function ScheduleExceptionsList({ + items, + facilityId, + userId, +}: Props) { const { t } = useTranslation(); if (items == null) { @@ -43,29 +47,37 @@ export default function ScheduleExceptionsList({ items }: Props) {
    {items.map((exception) => (
  • - +
  • ))}
); } -const ScheduleExceptionItem = (props: ScheduleException) => { +const ScheduleExceptionItem = ( + props: ScheduleException & { facilityId: string; userId: string }, +) => { const { t } = useTranslation(); - const facilityId = useSlug("facility"); const queryClient = useQueryClient(); const { mutate: deleteException, isPending } = useMutation({ mutationFn: mutate(ScheduleAPIs.exceptions.delete, { pathParams: { id: props.id, - facility_id: facilityId, + facility_id: props.facilityId, }, }), onSuccess: () => { toast.success(t("exception_deleted")); queryClient.invalidateQueries({ - queryKey: ["user-availability-exceptions", props.user], + queryKey: [ + "user-schedule-exceptions", + { facilityId: props.facilityId, userId: props.userId }, + ], }); }, }); diff --git a/src/components/Schedule/ScheduleTemplateForm.tsx b/src/components/Schedule/ScheduleTemplateForm.tsx index 570b618e21e..7e7d4bcd2f1 100644 --- a/src/components/Schedule/ScheduleTemplateForm.tsx +++ b/src/components/Schedule/ScheduleTemplateForm.tsx @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -41,12 +41,9 @@ import { } from "@/components/Schedule/helpers"; import { ScheduleSlotTypes } from "@/components/Schedule/types"; -import useSlug from "@/hooks/useSlug"; - import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; -import { UserBase } from "@/types/user/user"; const formSchema = z.object({ name: z.string().min(1, "Template name is required"), @@ -79,13 +76,14 @@ const formSchema = z.object({ }); interface Props { - onRefresh?: () => void; - user: UserBase; + facilityId: string; + userId: string; } -export default function ScheduleTemplateForm({ user, onRefresh }: Props) { - const facilityId = useSlug("facility"); +export default function ScheduleTemplateForm({ facilityId, userId }: Props) { const { t } = useTranslation(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); const form = useForm>({ @@ -109,31 +107,26 @@ export default function ScheduleTemplateForm({ user, onRefresh }: Props) { }, }); - const { - mutate: createTemplate, - isPending, - isSuccess, - } = useMutation({ + const { mutate: createTemplate, isPending } = useMutation({ mutationFn: mutate(ScheduleAPIs.templates.create, { pathParams: { facility_id: facilityId }, }), - }); - - useEffect(() => { - if (isSuccess) { + onSuccess: () => { toast.success("Schedule template created successfully"); setOpen(false); form.reset(); - onRefresh?.(); - } - }, [isSuccess]); + queryClient.invalidateQueries({ + queryKey: ["user-schedule-templates", { facilityId, userId }], + }); + }, + }); async function onSubmit(values: z.infer) { createTemplate({ valid_from: dateQueryString(values.valid_from), valid_to: dateQueryString(values.valid_to), name: values.name, - user: user.id as unknown as string, + user: userId, availabilities: values.availabilities.map((availability) => ({ name: availability.name, slot_type: availability.slot_type, @@ -180,9 +173,6 @@ export default function ScheduleTemplateForm({ user, onRefresh }: Props) { ); }; - console.log(form.formState.errors); - console.log(form.formState); - return ( diff --git a/src/components/Schedule/ScheduleTemplatesList.tsx b/src/components/Schedule/ScheduleTemplatesList.tsx index c6464d2c211..c2c45526f49 100644 --- a/src/components/Schedule/ScheduleTemplatesList.tsx +++ b/src/components/Schedule/ScheduleTemplatesList.tsx @@ -1,6 +1,10 @@ import { DotsHorizontalIcon } from "@radix-ui/react-icons"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { format, parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { cn } from "@/lib/utils"; import ColoredIndicator from "@/CAREUI/display/ColoredIndicator"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -14,6 +18,7 @@ import { } from "@/components/ui/dropdown-menu"; import Loading from "@/components/Common/Loading"; +import { ScheduleAPIs } from "@/components/Schedule/api"; import { getDaysOfWeekFromAvailabilities, getSlotsPerSession, @@ -24,11 +29,20 @@ import { } from "@/components/Schedule/types"; import { formatAvailabilityTime } from "@/components/Users/UserAvailabilityTab"; +import mutate from "@/Utils/request/mutate"; + interface Props { items?: ScheduleTemplate[]; + facilityId: string; + userId: string; } -export default function ScheduleTemplatesList({ items }: Props) { +export default function ScheduleTemplatesList({ + items, + facilityId, + userId, +}: Props) { + const { t } = useTranslation(); if (items == null) { return ; } @@ -37,7 +51,7 @@ export default function ScheduleTemplatesList({ items }: Props) { return (
-

No schedule templates found for this month.

+

{t("no_schedule_templates_found")}

); } @@ -46,17 +60,48 @@ export default function ScheduleTemplatesList({ items }: Props) {
    {items.map((template) => (
  • - +
  • ))}
); } -const ScheduleTemplateItem = (props: ScheduleTemplate) => { +const ScheduleTemplateItem = ( + props: ScheduleTemplate & { facilityId: string; userId: string }, +) => { const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { mutate: deleteTemplate, isPending } = useMutation({ + mutationFn: mutate(ScheduleAPIs.templates.delete, { + pathParams: { + facility_id: props.facilityId, + id: props.id, + }, + }), + onSuccess: () => { + toast.success(t("template_deleted")); + queryClient.invalidateQueries({ + queryKey: [ + "user-schedule-templates", + { facilityId: props.facilityId, userId: props.userId }, + ], + }); + }, + }); + return ( -
+
{ - Delete + deleteTemplate()}> + {t("delete")} +
diff --git a/src/components/Schedule/api.ts b/src/components/Schedule/api.ts index fd500ade191..dc6e56655d9 100644 --- a/src/components/Schedule/api.ts +++ b/src/components/Schedule/api.ts @@ -23,6 +23,7 @@ export const ScheduleAPIs = { delete: { path: "/api/v1/facility/{facility_id}/schedule/{id}/", method: "DELETE", + TBody: Type(), TRes: Type(), }, list: { @@ -94,5 +95,11 @@ export const ScheduleAPIs = { TBody: Type>>(), TRes: Type(), }, + cancel: { + path: "/api/v1/facility/{facility_id}/appointments/{id}/cancel/", + method: "POST", + TBody: Type<{ reason: "cancelled" | "entered_in_error" }>(), + TRes: Type(), + }, }, } as const; diff --git a/src/components/Users/UserAvailabilityTab.tsx b/src/components/Users/UserAvailabilityTab.tsx index e949cceb2bf..17c771639c9 100644 --- a/src/components/Users/UserAvailabilityTab.tsx +++ b/src/components/Users/UserAvailabilityTab.tsx @@ -1,6 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; +import { useState } from "react"; import { cn } from "@/lib/utils"; @@ -44,15 +43,8 @@ export default function UserAvailabilityTab({ userData: user }: Props) { const facilityId = useSlug("facility"); - // TODO: remove this once we have a way to get the facilityId - useEffect(() => { - if (!facilityId) { - toast.error("User needs to be linked to a home facility"); - } - }, [facilityId]); - const templatesQuery = useQuery({ - queryKey: ["user-availability-templates", user.username], + queryKey: ["user-schedule-templates", { facilityId, userId: user.id }], queryFn: query(ScheduleAPIs.templates.list, { pathParams: { facility_id: facilityId! }, queryParams: { user: user.id }, @@ -61,7 +53,7 @@ export default function UserAvailabilityTab({ userData: user }: Props) { }); const exceptionsQuery = useQuery({ - queryKey: ["user-availability-exceptions", user.username], + queryKey: ["user-schedule-exceptions", { facilityId, userId: user.id }], queryFn: query(ScheduleAPIs.exceptions.list, { pathParams: { facility_id: facilityId! }, queryParams: { user: user.id }, @@ -73,9 +65,9 @@ export default function UserAvailabilityTab({ userData: user }: Props) { } return ( -
+
{ @@ -149,7 +141,7 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
@@ -242,16 +234,10 @@ export default function UserAvailabilityTab({ userData: user }: Props) {
{view === "schedule" && ( - + )} {view === "exceptions" && ( - + )}
@@ -259,6 +245,8 @@ export default function UserAvailabilityTab({ userData: user }: Props) { {view === "schedule" && ( )} From aa10192d8228c2e1272abeeb5433e34f70469878 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 7 Jan 2025 19:12:55 +0530 Subject: [PATCH 03/10] clean up and move api and types to new folder --- public/locale/en.json | 1 + src/Utils/types.ts | 2 +- .../PatientDetailsTab/Appointments.tsx | 4 +- ...ntQuestion.tsx => AppointmentQuestion.tsx} | 20 +-- .../QuestionTypes/QuestionInput.tsx | 6 +- .../Questionnaire/structured/handlers.ts | 8 +- .../Questionnaire/structured/types.ts | 13 +- .../Appointments/AppointmentCreatePage.tsx | 8 +- .../Appointments/AppointmentDetailsPage.tsx | 20 ++- .../Appointments/AppointmentTokenCard.tsx | 2 +- .../Appointments/AppointmentsPage.tsx | 24 +-- src/components/Schedule/Appointments/utils.ts | 22 +-- .../Schedule/ScheduleExceptionForm.tsx | 5 +- .../Schedule/ScheduleExceptionsList.tsx | 6 +- .../Schedule/ScheduleTemplateForm.tsx | 45 ++--- .../Schedule/ScheduleTemplatesList.tsx | 11 +- src/components/Schedule/helpers.ts | 7 +- src/components/Schedule/types.ts | 115 ------------- src/components/Users/UserAvailabilityTab.tsx | 10 +- src/pages/Appoinments/PatientRegistration.tsx | 14 +- src/pages/Appoinments/PatientSelect.tsx | 14 +- src/pages/Appoinments/Schedule.tsx | 6 +- src/pages/Patient/Utils.tsx | 1 + src/pages/Patient/index.tsx | 2 +- src/types/questionnaire/form.ts | 7 +- src/types/questionnaire/question.ts | 2 +- src/types/scheduling/PublicAppointmentApi.ts | 13 +- src/types/scheduling/schedule.ts | 156 ++++++++++++++++++ .../scheduling/scheduleApis.ts} | 109 +++++++----- src/types/user/userApi.ts | 3 +- 30 files changed, 362 insertions(+), 294 deletions(-) rename src/components/Questionnaire/QuestionTypes/{FollowUpAppointmentQuestion.tsx => AppointmentQuestion.tsx} (91%) delete mode 100644 src/components/Schedule/types.ts create mode 100644 src/types/scheduling/schedule.ts rename src/{components/Schedule/api.ts => types/scheduling/scheduleApis.ts} (56%) diff --git a/public/locale/en.json b/public/locale/en.json index e8c332308d2..94fb25b7957 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1398,6 +1398,7 @@ "patient_update_error": "Could not update patient", "patient_update_success": "Patient Updated Sucessfully", "patients": "Patients", + "patients_per_slot": "Patients per Slot", "pending": "Pending", "permanent_address": "Permanent Address", "permission_denied": "You do not have permission to perform this action", diff --git a/src/Utils/types.ts b/src/Utils/types.ts index 22da8867b61..cdee60e1f2f 100644 --- a/src/Utils/types.ts +++ b/src/Utils/types.ts @@ -48,4 +48,4 @@ export type WritableOnly = T extends object type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B; -export type Time = `${number}:${number}`; +export type Time = `${number}:${number}` | `${number}:${number}:${number}`; diff --git a/src/components/Patient/PatientDetailsTab/Appointments.tsx b/src/components/Patient/PatientDetailsTab/Appointments.tsx index 70c0f6f081e..cf1da394da3 100644 --- a/src/components/Patient/PatientDetailsTab/Appointments.tsx +++ b/src/components/Patient/PatientDetailsTab/Appointments.tsx @@ -17,10 +17,10 @@ import { import { Avatar } from "@/components/Common/Avatar"; import { PatientProps } from "@/components/Patient/PatientDetailsTab"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import query from "@/Utils/request/query"; import { formatDateTime, formatName } from "@/Utils/utils"; +import scheduleApis from "@/types/scheduling/scheduleApis"; export const Appointments = (props: PatientProps) => { const { patientData, facilityId, id } = props; @@ -28,7 +28,7 @@ export const Appointments = (props: PatientProps) => { const { data } = useQuery({ queryKey: ["patient-appointments", id], - queryFn: query(ScheduleAPIs.appointments.list, { + queryFn: query(scheduleApis.appointments.list, { pathParams: { facility_id: facilityId }, queryParams: { patient: id, limit: 100 }, }), diff --git a/src/components/Questionnaire/QuestionTypes/FollowUpAppointmentQuestion.tsx b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx similarity index 91% rename from src/components/Questionnaire/QuestionTypes/FollowUpAppointmentQuestion.tsx rename to src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx index 098c4f71151..4add6a49a7b 100644 --- a/src/components/Questionnaire/QuestionTypes/FollowUpAppointmentQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/AppointmentQuestion.tsx @@ -16,8 +16,6 @@ import { Textarea } from "@/components/ui/textarea"; import { Avatar } from "@/components/Common/Avatar"; import { groupSlotsByAvailability } from "@/components/Schedule/Appointments/utils"; -import { ScheduleAPIs } from "@/components/Schedule/api"; -import { FollowUpAppointmentRequest } from "@/components/Schedule/types"; import useSlug from "@/hooks/useSlug"; @@ -28,6 +26,8 @@ import { ResponseValue, } from "@/types/questionnaire/form"; import { Question } from "@/types/questionnaire/question"; +import { CreateAppointmentQuestion } from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; import { UserBase } from "@/types/user/user"; interface FollowUpVisitQuestionProps { @@ -37,7 +37,7 @@ interface FollowUpVisitQuestionProps { disabled?: boolean; } -export function FollowUpAppointmentQuestion({ +export function AppointmentQuestion({ question, questionnaireResponse, updateQuestionnaireResponseCB, @@ -49,18 +49,18 @@ export function FollowUpAppointmentQuestion({ const values = (questionnaireResponse.values?.[0] - ?.value as unknown as FollowUpAppointmentRequest[]) || []; + ?.value as unknown as CreateAppointmentQuestion[]) || []; const value = values[0] ?? {}; - const handleUpdate = (updates: Partial) => { - const followUpAppointment = { ...value, ...updates }; + const handleUpdate = (updates: Partial) => { + const appointment = { ...value, ...updates }; updateQuestionnaireResponseCB({ ...questionnaireResponse, values: [ { - type: "follow_up_appointment", - value: [followUpAppointment] as unknown as ResponseValue["value"], + type: "appointment", + value: [appointment] as unknown as ResponseValue["value"], }, ], }); @@ -70,7 +70,7 @@ export function FollowUpAppointmentQuestion({ const resourcesQuery = useQuery({ queryKey: ["availableResources", facilityId], - queryFn: query(ScheduleAPIs.appointments.availableUsers, { + queryFn: query(scheduleApis.appointments.availableUsers, { pathParams: { facility_id: facilityId }, }), }); @@ -82,7 +82,7 @@ export function FollowUpAppointmentQuestion({ resource?.id, dateQueryString(selectedDate), ], - queryFn: query(ScheduleAPIs.slots.getSlotsForDay, { + queryFn: query(scheduleApis.slots.getSlotsForDay, { pathParams: { facility_id: facilityId }, body: { user: resource?.id, diff --git a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx index e0c4b3ed631..9c2ea890b29 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionInput.tsx @@ -2,7 +2,7 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; -import { FollowUpAppointmentQuestion } from "@/components/Questionnaire/QuestionTypes/FollowUpAppointmentQuestion"; +import { AppointmentQuestion } from "@/components/Questionnaire/QuestionTypes/AppointmentQuestion"; import { QuestionValidationError } from "@/types/questionnaire/batch"; import type { @@ -163,8 +163,8 @@ export function QuestionInput({ return ; case "diagnosis": return ; - case "follow_up_appointment": - return ; + case "appointment": + return ; case "encounter": if (encounterId) { return ( diff --git a/src/components/Questionnaire/structured/handlers.ts b/src/components/Questionnaire/structured/handlers.ts index 380860be3e6..a36cf779878 100644 --- a/src/components/Questionnaire/structured/handlers.ts +++ b/src/components/Questionnaire/structured/handlers.ts @@ -153,9 +153,9 @@ const handlers: { }); }, }, - follow_up_appointment: { - getRequests: (followUpAppointment, { facilityId, patientId }) => { - const { reason_for_visit, slot_id } = followUpAppointment[0]; + appointment: { + getRequests: (appointment, { facilityId, patientId }) => { + const { reason_for_visit, slot_id } = appointment[0]; return [ { url: `/api/v1/facility/${facilityId}/slots/${slot_id}/create_appointment/`, @@ -164,7 +164,7 @@ const handlers: { reason_for_visit, patient: patientId, }, - reference_id: "follow_up_appointment", + reference_id: "appointment", }, ]; }, diff --git a/src/components/Questionnaire/structured/types.ts b/src/components/Questionnaire/structured/types.ts index 6995831cd6c..a72debf1990 100644 --- a/src/components/Questionnaire/structured/types.ts +++ b/src/components/Questionnaire/structured/types.ts @@ -1,8 +1,3 @@ -import { - AppointmentCreate, - FollowUpAppointmentRequest, -} from "@/components/Schedule/types"; - import { AllergyIntolerance, AllergyIntoleranceRequest, @@ -13,6 +8,10 @@ import { MedicationRequest } from "@/types/emr/medicationRequest"; import { MedicationStatement } from "@/types/emr/medicationStatement"; import { Symptom, SymptomRequest } from "@/types/emr/symptom/symptom"; import { StructuredQuestionType } from "@/types/questionnaire/question"; +import { + AppointmentCreateRequest, + CreateAppointmentQuestion, +} from "@/types/scheduling/schedule"; // Map structured types to their data types export interface StructuredDataMap { @@ -22,7 +21,7 @@ export interface StructuredDataMap { symptom: Symptom; diagnosis: Diagnosis; encounter: Encounter; - follow_up_appointment: FollowUpAppointmentRequest; + appointment: CreateAppointmentQuestion; } // Map structured types to their request types @@ -33,7 +32,7 @@ export interface StructuredRequestMap { symptom: SymptomRequest; diagnosis: DiagnosisRequest; encounter: EncounterEditRequest; - follow_up_appointment: AppointmentCreate; + appointment: AppointmentCreateRequest; } export type RequestTypeFor = diff --git a/src/components/Schedule/Appointments/AppointmentCreatePage.tsx b/src/components/Schedule/Appointments/AppointmentCreatePage.tsx index 772e7b90361..4d29327d26d 100644 --- a/src/components/Schedule/Appointments/AppointmentCreatePage.tsx +++ b/src/components/Schedule/Appointments/AppointmentCreatePage.tsx @@ -28,13 +28,13 @@ import { groupSlotsByAvailability, useAvailabilityHeatmap, } from "@/components/Schedule/Appointments/utils"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import useAppHistory from "@/hooks/useAppHistory"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { dateQueryString, formatDisplayName, formatName } from "@/Utils/utils"; +import scheduleApis from "@/types/scheduling/scheduleApis"; interface Props { facilityId: string; @@ -54,7 +54,7 @@ export default function AppointmentCreatePage(props: Props) { const resourcesQuery = useQuery({ queryKey: ["availableResources", props.facilityId], - queryFn: query(ScheduleAPIs.appointments.availableUsers, { + queryFn: query(scheduleApis.appointments.availableUsers, { pathParams: { facility_id: props.facilityId, }, @@ -75,7 +75,7 @@ export default function AppointmentCreatePage(props: Props) { resourceId, dateQueryString(selectedDate), ], - queryFn: query(ScheduleAPIs.slots.getSlotsForDay, { + queryFn: query(scheduleApis.slots.getSlotsForDay, { pathParams: { facility_id: props.facilityId }, body: { user: resourceId, @@ -86,7 +86,7 @@ export default function AppointmentCreatePage(props: Props) { }); const { mutateAsync: createAppointment } = useMutation({ - mutationFn: mutate(ScheduleAPIs.slots.createAppointment, { + mutationFn: mutate(scheduleApis.slots.createAppointment, { pathParams: { facility_id: props.facilityId, slot_id: selectedSlotId ?? "", diff --git a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx index 65463bb8a28..078dc5ffccb 100644 --- a/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentDetailsPage.tsx @@ -40,8 +40,6 @@ import { formatAppointmentSlotTime, printAppointment, } from "@/components/Schedule/Appointments/utils"; -import { ScheduleAPIs } from "@/components/Schedule/api"; -import { Appointment, AppointmentStatuses } from "@/components/Schedule/types"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; @@ -51,6 +49,12 @@ import { getReadableDuration, saveElementAsImage, } from "@/Utils/utils"; +import { + Appointment, + AppointmentStatuses, + AppointmentUpdateRequest, +} from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; interface Props { facilityId: string; @@ -72,7 +76,7 @@ export default function AppointmentDetailsPage(props: Props) { const appointmentQuery = useQuery({ queryKey: ["appointment", props.appointmentId], - queryFn: query(ScheduleAPIs.appointments.retrieve, { + queryFn: query(scheduleApis.appointments.retrieve, { pathParams: { facility_id: props.facilityId, id: props.appointmentId, @@ -93,9 +97,9 @@ export default function AppointmentDetailsPage(props: Props) { const { mutate: updateAppointment, isPending } = useMutation< Appointment, unknown, - { status: Appointment["status"] } + AppointmentUpdateRequest >({ - mutationFn: mutate(ScheduleAPIs.appointments.update, { + mutationFn: mutate(scheduleApis.appointments.update, { pathParams: { facility_id: props.facilityId, id: props.appointmentId, @@ -215,7 +219,9 @@ const AppointmentDetails = ({ entered_in_error: "destructive", cancelled: "destructive", noshow: "destructive", - } as Record + } as Partial< + Record + > )[appointment.status] ?? "outline" } > @@ -375,7 +381,7 @@ const AppointmentActions = ({ const isToday = isSameDay(appointment.token_slot.start_datetime, new Date()); const { mutate: cancelAppointment } = useMutation({ - mutationFn: mutate(ScheduleAPIs.appointments.cancel, { + mutationFn: mutate(scheduleApis.appointments.cancel, { pathParams: { facility_id: facilityId, id: appointment.id, diff --git a/src/components/Schedule/Appointments/AppointmentTokenCard.tsx b/src/components/Schedule/Appointments/AppointmentTokenCard.tsx index 338f37fa9e5..dbf07cd8182 100644 --- a/src/components/Schedule/Appointments/AppointmentTokenCard.tsx +++ b/src/components/Schedule/Appointments/AppointmentTokenCard.tsx @@ -7,9 +7,9 @@ import { Label } from "@/components/ui/label"; import { FacilityModel } from "@/components/Facility/models"; import { formatAppointmentSlotTime } from "@/components/Schedule/Appointments/utils"; import { getFakeTokenNumber } from "@/components/Schedule/helpers"; -import { Appointment } from "@/components/Schedule/types"; import { formatName, formatPatientAge } from "@/Utils/utils"; +import { Appointment } from "@/types/scheduling/schedule"; interface Props { id?: string; diff --git a/src/components/Schedule/Appointments/AppointmentsPage.tsx b/src/components/Schedule/Appointments/AppointmentsPage.tsx index 3ecd7ee6fc6..26272f7f485 100644 --- a/src/components/Schedule/Appointments/AppointmentsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentsPage.tsx @@ -60,13 +60,7 @@ import { formatSlotTimeRange, groupSlotsByAvailability, } from "@/components/Schedule/Appointments/utils"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import { getFakeTokenNumber } from "@/components/Schedule/helpers"; -import { - Appointment, - AppointmentStatuses, - SlotAvailability, -} from "@/components/Schedule/types"; import useAuthUser from "@/hooks/useAuthUser"; @@ -79,6 +73,12 @@ import { formatName, formatPatientAge, } from "@/Utils/utils"; +import { + Appointment, + AppointmentStatuses, + TokenSlot, +} from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; interface QueryParams { practitioner?: string; @@ -105,7 +105,7 @@ export default function AppointmentsPage(props: { facilityId?: string }) { const resourcesQuery = useQuery({ queryKey: ["appointments-resources", facilityId], - queryFn: query(ScheduleAPIs.appointments.availableUsers, { + queryFn: query(scheduleApis.appointments.availableUsers, { pathParams: { facility_id: facilityId }, }), }); @@ -115,7 +115,7 @@ export default function AppointmentsPage(props: { facilityId?: string }) { const slotsQuery = useQuery({ queryKey: ["slots", facilityId, qParams.practitioner, date], - queryFn: query(ScheduleAPIs.slots.getSlotsForDay, { + queryFn: query(scheduleApis.slots.getSlotsForDay, { pathParams: { facility_id: facilityId }, body: { user: qParams.practitioner ?? "", @@ -400,7 +400,7 @@ function AppointmentColumn(props: { props.slot, props.date, ], - queryFn: query(ScheduleAPIs.appointments.list, { + queryFn: query(scheduleApis.appointments.list, { pathParams: { facility_id: props.facilityId }, queryParams: { status: props.status, @@ -506,7 +506,7 @@ function AppointmentRow(props: { props.slot, props.date, ], - queryFn: query(ScheduleAPIs.appointments.list, { + queryFn: query(scheduleApis.appointments.list, { pathParams: { facility_id: props.facilityId }, queryParams: { status: status, @@ -665,7 +665,7 @@ const AppointmentStatusDropdown = ({ const hasStarted = isPast(appointment.token_slot.start_datetime); const { mutate: updateAppointment } = useMutation({ - mutationFn: mutate(ScheduleAPIs.appointments.update, { + mutationFn: mutate(scheduleApis.appointments.update, { pathParams: { facility_id: facilityId, id: appointment.id, @@ -733,7 +733,7 @@ const AppointmentStatusDropdown = ({ }; interface SlotFilterProps { - slots: SlotAvailability[]; + slots: TokenSlot[]; disableInline?: boolean; disabled?: boolean; selectedSlot: string | undefined; diff --git a/src/components/Schedule/Appointments/utils.ts b/src/components/Schedule/Appointments/utils.ts index 6460b568391..6456b6cb451 100644 --- a/src/components/Schedule/Appointments/utils.ts +++ b/src/components/Schedule/Appointments/utils.ts @@ -11,13 +11,7 @@ import { TFunction } from "i18next"; import { toast } from "sonner"; import { FacilityModel } from "@/components/Facility/models"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import { getFakeTokenNumber } from "@/components/Schedule/helpers"; -import { - Appointment, - AvailabilityHeatmap, - SlotAvailability, -} from "@/components/Schedule/types"; import query from "@/Utils/request/query"; import { @@ -26,11 +20,17 @@ import { formatPatientAge, getMonthStartAndEnd, } from "@/Utils/utils"; +import { + Appointment, + AvailabilityHeatmapResponse, + TokenSlot, +} from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; -export const groupSlotsByAvailability = (slots: SlotAvailability[]) => { +export const groupSlotsByAvailability = (slots: TokenSlot[]) => { const result: { - availability: SlotAvailability["availability"]; - slots: Omit[]; + availability: TokenSlot["availability"]; + slots: Omit[]; }[] = []; for (const slot of slots) { @@ -76,7 +76,7 @@ export const useAvailabilityHeatmap = ({ const fromDate = dateQueryString(max([start, startOfToday()])); const toDate = dateQueryString(end); - let queryFn = query(ScheduleAPIs.slots.availabilityHeatmap, { + let queryFn = query(scheduleApis.slots.availabilityStats, { pathParams: { facility_id: facilityId }, body: { user: userId, @@ -105,7 +105,7 @@ const getInfiniteAvailabilityHeatmap = ({ }) => { const dates = eachDayOfInterval({ start: fromDate, end: toDate }); - const result: AvailabilityHeatmap = {}; + const result: AvailabilityHeatmapResponse = {}; for (const date of dates) { result[dateQueryString(date)] = { total_slots: Infinity, booked_slots: 0 }; diff --git a/src/components/Schedule/ScheduleExceptionForm.tsx b/src/components/Schedule/ScheduleExceptionForm.tsx index e48ae4f5b51..2aa00d7a01c 100644 --- a/src/components/Schedule/ScheduleExceptionForm.tsx +++ b/src/components/Schedule/ScheduleExceptionForm.tsx @@ -29,11 +29,10 @@ import { SheetTrigger, } from "@/components/ui/sheet"; -import { ScheduleAPIs } from "@/components/Schedule/api"; - import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; +import scheduleApis from "@/types/scheduling/scheduleApis"; const formSchema = z.object({ reason: z.string().min(1, "Reason is required"), @@ -74,7 +73,7 @@ export default function ScheduleExceptionForm({ facilityId, userId }: Props) { }); const { mutate: createException, isPending } = useMutation({ - mutationFn: mutate(ScheduleAPIs.exceptions.create, { + mutationFn: mutate(scheduleApis.exceptions.create, { pathParams: { facility_id: facilityId }, }), onSuccess: () => { diff --git a/src/components/Schedule/ScheduleExceptionsList.tsx b/src/components/Schedule/ScheduleExceptionsList.tsx index bbfe734bf84..9e66bddc9dc 100644 --- a/src/components/Schedule/ScheduleExceptionsList.tsx +++ b/src/components/Schedule/ScheduleExceptionsList.tsx @@ -11,11 +11,11 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import Loading from "@/components/Common/Loading"; -import { ScheduleAPIs } from "@/components/Schedule/api"; -import { ScheduleException } from "@/components/Schedule/types"; import mutate from "@/Utils/request/mutate"; import { formatTimeShort } from "@/Utils/utils"; +import { ScheduleException } from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; interface Props { items?: ScheduleException[]; @@ -65,7 +65,7 @@ const ScheduleExceptionItem = ( const queryClient = useQueryClient(); const { mutate: deleteException, isPending } = useMutation({ - mutationFn: mutate(ScheduleAPIs.exceptions.delete, { + mutationFn: mutate(scheduleApis.exceptions.delete, { pathParams: { id: props.id, facility_id: props.facilityId, diff --git a/src/components/Schedule/ScheduleTemplateForm.tsx b/src/components/Schedule/ScheduleTemplateForm.tsx index 7e7d4bcd2f1..cc1825e630e 100644 --- a/src/components/Schedule/ScheduleTemplateForm.tsx +++ b/src/components/Schedule/ScheduleTemplateForm.tsx @@ -34,16 +34,15 @@ import { } from "@/components/ui/sheet"; import { Textarea } from "@/components/ui/textarea"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import { getSlotsPerSession, getTokenDuration, } from "@/components/Schedule/helpers"; -import { ScheduleSlotTypes } from "@/components/Schedule/types"; import mutate from "@/Utils/request/mutate"; import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; +import scheduleApis from "@/types/scheduling/scheduleApis"; const formSchema = z.object({ name: z.string().min(1, "Template name is required"), @@ -60,7 +59,7 @@ const formSchema = z.object({ .array( z.object({ name: z.string().min(1, "Session name is required"), - slot_type: z.enum(ScheduleSlotTypes), + slot_type: z.enum(["appointment", "open", "closed"]), reason: z.string(), start_time: z .string() @@ -108,7 +107,7 @@ export default function ScheduleTemplateForm({ facilityId, userId }: Props) { }); const { mutate: createTemplate, isPending } = useMutation({ - mutationFn: mutate(ScheduleAPIs.templates.create, { + mutationFn: mutate(scheduleApis.templates.create, { pathParams: { facility_id: facilityId }, }), onSuccess: () => { @@ -323,23 +322,25 @@ export default function ScheduleTemplateForm({ facilityId, userId }: Props) { defaultValue={field.value} className="flex space-x-4" > - {ScheduleSlotTypes.map((type) => ( -
- -
+ ), + )} @@ -403,7 +404,9 @@ export default function ScheduleTemplateForm({ facilityId, userId }: Props) { name={`availabilities.${index}.tokens_per_slot`} render={({ field }) => ( - Tokens per Slot + + {t("patients_per_slot")} + {/* TODO: Temp. hack since backend is giving slot_type as number in Response */} { - ScheduleSlotTypes[ + ["open", "appointment", "closed"][ (slot.slot_type as unknown as number) - 1 ] } diff --git a/src/components/Schedule/helpers.ts b/src/components/Schedule/helpers.ts index 5a5d2014851..ec2868c8fec 100644 --- a/src/components/Schedule/helpers.ts +++ b/src/components/Schedule/helpers.ts @@ -1,8 +1,7 @@ import { isSameDay, isWithinInterval } from "date-fns"; -import { Appointment, ScheduleTemplate } from "@/components/Schedule/types"; - import { Time } from "@/Utils/types"; +import { Appointment, ScheduleAvailability } from "@/types/scheduling/schedule"; export const isDateInRange = ( date: Date, @@ -51,7 +50,7 @@ export function getTokenDuration( } export const getDaysOfWeekFromAvailabilities = ( - availabilities: ScheduleTemplate["availabilities"], + availabilities: ScheduleAvailability[], ) => { return [ ...new Set( @@ -63,7 +62,7 @@ export const getDaysOfWeekFromAvailabilities = ( }; export const filterAvailabilitiesByDayOfWeek = ( - availabilities: ScheduleTemplate["availabilities"], + availabilities: ScheduleAvailability[], date?: Date, ) => { // Doing this weird things because backend uses python's 0-6. diff --git a/src/components/Schedule/types.ts b/src/components/Schedule/types.ts deleted file mode 100644 index 725be79979e..00000000000 --- a/src/components/Schedule/types.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { DayOfWeekValue } from "@/CAREUI/interactive/WeekdayCheckbox"; - -import { Time } from "@/Utils/types"; -import { UserBase } from "@/types/user/user"; - -export interface ScheduleTemplate { - readonly id: string; - user: string; - name: string; - valid_from: string; - valid_to: string; - availabilities: { - readonly id: string; - name: string; - slot_type: "appointment" | "open" | "closed"; - slot_size_in_minutes: number; - tokens_per_slot: number; - readonly create_tokens: boolean; - reason: string; - availability: { - day_of_week: DayOfWeekValue; - start_time: Time; - end_time: Time; - }[]; - }[]; - readonly create_by: UserBase; - readonly updated_by: UserBase; -} - -export const ScheduleSlotTypes = ["open", "appointment", "closed"] as const; - -export type ScheduleAvailability = ScheduleTemplate["availabilities"][number]; - -export interface ScheduleException { - readonly id: string; - user: string; // UUID of user - reason: string; - valid_from: string; // date in YYYY-MM-DD format - valid_to: string; // date in YYYY-MM-DD format - start_time: Time; // time in HH:MM format - end_time: Time; // time in HH:MM format -} - -export interface SlotAvailability { - readonly id: string; - readonly availability: { - readonly name: string; - readonly tokens_per_slot: number; - }; - readonly start_datetime: string; - readonly end_datetime: string; - readonly allocated: number; -} - -export interface AppointmentCreate { - patient: string; - reason_for_visit: string; -} - -interface AppointmentPatient { - readonly id: string; - readonly name: string; - readonly gender: number; - readonly phone_number: string; - readonly emergency_phone_number: string; - readonly address: string; - readonly pincode: string; - readonly state: string | null; - readonly district: string | null; - readonly local_body: string | null; - readonly ward: string | null; - readonly date_of_birth: string | null; - readonly year_of_birth: string | null; -} - -export const AppointmentStatuses = [ - "proposed", - "pending", - "booked", - "arrived", - "fulfilled", - "cancelled", - "noshow", - "entered_in_error", - "checked_in", - "waitlist", - "in_consultation", -] as const; - -export interface Appointment { - readonly id: string; - readonly token_slot: SlotAvailability; - readonly patient: AppointmentPatient; - readonly booked_on: string; - /** - * This is null if the appointment was booked by the patient itself. - */ - readonly booked_by: UserBase | null; - status: (typeof AppointmentStatuses)[number]; - readonly reason_for_visit: string; - readonly user: UserBase; -} - -export interface AvailabilityHeatmap { - [date: string]: { total_slots: number; booked_slots: number }; -} - -export interface CancelAppointmentRequest { - reason: "cancelled" | "entered_in_error"; -} - -export interface FollowUpAppointmentRequest { - reason_for_visit: string; - slot_id: string; -} diff --git a/src/components/Users/UserAvailabilityTab.tsx b/src/components/Users/UserAvailabilityTab.tsx index 17c771639c9..972358b5c03 100644 --- a/src/components/Users/UserAvailabilityTab.tsx +++ b/src/components/Users/UserAvailabilityTab.tsx @@ -19,18 +19,18 @@ import ScheduleExceptionForm from "@/components/Schedule/ScheduleExceptionForm"; import ScheduleExceptionsList from "@/components/Schedule/ScheduleExceptionsList"; import ScheduleTemplateForm from "@/components/Schedule/ScheduleTemplateForm"; import ScheduleTemplatesList from "@/components/Schedule/ScheduleTemplatesList"; -import { ScheduleAPIs } from "@/components/Schedule/api"; import { filterAvailabilitiesByDayOfWeek, getSlotsPerSession, isDateInRange, } from "@/components/Schedule/helpers"; -import { ScheduleAvailability } from "@/components/Schedule/types"; import useSlug from "@/hooks/useSlug"; import query from "@/Utils/request/query"; import { formatTimeShort } from "@/Utils/utils"; +import { AvailabilityDateTime } from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; import { UserBase } from "@/types/user/user"; type Props = { @@ -45,7 +45,7 @@ export default function UserAvailabilityTab({ userData: user }: Props) { const templatesQuery = useQuery({ queryKey: ["user-schedule-templates", { facilityId, userId: user.id }], - queryFn: query(ScheduleAPIs.templates.list, { + queryFn: query(scheduleApis.templates.list, { pathParams: { facility_id: facilityId! }, queryParams: { user: user.id }, }), @@ -54,7 +54,7 @@ export default function UserAvailabilityTab({ userData: user }: Props) { const exceptionsQuery = useQuery({ queryKey: ["user-schedule-exceptions", { facilityId, userId: user.id }], - queryFn: query(ScheduleAPIs.exceptions.list, { + queryFn: query(scheduleApis.exceptions.list, { pathParams: { facility_id: facilityId! }, queryParams: { user: user.id }, }), @@ -287,7 +287,7 @@ const diagonalStripes = { // TODO: remove this in favour of supporting flexible day of week availability export const formatAvailabilityTime = ( - availability: ScheduleAvailability["availability"], + availability: AvailabilityDateTime[], ) => { const startTime = availability[0].start_time; const endTime = availability[0].end_time; diff --git a/src/pages/Appoinments/PatientRegistration.tsx b/src/pages/Appoinments/PatientRegistration.tsx index 0e4ada8b57f..0fa833e32b9 100644 --- a/src/pages/Appoinments/PatientRegistration.tsx +++ b/src/pages/Appoinments/PatientRegistration.tsx @@ -23,11 +23,6 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Textarea } from "@/components/ui/textarea"; import DateFormField from "@/components/Form/FormFields/DateFormField"; -import { - Appointment, - AppointmentCreate, - SlotAvailability, -} from "@/components/Schedule/types"; import { CarePatientTokenKey, GENDER_TYPES } from "@/common/constants"; import { validateName, validatePincode } from "@/common/validation"; @@ -44,6 +39,11 @@ import { } from "@/pages/Patient/Utils"; import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; +import { + Appointment, + AppointmentCreateRequest, + TokenSlot, +} from "@/types/scheduling/schedule"; import OrganizationSelector from "../Organization/components/OrganizationSelector"; @@ -70,7 +70,7 @@ export function PatientRegistration(props: PatientRegistrationProps) { const { staffId } = props; const selectedSlot = JSON.parse( localStorage.getItem("selectedSlot") ?? "", - ) as SlotAvailability; + ) as TokenSlot; const reason = localStorage.getItem("reason"); const tokenData: TokenData = JSON.parse( localStorage.getItem(CarePatientTokenKey) || "{}", @@ -140,7 +140,7 @@ export function PatientRegistration(props: PatientRegistrationProps) { }); const { mutate: createAppointment } = useMutation({ - mutationFn: (body: AppointmentCreate) => + mutationFn: (body: AppointmentCreateRequest) => mutate(PublicAppointmentApi.createAppointment, { pathParams: { id: selectedSlot?.id }, body, diff --git a/src/pages/Appoinments/PatientSelect.tsx b/src/pages/Appoinments/PatientSelect.tsx index 1dd8d75f626..f5fa9c0b291 100644 --- a/src/pages/Appoinments/PatientSelect.tsx +++ b/src/pages/Appoinments/PatientSelect.tsx @@ -9,11 +9,6 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import Loading from "@/components/Common/Loading"; -import { - Appointment, - AppointmentCreate, - SlotAvailability, -} from "@/components/Schedule/types"; import { CarePatientTokenKey } from "@/common/constants"; @@ -25,6 +20,11 @@ import { PaginatedResponse } from "@/Utils/request/types"; import { AppointmentPatient } from "@/pages/Patient/Utils"; import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; +import { + Appointment, + AppointmentCreateRequest, + TokenSlot, +} from "@/types/scheduling/schedule"; export default function PatientSelect({ facilityId, @@ -36,7 +36,7 @@ export default function PatientSelect({ const { t } = useTranslation(); const selectedSlot = JSON.parse( localStorage.getItem("selectedSlot") ?? "", - ) as SlotAvailability; + ) as TokenSlot; const reason = localStorage.getItem("reason"); const tokenData: TokenData = JSON.parse( localStorage.getItem(CarePatientTokenKey) || "{}", @@ -72,7 +72,7 @@ export default function PatientSelect({ }); const { mutate: createAppointment } = useMutation({ - mutationFn: (body: AppointmentCreate) => + mutationFn: (body: AppointmentCreateRequest) => mutate(PublicAppointmentApi.createAppointment, { pathParams: { id: selectedSlot?.id }, body, diff --git a/src/pages/Appoinments/Schedule.tsx b/src/pages/Appoinments/Schedule.tsx index e31b63a71a8..2eeefbc5f05 100644 --- a/src/pages/Appoinments/Schedule.tsx +++ b/src/pages/Appoinments/Schedule.tsx @@ -18,7 +18,6 @@ import { Avatar } from "@/components/Common/Avatar"; import Loading from "@/components/Common/Loading"; import { FacilityModel } from "@/components/Facility/models"; import { groupSlotsByAvailability } from "@/components/Schedule/Appointments/utils"; -import { SlotAvailability } from "@/components/Schedule/types"; import { CarePatientTokenKey } from "@/common/constants"; @@ -30,6 +29,7 @@ import { RequestResult } from "@/Utils/request/types"; import { dateQueryString } from "@/Utils/utils"; import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; +import { TokenSlot } from "@/types/scheduling/schedule"; interface AppointmentsProps { facilityId: string; @@ -41,7 +41,7 @@ export function ScheduleAppointment(props: AppointmentsProps) { const { facilityId, staffId } = props; const [selectedMonth, setSelectedMonth] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedSlot, setSelectedSlot] = useState(); + const [selectedSlot, setSelectedSlot] = useState(); const [reason, setReason] = useState(""); const tokenData: TokenData = JSON.parse( @@ -83,7 +83,7 @@ export function ScheduleAppointment(props: AppointmentsProps) { Notification.Error({ msg: "Error while fetching user data" }); } - const slotsQuery = useQuery<{ results: SlotAvailability[] }>({ + const slotsQuery = useQuery<{ results: TokenSlot[] }>({ queryKey: ["slots", facilityId, staffId, selectedDate], queryFn: query(PublicAppointmentApi.getSlotsForDay, { body: { diff --git a/src/pages/Patient/Utils.tsx b/src/pages/Patient/Utils.tsx index f660f0ec856..68f3ae524a3 100644 --- a/src/pages/Patient/Utils.tsx +++ b/src/pages/Patient/Utils.tsx @@ -16,6 +16,7 @@ export type AppointmentPatient = { external_id: string; name: string; phone_number: string; + emergency_phone_number: string; address: string; date_of_birth?: string; year_of_birth?: string; diff --git a/src/pages/Patient/index.tsx b/src/pages/Patient/index.tsx index 083febc7f10..8246d88796d 100644 --- a/src/pages/Patient/index.tsx +++ b/src/pages/Patient/index.tsx @@ -19,13 +19,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Loading from "@/components/Common/Loading"; import { formatAppointmentSlotTime } from "@/components/Schedule/Appointments/utils"; -import { Appointment } from "@/components/Schedule/types"; import { usePatientContext } from "@/hooks/useAuthOrPatientUser"; import query from "@/Utils/request/query"; import { formatName, formatPatientAge } from "@/Utils/utils"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; +import { Appointment } from "@/types/scheduling/schedule"; function PatientIndex() { const { t } = useTranslation(); diff --git a/src/types/questionnaire/form.ts b/src/types/questionnaire/form.ts index eedd90c49b2..1e6ed093ecf 100644 --- a/src/types/questionnaire/form.ts +++ b/src/types/questionnaire/form.ts @@ -1,5 +1,3 @@ -import { FollowUpAppointmentRequest } from "@/components/Schedule/types"; - import { AllergyIntolerance } from "@/types/emr/allergyIntolerance/allergyIntolerance"; import { Diagnosis } from "@/types/emr/diagnosis/diagnosis"; import { Encounter } from "@/types/emr/encounter"; @@ -9,6 +7,7 @@ import { Symptom } from "@/types/emr/symptom/symptom"; import { Code } from "@/types/questionnaire/code"; import { Quantity } from "@/types/questionnaire/quantity"; import { StructuredQuestionType } from "@/types/questionnaire/question"; +import { CreateAppointmentQuestion } from "@/types/scheduling/schedule"; export type ResponseValue = { type: @@ -22,7 +21,7 @@ export type ResponseValue = { | "symptom" | "diagnosis" | "encounter" - | "follow_up_appointment"; + | "appointment"; value?: | string @@ -35,7 +34,7 @@ export type ResponseValue = { | Symptom[] | Diagnosis[] | Encounter - | FollowUpAppointmentRequest; + | CreateAppointmentQuestion; value_code?: Code; value_quantity?: Quantity; }; diff --git a/src/types/questionnaire/question.ts b/src/types/questionnaire/question.ts index e41fdb3498e..aa0e5e355ac 100644 --- a/src/types/questionnaire/question.ts +++ b/src/types/questionnaire/question.ts @@ -23,7 +23,7 @@ export type StructuredQuestionType = | "symptom" | "diagnosis" | "encounter" - | "follow_up_appointment"; + | "appointment"; type EnableWhenNumeric = { operator: "greater" | "less" | "greater_or_equals" | "less_or_equals"; diff --git a/src/types/scheduling/PublicAppointmentApi.ts b/src/types/scheduling/PublicAppointmentApi.ts index 05bab9ea215..43a9aff49ad 100644 --- a/src/types/scheduling/PublicAppointmentApi.ts +++ b/src/types/scheduling/PublicAppointmentApi.ts @@ -1,16 +1,15 @@ +import { Type } from "@/Utils/request/api"; import { Appointment, - AppointmentCreate, - SlotAvailability, -} from "@/components/Schedule/types"; - -import { Type } from "@/Utils/request/api"; + AppointmentCreateRequest, + TokenSlot, +} from "@/types/scheduling/schedule"; export default { getSlotsForDay: { path: "/api/v1/otp/slots/get_slots_for_day/", method: "POST", - TRes: Type<{ results: SlotAvailability[] }>(), + TRes: Type<{ results: TokenSlot[] }>(), TBody: Type<{ facility: string; user: string; day: string }>(), }, getAppointments: { @@ -22,6 +21,6 @@ export default { path: "/api/v1/otp/slots/{id}/create_appointment/", method: "POST", TRes: Type(), - TBody: Type(), + TBody: Type(), }, } as const; diff --git a/src/types/scheduling/schedule.ts b/src/types/scheduling/schedule.ts new file mode 100644 index 00000000000..f55a1e5a866 --- /dev/null +++ b/src/types/scheduling/schedule.ts @@ -0,0 +1,156 @@ +import { Time } from "@/Utils/types"; +import { AppointmentPatient } from "@/pages/Patient/Utils"; +import { UserBase } from "@/types/user/user"; + +export enum DayOfWeek { + MONDAY = 0, + TUESDAY = 1, + WEDNESDAY = 2, + THURSDAY = 3, + FRIDAY = 4, + SATURDAY = 5, + SUNDAY = 6, +} + +export type ScheduleSlotType = "appointment" | "open" | "closed"; + +export interface AvailabilityDateTime { + day_of_week: DayOfWeek; + start_time: Time; + end_time: Time; +} + +export interface ScheduleTemplate { + id: string; + name: string; + valid_from: string; + valid_to: string; + availabilities: ScheduleAvailability[]; + created_by: UserBase; + updated_by: UserBase; +} + +export interface ScheduleTemplateCreateRequest { + user: string; + name: string; + valid_from: string; // datetime + valid_to: string; // datetime + availabilities: { + name: string; + slot_type: ScheduleSlotType; + slot_size_in_minutes: number; + tokens_per_slot: number; + reason: string; + availability: AvailabilityDateTime[]; + }[]; +} + +export interface ScheduleTemplateUpdateRequest { + name: string; + valid_from: string; + valid_to: string; +} + +export interface ScheduleAvailability { + id: string; + name: string; + slot_type: ScheduleSlotType; + slot_size_in_minutes: number; + tokens_per_slot: number; + reason: string; + availability: AvailabilityDateTime[]; +} + +export interface ScheduleException { + id: string; + reason: string; + valid_from: string; // date in YYYY-MM-DD format + valid_to: string; // date in YYYY-MM-DD format + start_time: Time; + end_time: Time; +} + +export interface ScheduleExceptionCreateRequest { + user: string; // user's id + reason: string; + valid_from: string; + valid_to: string; + start_time: Time; + end_time: Time; +} + +export interface TokenSlot { + id: string; + availability: { + name: string; + tokens_per_slot: number; + }; + start_datetime: string; // timezone naive datetime + end_datetime: string; // timezone naive datetime + allocated: number; +} + +export interface AvailabilityHeatmapRequest { + from_date: string; + to_date: string; + user: string; +} + +export interface AvailabilityHeatmapResponse { + [date: string]: { total_slots: number; booked_slots: number }; +} + +export const AppointmentNonCancelledStatuses = [ + "proposed", + "pending", + "booked", + "arrived", + "fulfilled", + "noshow", + "checked_in", + "waitlist", + "in_consultation", +] as const; + +export const AppointmentCancelledStatuses = [ + "cancelled", + "entered_in_error", +] as const; + +export const AppointmentStatuses = [ + ...AppointmentNonCancelledStatuses, + ...AppointmentCancelledStatuses, +] as const; + +export type AppointmentNonCancelledStatus = + (typeof AppointmentNonCancelledStatuses)[number]; + +export type AppointmentCancelledStatus = + (typeof AppointmentCancelledStatuses)[number]; + +export type AppointmentStatus = (typeof AppointmentStatuses)[number]; + +export interface Appointment { + id: string; + token_slot: TokenSlot; + patient: AppointmentPatient; + booked_on: string; + status: AppointmentNonCancelledStatus; + reason_for_visit: string; + user: UserBase; + booked_by: UserBase | null; // This is null if the appointment was booked by the patient itself. +} + +export interface AppointmentCreateRequest { + patient: string; + reason_for_visit: string; +} + +export interface AppointmentUpdateRequest { + status: Appointment["status"]; +} + +export interface CreateAppointmentQuestion { + reason_for_visit: string; + slot_id: string; +} diff --git a/src/components/Schedule/api.ts b/src/types/scheduling/scheduleApis.ts similarity index 56% rename from src/components/Schedule/api.ts rename to src/types/scheduling/scheduleApis.ts index dc6e56655d9..993ab55ccff 100644 --- a/src/components/Schedule/api.ts +++ b/src/types/scheduling/scheduleApis.ts @@ -1,105 +1,130 @@ +import { HttpMethod, Type } from "@/Utils/request/api"; +import { PaginatedResponse } from "@/Utils/request/types"; import { Appointment, - AppointmentCreate, - AvailabilityHeatmap, + AppointmentCreateRequest, + AppointmentUpdateRequest, + AvailabilityHeatmapRequest, + AvailabilityHeatmapResponse, ScheduleException, + ScheduleExceptionCreateRequest, ScheduleTemplate, - SlotAvailability, -} from "@/components/Schedule/types"; - -import { Type } from "@/Utils/request/api"; -import { PaginatedResponse } from "@/Utils/request/types"; -import { Writable, WritableOnly } from "@/Utils/types"; + ScheduleTemplateCreateRequest, + ScheduleTemplateUpdateRequest, + TokenSlot, +} from "@/types/scheduling/schedule"; import { UserBase } from "@/types/user/user"; -export const ScheduleAPIs = { +export default { + /** + * Schedule Template Related APIs + */ templates: { create: { path: "/api/v1/facility/{facility_id}/schedule/", - method: "POST", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + list: { + path: "/api/v1/facility/{facility_id}/schedule/", + method: HttpMethod.GET, + TRes: Type>(), + }, + update: { + path: "/api/v1/facility/{facility_id}/schedule/{id}/", + method: HttpMethod.PUT, + TBody: Type(), TRes: Type(), - TBody: Type>(), }, delete: { path: "/api/v1/facility/{facility_id}/schedule/{id}/", - method: "DELETE", + method: HttpMethod.DELETE, TBody: Type(), TRes: Type(), }, - list: { - path: "/api/v1/facility/{facility_id}/schedule/", - method: "GET", - TRes: Type>(), - }, }, + /** + * Schedule Exception Related APIs + */ exceptions: { create: { path: "/api/v1/facility/{facility_id}/schedule_exceptions/", - method: "POST", + method: HttpMethod.POST, TRes: Type(), - TBody: Type>(), + TBody: Type(), }, list: { path: "/api/v1/facility/{facility_id}/schedule_exceptions/", - method: "GET", + method: HttpMethod.GET, TRes: Type>(), }, delete: { path: "/api/v1/facility/{facility_id}/schedule_exceptions/{id}/", - method: "DELETE", + method: HttpMethod.DELETE, TRes: Type(), + TBody: Type(), }, }, + /** + * Schedule Token Slot Related APIs + */ slots: { - availabilityHeatmap: { - path: "/api/v1/facility/{facility_id}/slots/availability_stats/", - method: "POST", - TRes: Type(), - TBody: Type<{ from_date: string; to_date: string; user: string }>(), - }, getSlotsForDay: { path: "/api/v1/facility/{facility_id}/slots/get_slots_for_day/", - method: "POST", - TRes: Type<{ results: SlotAvailability[] }>(), + method: HttpMethod.POST, + TRes: Type<{ results: TokenSlot[] }>(), TBody: Type<{ user: string; day: string }>(), }, + availabilityStats: { + path: "/api/v1/facility/{facility_id}/slots/availability_stats/", + method: HttpMethod.POST, + TBody: Type(), + TRes: Type(), + }, createAppointment: { path: "/api/v1/facility/{facility_id}/slots/{slot_id}/create_appointment/", - method: "POST", - TBody: Type(), + method: HttpMethod.POST, + TBody: Type(), TRes: Type(), }, }, + /** + * Appointment Related APIs + */ appointments: { - availableUsers: { - path: "/api/v1/facility/{facility_id}/appointments/available_users/", - method: "GET", - TRes: Type<{ users: UserBase[] }>(), - }, list: { path: "/api/v1/facility/{facility_id}/appointments/", - method: "GET", + method: HttpMethod.GET, TRes: Type>(), }, retrieve: { path: "/api/v1/facility/{facility_id}/appointments/{id}/", - method: "GET", + method: HttpMethod.GET, TRes: Type(), }, update: { path: "/api/v1/facility/{facility_id}/appointments/{id}/", - method: "PUT", - TBody: Type>>(), + method: HttpMethod.PUT, + TBody: Type(), TRes: Type(), }, cancel: { path: "/api/v1/facility/{facility_id}/appointments/{id}/cancel/", - method: "POST", + method: HttpMethod.POST, TBody: Type<{ reason: "cancelled" | "entered_in_error" }>(), TRes: Type(), }, + /** + * Lists schedulable users for a facility + */ + availableUsers: { + path: "/api/v1/facility/{facility_id}/appointments/available_users/", + method: HttpMethod.GET, + TRes: Type<{ users: UserBase[] }>(), + }, }, -} as const; +}; diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index 61292d5c473..924526d7ec7 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -1,7 +1,6 @@ import { HttpMethod, Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; - -import { UserBase } from "./user"; +import { UserBase } from "@/types/user/user"; export default { list: { From 52c17ad78da5de840443d0e484c3068b1f57f0a4 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 8 Jan 2025 19:17:27 +0530 Subject: [PATCH 04/10] cancel appointment from public and other things --- public/locale/en.json | 4 +- src/Utils/request/utils.ts | 8 +- src/components/Facility/FacilityUsers.tsx | 2 +- .../Form/FormFields/DateFormField.tsx | 13 +- src/components/Form/FormFields/FormField.tsx | 3 + .../Form/FormFields/RadioFormField.tsx | 3 + .../Form/FormFields/SelectFormField.tsx | 6 + .../Form/FormFields/TextAreaFormField.tsx | 3 + .../Form/FormFields/TextFormField.tsx | 3 + .../Appointments/AppointmentsPage.tsx | 5 +- .../Schedule/ScheduleTemplateEditForm.tsx | 392 ++++++++++++++++++ .../Schedule/ScheduleTemplateForm.tsx | 3 +- .../Schedule/ScheduleTemplatesList.tsx | 227 ++++++---- src/components/Users/UserAvailabilityTab.tsx | 12 +- src/components/ui/sidebar/app-sidebar.tsx | 2 +- src/pages/Patient/index.tsx | 28 +- src/types/scheduling/PublicAppointmentApi.ts | 14 +- src/types/scheduling/schedule.ts | 15 + src/types/scheduling/scheduleApis.ts | 50 ++- 19 files changed, 668 insertions(+), 125 deletions(-) create mode 100644 src/components/Schedule/ScheduleTemplateEditForm.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 77afa374486..eb18efd6829 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -761,6 +761,7 @@ "edit_policy_description": "Add or edit patient's insurance details", "edit_prescriptions": "Edit Prescriptions", "edit_profile": "Edit Profile", + "edit_schedule_template": "Edit Schedule Template", "edit_user_profile": "Edit Profile", "edited_by": "Edited by", "edited_on": "Edited on", @@ -1216,6 +1217,7 @@ "moving_camera": "Moving Camera", "my_doctors": "My Doctors", "my_profile": "My Profile", + "my_schedules": "My Schedules", "name": "Name", "name_of_hospital": "Name of Hospital", "name_of_shifting_approving_facility": "Name of shifting approving facility", @@ -1560,6 +1562,7 @@ "requested_by": "Requested By", "required": "Required", "required_quantity": "Required Quantity", + "reschedule": "Reschedule", "resend_otp": "Resend OTP", "reset": "Reset", "reset_password": "Reset Password", @@ -1608,7 +1611,6 @@ "schedule_calendar": "Schedule Calendar", "schedule_information": "Schedule Information", "scheduled": "Scheduled", - "schedules": "Schedules", "scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}", "scribe_error": "Could not autofill fields", "search": "Search", diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index 86d9a51eebc..24c492633d3 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -1,8 +1,8 @@ import { Dispatch, SetStateAction } from "react"; +import { toast } from "sonner"; import { LocalStorageKeys } from "@/common/constants"; -import * as Notification from "@/Utils/Notifications"; import { QueryParams, RequestOptions } from "@/Utils/request/types"; export function makeUrl( @@ -43,6 +43,10 @@ const makeQueryParams = (query: QueryParams) => { return qParams.toString(); }; +/** + * TODO: consider replacing this with inferring the types from the route and using a generic + * to ensure that the path params are not missing. + */ const ensurePathNotMissingReplacements = (path: string) => { const missingParams = path.match(/\{.*\}/g); @@ -50,7 +54,7 @@ const ensurePathNotMissingReplacements = (path: string) => { const msg = `Missing path params: ${missingParams.join( ", ", )}. Path: ${path}`; - Notification.Error({ msg }); + toast.error(msg); throw new Error(msg); } }; diff --git a/src/components/Facility/FacilityUsers.tsx b/src/components/Facility/FacilityUsers.tsx index a4af2733d83..4326cc2d618 100644 --- a/src/components/Facility/FacilityUsers.tsx +++ b/src/components/Facility/FacilityUsers.tsx @@ -82,7 +82,7 @@ export default function FacilityUsers(props: { facilityId: number }) { } return ( - + & { }; /** - * A FormField to pick date. - * - * Example usage: - * - * ```jsx - * - * ``` + * @deprecated use shadcn/ui's date-picker instead */ const DateFormField = (props: Props) => { const field = useFormFieldPropsResolver(props); diff --git a/src/components/Form/FormFields/FormField.tsx b/src/components/Form/FormFields/FormField.tsx index 7f9c2699d64..f3ad0559e44 100644 --- a/src/components/Form/FormFields/FormField.tsx +++ b/src/components/Form/FormFields/FormField.tsx @@ -48,6 +48,9 @@ export const FieldErrorText = (props: ErrorProps) => { ); }; +/** + * @deprecated use shadcn/ui's solution for form fields instead along with react-hook-form + */ const FormField = ({ field, ...props diff --git a/src/components/Form/FormFields/RadioFormField.tsx b/src/components/Form/FormFields/RadioFormField.tsx index 79cdb64a579..ca205fcccad 100644 --- a/src/components/Form/FormFields/RadioFormField.tsx +++ b/src/components/Form/FormFields/RadioFormField.tsx @@ -17,6 +17,9 @@ type Props = FormFieldBaseProps & { layout?: "vertical" | "horizontal" | "grid" | "auto"; }; +/** + * @deprecated use shadcn/ui's radio-group instead + */ const RadioFormField = (props: Props) => { const field = useFormFieldPropsResolver(props); return ( diff --git a/src/components/Form/FormFields/SelectFormField.tsx b/src/components/Form/FormFields/SelectFormField.tsx index 5cf992d8bdd..aa712dc16a1 100644 --- a/src/components/Form/FormFields/SelectFormField.tsx +++ b/src/components/Form/FormFields/SelectFormField.tsx @@ -21,6 +21,9 @@ type SelectFormFieldProps = FormFieldBaseProps & { inputClassName?: string; }; +/** + * @deprecated use shadcn/ui's select instead + */ export const SelectFormField = (props: SelectFormFieldProps) => { const field = useFormFieldPropsResolver(props); return ( @@ -58,6 +61,9 @@ type MultiSelectFormFieldProps = FormFieldBaseProps & { optionDisabled?: OptionCallback; }; +/** + * @deprecated + */ export const MultiSelectFormField = ( props: MultiSelectFormFieldProps, ) => { diff --git a/src/components/Form/FormFields/TextAreaFormField.tsx b/src/components/Form/FormFields/TextAreaFormField.tsx index f26717810d4..b4e85e226ea 100644 --- a/src/components/Form/FormFields/TextAreaFormField.tsx +++ b/src/components/Form/FormFields/TextAreaFormField.tsx @@ -19,6 +19,9 @@ export type TextAreaFormFieldProps = FormFieldBaseProps & { onBlur?: (event: React.FocusEvent) => void; }; +/** + * @deprecated use shadcn/ui's textarea instead + */ const TextAreaFormField = forwardRef( ( { rows = 3, ...props }: TextAreaFormFieldProps, diff --git a/src/components/Form/FormFields/TextFormField.tsx b/src/components/Form/FormFields/TextFormField.tsx index c9662f83917..8f816c31a2a 100644 --- a/src/components/Form/FormFields/TextFormField.tsx +++ b/src/components/Form/FormFields/TextFormField.tsx @@ -32,6 +32,9 @@ export type TextFormFieldProps = FormFieldBaseProps & clearable?: boolean | undefined; }; +/** + * @deprecated use shadcn/ui's Input instead + */ const TextFormField = forwardRef((props: TextFormFieldProps, ref) => { const field = useFormFieldPropsResolver(props); const { leading, trailing } = props; diff --git a/src/components/Schedule/Appointments/AppointmentsPage.tsx b/src/components/Schedule/Appointments/AppointmentsPage.tsx index 26272f7f485..9dc2a6202d5 100644 --- a/src/components/Schedule/Appointments/AppointmentsPage.tsx +++ b/src/components/Schedule/Appointments/AppointmentsPage.tsx @@ -94,6 +94,7 @@ export default function AppointmentsPage(props: { facilityId?: string }) { const date = qParams.date ?? dateQueryString(new Date()); const setQParams = (params: QueryParams) => { + // TODO: use null for deletion as per raviger's docs params = FiltersCache.utils.clean({ ...qParams, ...params }); _setQParams(params, { replace: true }); }; @@ -407,7 +408,9 @@ function AppointmentColumn(props: { limit: 100, slot: props.slot, user: props.practitioner, - date: props.date, + // TODO: update this + // date_after: props.date, + // date_before: props.date, }, }), }); diff --git a/src/components/Schedule/ScheduleTemplateEditForm.tsx b/src/components/Schedule/ScheduleTemplateEditForm.tsx new file mode 100644 index 00000000000..c2efdba4a3f --- /dev/null +++ b/src/components/Schedule/ScheduleTemplateEditForm.tsx @@ -0,0 +1,392 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { DatePicker } from "@/components/ui/date-picker"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; + +import { getSlotsPerSession } from "@/components/Schedule/helpers"; +import { formatAvailabilityTime } from "@/components/Users/UserAvailabilityTab"; + +import mutate from "@/Utils/request/mutate"; +import { dateQueryString } from "@/Utils/utils"; +import { + AvailabilityDateTime, + ScheduleAvailability, + ScheduleTemplate, +} from "@/types/scheduling/schedule"; +import { DayOfWeek } from "@/types/scheduling/schedule"; +import scheduleApis from "@/types/scheduling/scheduleApis"; + +const templateFormSchema = z.object({ + name: z.string().min(1, "Template name is required"), + valid_from: z.date({ + required_error: "Valid from date is required", + }), + valid_to: z.date({ + required_error: "Valid to date is required", + }), +}); + +const availabilityFormSchema = z.object({ + name: z.string().min(1, "Session name is required"), + tokens_per_slot: z.number().min(1, "Must be greater than 0"), + reason: z.string(), +}); + +export default function ScheduleTemplateEditForm({ + template, + facilityId, + userId, +}: { + template: ScheduleTemplate; + facilityId: string; + userId: string; +}) { + return ( +
+ + {template.availabilities.map((availability) => ( + + ))} +
+ ); +} + +const ScheduleTemplateEditor = ({ + template, + facilityId, + userId, +}: { + template: ScheduleTemplate; + facilityId: string; + userId: string; +}) => { + const queryClient = useQueryClient(); + + const form = useForm>({ + resolver: zodResolver(templateFormSchema), + defaultValues: { + name: template.name, + valid_from: new Date(template.valid_from), + valid_to: new Date(template.valid_to), + }, + }); + + const { mutate: updateTemplate, isPending } = useMutation({ + mutationFn: mutate(scheduleApis.templates.update, { + pathParams: { + facility_id: facilityId, + id: template.id, + }, + }), + onSuccess: () => { + toast.success("Schedule template updated successfully"); + queryClient.invalidateQueries({ + queryKey: ["user-schedule-templates", { facilityId, userId }], + }); + }, + }); + + function onSubmit(values: z.infer) { + updateTemplate({ + name: values.name, + valid_from: dateQueryString(values.valid_from), + valid_to: dateQueryString(values.valid_to), + }); + } + + return ( +
+
+ + ( + + Template Name + + + + + + )} + /> + +
+ ( + + Valid From + field.onChange(date)} + /> + + + )} + /> + + ( + + Valid Till + field.onChange(date)} + /> + + + )} + /> +
+ +
+ +
+ + +
+ ); +}; + +const ScheduleAvailabilityEditor = ({ + availability, + scheduleId, + facilityId, + userId, +}: { + availability: ScheduleAvailability; + scheduleId: string; + facilityId: string; + userId: string; +}) => { + const queryClient = useQueryClient(); + + const form = useForm>({ + resolver: zodResolver(availabilityFormSchema), + defaultValues: { + name: availability.name, + tokens_per_slot: availability.tokens_per_slot, + reason: availability.reason || "", + }, + }); + + const { mutate: updateAvailability, isPending: isUpdating } = useMutation({ + mutationFn: mutate(scheduleApis.templates.availabilities.update, { + pathParams: { + facility_id: facilityId, + schedule_id: scheduleId, + id: availability.id, + }, + }), + onSuccess: () => { + toast.success("Schedule availability updated successfully"); + queryClient.invalidateQueries({ + queryKey: ["user-schedule-templates", { facilityId, userId }], + }); + }, + }); + + const { mutate: deleteAvailability, isPending: isDeleting } = useMutation({ + mutationFn: mutate(scheduleApis.templates.availabilities.delete, { + pathParams: { + facility_id: facilityId, + schedule_id: scheduleId, + id: availability.id, + }, + }), + onSuccess: () => { + toast.success("Schedule availability deleted successfully"); + queryClient.invalidateQueries({ + queryKey: ["user-schedule-templates", { facilityId, userId }], + }); + }, + }); + + function onSubmit(values: z.infer) { + updateAvailability({ + name: values.name, + tokens_per_slot: values.tokens_per_slot, + reason: values.reason, + }); + } + + // Group availabilities by day of week + const availabilitiesByDay = availability.availability.reduce( + (acc, curr) => { + const day = curr.day_of_week; + if (!acc[day]) { + acc[day] = []; + } + acc[day].push(curr); + return acc; + }, + {} as Record, + ); + + // Calculate total slots + const totalSlots = Math.floor( + getSlotsPerSession( + availability.availability[0].start_time, + availability.availability[0].end_time, + availability.slot_size_in_minutes, + ) ?? 0, + ); + + return ( +
+
+
+ +
+ {availability.name} +

+ {availability.slot_type} + | + {totalSlots} slots + | + {availability.slot_size_in_minutes} min. +

+
+
+ + + + + + + deleteAvailability()} + disabled={isUpdating || isDeleting} + className="text-red-600" + > + + Delete + + + +
+ +
+ +
+ ( + + Session Title + + + + + + )} + /> + + ( + + Patients per slot + + field.onChange(e.target.valueAsNumber)} + /> + + + + )} + /> +
+ +
+ Schedule +
+ {Object.entries(availabilitiesByDay).map(([day, times]) => ( +

+ + {DayOfWeek[parseInt(day)].charAt(0) + + DayOfWeek[parseInt(day)].slice(1).toLowerCase()} + + + {times + .map((time) => formatAvailabilityTime([time])) + .join(", ")} + +

+ ))} +
+
+ + ( + + Remarks + +