Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appointment as structured question + other enhancements #9661

Merged
merged 8 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,8 @@
"select_register_patient": "Select/Register Patient",
"select_seven_day_period": "Select a seven day period",
"select_skills": "Select and add some skills",
"select_time": "Select time",
"select_time_slot": "Select time slot",
"select_wards": "Select wards",
"self_booked": "Self-booked",
"send": "Send",
Expand Down Expand Up @@ -1631,6 +1633,7 @@
"skill_add_error": "Error while adding skill",
"skill_added_successfully": "Skill added successfully",
"skills": "Skills",
"slots_left": "slots left",
"social_profile": "Social Profile",
"social_profile_detail": "Include occupation, ration card category, socioeconomic status, and domestic healthcare support for a complete profile.",
"socioeconomic_status": "Socioeconomic status",
Expand Down Expand Up @@ -1833,6 +1836,7 @@
"view_details": "View Details",
"view_facility": "View Facility",
"view_files": "View Files",
"view_patient": "View Patient",
"view_patients": "View Patients",
"view_update_patient_files": "View/Update patient files",
"view_updates": "View Updates",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { useQuery } from "@tanstack/react-query";
import { format } from "date-fns";
import { useState } from "react";
import { useTranslation } from "react-i18next";

import { DatePicker } from "@/components/ui/date-picker";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 { FollowUpAppointmentQuestionRequest } from "@/components/Schedule/types";

import useSlug from "@/hooks/useSlug";

import query from "@/Utils/request/query";
import { dateQueryString, formatDisplayName } from "@/Utils/utils";
import {
QuestionnaireResponse,
ResponseValue,
} from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";
import { UserBase } from "@/types/user/user";

interface FollowUpVisitQuestionProps {
question: Question;
questionnaireResponse: QuestionnaireResponse;
updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void;
disabled?: boolean;
}

