Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,7 @@
"choose_state": "Choose State",
"choose_your_login_method_to_continue": "Choose your login method to continue",
"chosen_encounter": "Chosen Encounter",
"chosen_overlapping_slots_warning": "You've chosen overlapping slots. You can still book both if the patient is okay with it.",
Comment thread
shivankacker marked this conversation as resolved.
"chronic_condition_one": "Chronic Condition",
"chronic_condition_other": "Chronic Conditions",
"claim__add_item": "Add Item",
Expand Down Expand Up @@ -1249,6 +1250,7 @@
"claim__use__claim": "Claim",
"claim__use__preauthorization": "Pre Authorization",
"claims": "Claims",
"clash_new_slot_overlaps": "The new slot selected with <bold>{{resourceName}}</bold> at <bold>{{time}}</bold> overlaps slightly.",
"class": "Class",
"class_history": "Class History",
"classification": "Classification",
Expand Down Expand Up @@ -1536,6 +1538,7 @@
"contexts": "Contexts",
"continue": "Continue",
"continue_and_clear": "Continue and Clear",
"continue_anyway": "Continue anyway",
"continue_watching": "Continue watching",
"contribute_github": "Contribute on Github",
"copied_to_clipboard": "Copied to clipboard!",
Expand Down Expand Up @@ -3568,6 +3571,7 @@
"moving_camera": "Moving Camera",
"mrp": "MRP",
"multi_invoice": "Multi Invoice",
"multiple_appointment_alert": "Multiple Appointment Alert",
"multiple_users_linked_to_phone": "Multiple users linked to this phone number",
"multiple_users_linked_to_phone_hint": "Enter your username so we can reset the correct account.",
"must_be_greater_than_value": "Must be greater than {{value}}",
Expand Down Expand Up @@ -4242,6 +4246,7 @@
"patient__volunteer-contact": "Volunteer Contact",
"patient_address": "Patient Address",
"patient_age": "Age",
"patient_already_has_appointment_on": "This patient already has an appointment on",
"patient_and_billing_details": "Patient and billing details",
"patient_basics": "Patient Basics",
"patient_birth_year_for_identity": "Please enter the patient's year of birth to verify their identity",
Expand Down Expand Up @@ -4396,6 +4401,7 @@
"phone_number_validation_error": "Entered phone number is not valid",
"phone_number_verified": "Phone Number Verified",
"pick_a_date": "Pick a date",
"pick_another_slot": "Pick another slot",
"pin": "PIN",
"pin_page": "Pin/Add to Overview",
"pin_page_already_pinned_description": "This page is already pinned to your dashboard. You can unpin it from the dashboard if you wish to pin a different page.",
Expand Down Expand Up @@ -6140,6 +6146,7 @@
"time_of_death": "Time of death",
"time_slot": "Time Slot",
"time_slots_per_day": "Time slot per day",
"timing_clash_alert": "Timing clash alert!",
"title": "Title",
"title_is_required": "Title is required",
"title_of_request": "Title of Request",
Expand Down Expand Up @@ -6252,6 +6259,7 @@
"try_different_abha_linking_option": "Want to try a different linking option, here are some more:",
"try_different_search": "Try a different search term",
"try_different_search_terms": "Try different search terms",
"trying_to_book_another_slot_same_doctor": "You're trying to book another slot with the <bold>same doctor</bold> at:",
"tube": "Tube",
"two_factor_authentication": "Two Factor Authentication",
"two_factor_authentication_active": "Two-factor authentication is currently active on your account.",
Expand Down Expand Up @@ -6655,6 +6663,7 @@
"welcome_back": "Welcome back!",
"welcome_back_to_hospital_dashboard": "Welcome back to the overview ",
"what_facility_assign_the_patient_to": "What facility would you like to assign the patient to",
"what_would_you_like_to_do": "What would you like to do?",
"whatsapp_number": "Whatsapp Number",
"whatsapp_number_same_as_phone_number": "WhatsApp number is same as phone number",
"why_the_asset_is_not_working": "Why the asset is not working?",
Expand Down
168 changes: 168 additions & 0 deletions src/pages/Appointments/BookAppointment/AppointmentConflictAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { format } from "date-fns";
import { MessageCircleWarning, X } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";

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

import { ScheduleResourceIcon } from "@/components/Schedule/ScheduleResourceIcon";
import {
Appointment,
formatScheduleResourceName,
SchedulableResourceType,
ScheduleResource,
TokenSlot,
} from "@/types/scheduling/schedule";

