From 20fc46dac55dadce2ca8d5ef12835092e40d36bd Mon Sep 17 00:00:00 2001 From: Ayobami Date: Mon, 30 Jun 2025 20:14:34 +0100 Subject: [PATCH] ISSUE 1: refactor date picker component to prevent greater from than to time and disabled overlapping slots --- src/components/frontend/DateTimePicker.jsx | 226 ++++++++++++++++----- 1 file changed, 177 insertions(+), 49 deletions(-) diff --git a/src/components/frontend/DateTimePicker.jsx b/src/components/frontend/DateTimePicker.jsx index 33b5221..f0128a9 100644 --- a/src/components/frontend/DateTimePicker.jsx +++ b/src/components/frontend/DateTimePicker.jsx @@ -1,4 +1,9 @@ -import { fullMonthsMapping, hourlySlots, monthsMapping, daysMapping } from "@/utils/date-time-utils"; +import { + fullMonthsMapping, + hourlySlots, + monthsMapping, + daysMapping, +} from "@/utils/date-time-utils"; import { formatScheduleDate, parseJsonSafely } from "@/utils/utils"; import moment from "moment"; import React, { useState } from "react"; @@ -7,12 +12,57 @@ import CalendarIcon from "./icons/CalendarIcon"; import NextIcon from "./icons/NextIcon"; import PrevIcon from "./icons/PrevIcon"; -const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalendar, setShowCalendar, toDefault, fromDefault, bookedSlots, scheduleTemplate, defaultMessage }) => { +const DateTimePicker = ({ + defaultDate, + register, + fieldNames, + setValue, + showCalendar, + setShowCalendar, + toDefault, + fromDefault, + bookedSlots, + scheduleTemplate, + defaultMessage, +}) => { const [selectedDate, setSelectedDate] = useState(defaultDate ?? new Date()); const [from, setFrom] = useState(fromDefault ?? ""); const [to, setTo] = useState(toDefault ?? ""); + const [error, setError] = useState(""); + + // Helper to check if a slot is booked + const isSlotBooked = (slotTime) => { + if (!bookedSlots || !Array.isArray(bookedSlots)) return false; + // Only check slots for the selected date + const selectedDateStr = moment(selectedDate).format("YYYY-MM-DD"); + return bookedSlots.some((booking) => { + // booking should have start and end in ISO or parseable format + const bookingStart = moment(booking.start); + const bookingEnd = moment(booking.end); + // Only check if booking is on the same day + if (bookingStart.format("YYYY-MM-DD") !== selectedDateStr) return false; + // If slotTime is within booking range + return ( + slotTime >= bookingStart.toDate() && slotTime < bookingEnd.toDate() + ); + }); + }; + + // Validate time range + const isTimeRangeValid = () => { + if (!from || !to) return false; + const formattedDate = moment(selectedDate).format("MM/DD/YY"); + const fromTime = new Date(formattedDate + " " + from); + const toTime = new Date(formattedDate + " " + to); + return fromTime < toTime; + }; const onApply = () => { + if (!isTimeRangeValid()) { + setError("Start time must be before end time."); + return; + } + setError(""); setValue("from", from); setValue("to", to); setValue("selectedDate", selectedDate); @@ -25,16 +75,14 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen onClick={() => setShowCalendar((prev) => !prev)} > {fieldNames.map((field, idx) => ( - + ))} -
-
+
+
{ setSelectedDate(newDate); @@ -84,33 +145,51 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen tileDisabled={({ date }) => { let customSlots = []; try { - if (scheduleTemplate?.custom_slots && (Object.keys(scheduleTemplate?.custom_slots))?.length > 0) { - customSlots = JSON.parse(scheduleTemplate?.custom_slots || "[]"); + if ( + scheduleTemplate?.custom_slots && + Object.keys(scheduleTemplate?.custom_slots)?.length > 0 + ) { + customSlots = JSON.parse( + scheduleTemplate?.custom_slots || "[]" + ); } } catch (e) { console.error("Invalid JSON in custom_slots", e); } - if (customSlots.length > 0 && customSlots[(formatScheduleDate(date)).toString()]?.length === 0) { + if ( + customSlots.length > 0 && + customSlots[formatScheduleDate(date).toString()] + ?.length === 0 + ) { return true; } - if (scheduleTemplate?.id && scheduleTemplate[daysMapping[date.getDay()]] != 1) { + if ( + scheduleTemplate?.id && + scheduleTemplate[daysMapping[date.getDay()]] != 1 + ) { return true; } }} minDate={new Date()} - maxDetail="month" + maxDetail='month' /> -

Pacific Time - US & Canada

-
-

From - {from}

+

+ Pacific Time - US & Canada +

+
+

From - {from}

Until - {to}

-
-

- {daysMapping[selectedDate.getDay()]} , {fullMonthsMapping[selectedDate.getMonth()]} {selectedDate.getDate()} +

+

+ + {daysMapping[selectedDate.getDay()]} + {" "} + , {fullMonthsMapping[selectedDate.getMonth()]}{" "} + {selectedDate.getDate()}

-
+
{hourlySlots.map((tm, idx) => { var formattedDate = moment(selectedDate).format("MM/DD/YY"); var fromTime = new Date(formattedDate + " " + from); @@ -120,29 +199,66 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen var json = scheduleTemplate.custom_slots ?? "[]"; var custom_slots_obj = parseJsonSafely(json, {}); var custom_slots = custom_slots_obj[formattedDate] ?? []; - custom_slots = custom_slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) })); - var template_slots = Array.isArray(scheduleTemplate.slots) ? scheduleTemplate.slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) })) : []; + custom_slots = custom_slots.map((slot) => ({ + fromTime: new Date(slot.start), + toTime: new Date(slot.end), + })); + var template_slots = Array.isArray(scheduleTemplate.slots) + ? scheduleTemplate.slots.map((slot) => ({ + fromTime: new Date(slot.start), + toTime: new Date(slot.end), + })) + : []; + + // --- NEW: Check if slot is booked --- + const slotBooked = isSlotBooked(slotTime); return (
-
+ {/* Error message for invalid time range */} + {error && ( +
{error}
+ )} +
-
-

From - {from}

+
+

From - {from}

Until - {to}

@@ -208,4 +336,4 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen ); }; -export default DateTimePicker; \ No newline at end of file +export default DateTimePicker;