export function FollowUpAppointmentQuestion({
question,
questionnaireResponse,
updateQuestionnaireResponseCB,
disabled,
}: FollowUpVisitQuestionProps) {
const { t } = useTranslation();
const [resource, setResource] = useState<UserBase>();
const [selectedDate, setSelectedDate] = useState<Date>();

const values =
(questionnaireResponse.values?.[0]
?.value as unknown as FollowUpAppointmentQuestionRequest[]) || [];

const value = values[0] ?? {};

const handleUpdate = (
updates: Partial<FollowUpAppointmentQuestionRequest>,
) => {
const followUpAppointment = { ...value, ...updates };
updateQuestionnaireResponseCB({
...questionnaireResponse,
values: [
{
type: "follow_up_appointment",
value: [followUpAppointment] as unknown as ResponseValue["value"],
},
],
});
};

const facilityId = useSlug("facility");

const resourcesQuery = useQuery({
queryKey: ["availableResources", facilityId],
queryFn: query(ScheduleAPIs.appointments.availableDoctors, {
pathParams: { facility_id: facilityId },
}),
});

const slotsQuery = useQuery({
queryKey: [
"slots",
facilityId,
resource?.id,
dateQueryString(selectedDate),
],
queryFn: query(ScheduleAPIs.slots.getSlotsForDay, {
pathParams: { facility_id: facilityId },
body: {
resource: resource?.id,
day: dateQueryString(selectedDate),
},
}),
enabled: !!resource && !!selectedDate,
});
Comment on lines +69 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for data fetching.

The queries lack error handling which could lead to poor user experience when network requests fail.

Consider adding error handling and loading states:

   const resourcesQuery = useQuery({
     queryKey: ["availableResources", facilityId],
     queryFn: query(ScheduleAPIs.appointments.availableDoctors, {
       pathParams: { facility_id: facilityId },
     }),
+    retry: 2,
+    onError: (error) => {
+      // Handle error (e.g., show toast notification)
+      console.error('Failed to fetch available doctors:', error);
+    }
   });

   const slotsQuery = useQuery({
     queryKey: [
       "slots",
       facilityId,
       resource?.id,
       dateQueryString(selectedDate),
     ],
     queryFn: query(ScheduleAPIs.slots.getSlotsForDay, {
       pathParams: { facility_id: facilityId },
       body: {
         resource: resource?.id,
         day: dateQueryString(selectedDate),
       },
     }),
     enabled: !!resource && !!selectedDate,
+    retry: 2,
+    onError: (error) => {
+      // Handle error (e.g., show toast notification)
+      console.error('Failed to fetch slots:', error);
+    }
   });

Also, consider displaying error states in the UI:

{resourcesQuery.isError && (
  <div className="text-red-500">
    {t("error_fetching_doctors")}
  </div>
)}

{slotsQuery.isError && (
  <div className="text-red-500">
    {t("error_fetching_slots")}
  </div>
)}


return (
<div className="space-y-4">
<Label className="text-base font-medium">
{question.text}
{question.required && <span className="ml-1 text-red-500">*</span>}
</Label>
<div className="space-y-4">
<div>
<Label className="mb-2">{t("reason_for_visit")}</Label>
<Textarea
placeholder={t("reason_for_visit_placeholder")}
value={value.reason_for_visit || ""}
onChange={(e) => handleUpdate({ reason_for_visit: e.target.value })}
disabled={disabled}
/>
</div>
<div>
<Label className="block mb-2">{t("select_practitioner")}</Label>
<Select
disabled={resourcesQuery.isLoading || disabled}
value={resource?.id}
onValueChange={(value) =>
setResource(
resourcesQuery.data?.users.find((r) => r.id === value),
)
}
>
<SelectTrigger>
<SelectValue placeholder={t("show_all")} />
</SelectTrigger>
<SelectContent>
{resourcesQuery.data?.users.map((user) => (
<SelectItem key={user.username} value={user.id}>
<div className="flex items-center gap-2">
<Avatar
imageUrl={user.profile_picture_url}
name={formatDisplayName(user)}
className="size-6 rounded-full"
/>
<span>{formatDisplayName(user)}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="flex gap-2">
<div className="flex-1">
<Label className="block mb-2">{t("select_date")}</Label>
<DatePicker date={selectedDate} onChange={setSelectedDate} />
</div>

<div className="flex-1">
<Label className="block mb-2">{t("select_time")}</Label>
{(!slotsQuery.data?.results ||
slotsQuery.data.results.length === 0) &&
selectedDate &&
resource ? (
<div className="rounded-md border border-input px-3 py-2 text-sm text-muted-foreground">
{t("no_slots_available")}
</div>
) : (
<Select
disabled={
!selectedDate || !resource || slotsQuery.isLoading || disabled
}
value={value.slot_id}
onValueChange={(slotId) => {
handleUpdate({ slot_id: slotId });
}}
>
<SelectTrigger>
<SelectValue placeholder={t("select_time_slot")} />
</SelectTrigger>
<SelectContent>
{slotsQuery.data?.results &&
groupSlotsByAvailability(slotsQuery.data.results).map(
({ availability, slots }) => (
<div key={availability.name}>
<div className="px-2 py-1.5 text-sm font-semibold">
{availability.name}
</div>
{slots.map((slot) => {
const isFullyBooked =
slot.allocated >= availability.tokens_per_slot;
return (
<SelectItem
key={slot.id}
value={slot.id}
disabled={isFullyBooked}
>
<div className="flex items-center justify-between">
<span>
{format(slot.start_datetime, "HH:mm")}
</span>
<span className="pl-1 text-xs text-gray-500">
{availability.tokens_per_slot -
slot.allocated}{" "}
{t("slots_left")}
</span>
</div>
</SelectItem>
);
})}
</div>
),
)}
{slotsQuery.data?.results.length === 0 && (
<div className="px-2 py-4 text-center text-sm text-gray-500">
{t("no_slots_available")}
</div>
)}
</SelectContent>
</Select>
)}
</div>
</div>
</div>
</div>
);
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import CareIcon from "@/CAREUI/icons/CareIcon";

import { Button } from "@/components/ui/button";

import { FollowUpAppointmentQuestion } from "@/components/Questionnaire/QuestionTypes/FollowUpAppointmentQuestion";

import { QuestionValidationError } from "@/types/questionnaire/batch";
import type {
QuestionnaireResponse,
Expand Down Expand Up @@ -161,6 +163,8 @@ export function QuestionInput({
return <SymptomQuestion {...commonProps} />;
case "diagnosis":
return <DiagnosisQuestion {...commonProps} />;
case "follow_up_appointment":
return <FollowUpAppointmentQuestion {...commonProps} />;
case "encounter":
if (encounterId) {
return (
Expand Down
3 changes: 1 addition & 2 deletions src/components/Questionnaire/QuestionnaireForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export function QuestionnaireForm({
key={`${form.questionnaire.id}-${index}`}
className="border rounded-lg p-6 space-y-6"
>
<div className="flex justify-between items-center flex-wrap">
<div className="flex justify-between items-center">
<div className="space-y-1">
<h2 className="text-xl font-semibold">
{form.questionnaire.title}
Expand All @@ -321,7 +321,6 @@ export function QuestionnaireForm({
type="button"
variant="ghost"
size="sm"
className="self-end"
onClick={() => {
setQuestionnaireForms((prev) =>
prev.filter(
Expand Down
19 changes: 17 additions & 2 deletions src/components/Questionnaire/structured/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ const handlers: {
}),
},
encounter: {
getRequests: (encounters, { patientId, facilityId, encounterId }) => {
getRequests: (encounters, { patientId, encounterId }) => {
if (!encounterId) return [];
console.log("Encounters", encounters, facilityId);
return encounters.map((encounter) => {
const body: RequestTypeFor<"encounter"> = {
organizations: [],
Expand All @@ -154,6 +153,22 @@ const handlers: {
});
},
},
follow_up_appointment: {
getRequests: (followUpAppointment, { facilityId, patientId }) => {
const { reason_for_visit, slot_id } = followUpAppointment[0];
return [
{
url: `/api/v1/facility/${facilityId}/slots/${slot_id}/create_appointment/`,
method: "POST",
body: {
reason_for_visit,
patient: patientId,
},
reference_id: "follow_up_appointment",
},
];
},
},
};

export const getStructuredRequests = <T extends StructuredQuestionType>(
Expand Down
7 changes: 7 additions & 0 deletions src/components/Questionnaire/structured/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
AppointmentCreate,
FollowUpAppointmentQuestionRequest,
} from "@/components/Schedule/types";

import {
AllergyIntolerance,
AllergyIntoleranceRequest,
Expand All @@ -17,6 +22,7 @@ export interface StructuredDataMap {
symptom: Symptom;
diagnosis: Diagnosis;
encounter: Encounter;
follow_up_appointment: FollowUpAppointmentQuestionRequest;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concerned about the naming here

}

// Map structured types to their request types
Expand All @@ -27,6 +33,7 @@ export interface StructuredRequestMap {
symptom: SymptomRequest;
diagnosis: DiagnosisRequest;
encounter: EncounterEditRequest;
follow_up_appointment: AppointmentCreate;
}

export type RequestTypeFor<T extends StructuredQuestionType> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export default function AppointmentCreatePage(props: Props) {
slot.allocated ===
availability.tokens_per_slot
}
className="flex flex-col items-center group"
className="flex flex-col items-center group gap-0"
>
<span className="font-semibold">
{format(slot.start_datetime, "HH:mm")}
Expand Down
Loading
Loading