export type AppointmentConflictType = "duplicate" | "clash";

interface AppointmentConflictAlertProps {
type: AppointmentConflictType;
conflictingAppointment: Appointment;
newSlot: Pick<TokenSlot, "start_datetime" | "end_datetime">;
newResource?: ScheduleResource;
onPickAnotherSlot: () => void;
onContinueAnyway: () => void;
onClose: () => void;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

onClose and onPickAnotherSlot do the exact same thing.

In the parent, both props are wired to dismissConflict. If they're identical, the component doesn't need two separate props — pick one name or combine them. Either the component API is over-engineered, or the intent was for these to do different things (e.g., onClose closes without selecting, onPickAnotherSlot scrolls to another slot). Right now, the API lies about having two distinct behaviors.

export const AppointmentConflictAlert = ({
type,
conflictingAppointment,
newSlot,
newResource,
onPickAnotherSlot,
onContinueAnyway,
onClose,
}: AppointmentConflictAlertProps) => {
const { t } = useTranslation();

const existingStart = new Date(
conflictingAppointment.token_slot.start_datetime,
);
const newStart = new Date(newSlot.start_datetime);

return (
<div className="flex flex-col gap-3 p-4">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2.5">
<div className="flex items-center justify-center size-9 rounded-full bg-yellow-100 shrink-0">
<MessageCircleWarning className="size-5 text-yellow-700" />
</div>
<span className="font-semibold text-gray-950">
{type === "duplicate"
? t("multiple_appointment_alert")
: t("timing_clash_alert")}
</span>
</div>
<button
type="button"
onClick={onClose}
aria-label={t("close")}
className="text-gray-400 hover:text-gray-600 shrink-0"
>
<X className="size-4" />
</button>
Comment thread
shivankacker marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>

<div className="border-t border-gray-200" />

<p className="text-sm text-gray-950">
{t("patient_already_has_appointment_on")}{" "}
<span className="font-semibold">
{format(existingStart, "dd MMM yyyy")} ·{" "}
{format(existingStart, "hh:mm a")}
</span>
</p>

<div className="flex items-center justify-between gap-2 rounded-md bg-gray-50 p-2">
<div className="flex items-center gap-2 min-w-0">
<ScheduleResourceIcon
resource={conflictingAppointment}
className="size-8"
/>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium text-gray-950 truncate">
{formatScheduleResourceName(conflictingAppointment)}
</span>
{conflictingAppointment.resource_type ===
SchedulableResourceType.Practitioner && (
<span className="text-xs text-gray-600">
{t(conflictingAppointment.resource.user_type)}
</span>
)}
</div>
</div>
{type === "duplicate" && (
<span className="text-sm font-medium text-green-700 whitespace-nowrap">
{format(existingStart, "hh:mm a")}
</span>
)}
</div>

{type === "duplicate" ? (
<p className="text-sm text-gray-950">
<Trans
i18nKey="trying_to_book_another_slot_same_doctor"
components={{ bold: <span className="font-semibold" /> }}
/>{" "}
<span className="font-semibold">
{format(newStart, "dd MMM yyyy")} · {format(newStart, "hh:mm a")}
</span>
Comment thread
shivankacker marked this conversation as resolved.
</p>
) : (
<>
<p className="text-sm text-gray-950">
<Trans
i18nKey="clash_new_slot_overlaps"
values={{
resourceName: newResource
? formatScheduleResourceName(newResource)
: "",
time: format(newStart, "hh:mm a"),
}}
components={{ bold: <span className="font-semibold" /> }}
/>
</p>

{newResource && (
<div className="flex items-center gap-2 rounded-md bg-gray-50 p-2">
<ScheduleResourceIcon resource={newResource} className="size-8" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium text-gray-950 truncate">
{formatScheduleResourceName(newResource)}
</span>
{newResource.resource_type ===
SchedulableResourceType.Practitioner && (
<span className="text-xs text-gray-600">
{t(newResource.resource.user_type)}
</span>
)}
</div>
</div>
)}
</>
)}

<p className="text-sm font-medium text-gray-950">
{t("what_would_you_like_to_do")}
</p>

<Button
type="button"
variant="primary"
className="w-full"
onClick={onPickAnotherSlot}
>
{t("pick_another_slot")}
</Button>

<button
type="button"
onClick={onContinueAnyway}
className="text-sm font-semibold text-gray-950 underline text-center mx-auto"
>
{t("continue_anyway")}
</button>
</div>
);
};
Loading
Loading