ISSUE 1: refactor date picker component to prevent greater from than to time and disabled overlapping slots

This commit is contained in:
Ayobami
2025-06-30 20:14:34 +01:00
parent 40dbe17c78
commit 20fc46dac5
+177 -49
View File
@@ -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) => (
<input
key={idx}
type="hidden"
{...register(field)}
/>
<input key={idx} type='hidden' {...register(field)} />
))}
<button
type="button"
className={`${showCalendar ? "" : "border-2"} md:border-2 p-2 w-full md:relative flex pr-16 gap-2 items-center`}
type='button'
className={`${
showCalendar ? "" : "border-2"
} flex w-full items-center gap-2 p-2 pr-16 md:relative md:border-2`}
onClick={(e) => {
setShowCalendar((prev) => !prev);
e.stopPropagation();
@@ -44,31 +92,44 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen
<CalendarIcon />
</div>
<span
id="booking-time"
className={showCalendar ? "hidden" : "inline whitespace-nowrap md:text-base text-sm"}
id='booking-time'
className={
showCalendar
? "hidden"
: "inline whitespace-nowrap text-sm md:text-base"
}
>
{from && to
? monthsMapping[selectedDate.getMonth()] + " " + selectedDate.getDate() + "/" + selectedDate.getFullYear() + " - " + from + " to " + to
? monthsMapping[selectedDate.getMonth()] +
" " +
selectedDate.getDate() +
"/" +
selectedDate.getFullYear() +
" - " +
from +
" to " +
to
: defaultMessage ?? "Select date and time"}
</span>
{
<div
className={`${showCalendar ? "block" : "hidden"
} absolute md:w-[unset] w-[80vw] bottom-[15px] top-[0%] md:-top-[22.5rem] 2xl:-top-[20rem] md:-left-10 lg:left-[-150px] left-0 md:right-[unset] right-0 text-center mx-auto shadow-lg bg-white border-2 text-sm md:max-h-[unset] min-h-[55vh] overflow-y-auto`}
className={`${
showCalendar ? "block" : "hidden"
} absolute bottom-[15px] left-0 right-0 top-[0%] mx-auto min-h-[55vh] w-[80vw] overflow-y-auto border-2 bg-white text-center text-sm shadow-lg md:-left-10 md:-top-[22.5rem] md:right-[unset] md:max-h-[unset] md:w-[unset] lg:left-[-150px] 2xl:-top-[20rem]`}
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center border-b p-[16px]">
<h3 className="text-xl font-semibold">Select date and time</h3>
<div className='flex items-center justify-between border-b p-[16px]'>
<h3 className='text-xl font-semibold'>Select date and time</h3>
<button
type="button"
type='button'
onClick={() => setShowCalendar(false)}
className="p-1 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full"
className='rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300'
>
&#x2715;
</button>
</div>
<div className="flex md:flex-row flex-col">
<div className="">
<div className='flex flex-col md:flex-row'>
<div className=''>
<Calendar
onChange={(newDate) => {
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'
/>
<p className="text-left p-[16px] text-[#667085]">Pacific Time - US & Canada</p>
<div className="md:flex hidden px-[16px] py-1 cursor-default text-left">
<p className="min-w-[150px]">From - {from}</p>
<p className='p-[16px] text-left text-[#667085]'>
Pacific Time - US & Canada
</p>
<div className='hidden cursor-default px-[16px] py-1 text-left md:flex'>
<p className='min-w-[150px]'>From - {from}</p>
<p>Until - {to}</p>
</div>
</div>
<div className="p-2">
<p className="font-semibold mb-4 text-center">
<span className="capitalize">{daysMapping[selectedDate.getDay()]}</span> , {fullMonthsMapping[selectedDate.getMonth()]} {selectedDate.getDate()}
<div className='p-2'>
<p className='mb-4 text-center font-semibold'>
<span className='capitalize'>
{daysMapping[selectedDate.getDay()]}
</span>{" "}
, {fullMonthsMapping[selectedDate.getMonth()]}{" "}
{selectedDate.getDate()}
</p>
<div className="flex flex-col gap-[12px] custom-calendar-scroll review-scroll overflow-y-auto overflow-x-hidden md:max-h-[270px] max-h-[150px] md:px-6 px-3 text-[#667085]">
<div className='custom-calendar-scroll review-scroll flex max-h-[150px] flex-col gap-[12px] overflow-y-auto overflow-x-hidden px-3 text-[#667085] md:max-h-[270px] md:px-6'>
{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 (
<button
type="button"
type='button'
key={idx}
className={`${from == tm || to == tm ? "border-black border-2" : "border disabled:bg-[#F2F4F7] disabled:line-through border-[#EAECF0]"
} md:w-[152px] w-full text-center py-[8px] ${from && to && fromTime <= slotTime && toTime >= slotTime ? "font-semibold between-slots" : ""}`}
className={`${
from == tm || to == tm
? "border-2 border-black"
: "border border-[#EAECF0] disabled:bg-[#F2F4F7] disabled:line-through"
} w-full py-[8px] text-center md:w-[152px] ${
from &&
to &&
fromTime <= slotTime &&
toTime >= slotTime
? "between-slots font-semibold"
: ""
}`}
onClick={(e) => {
if (from == tm) {
setFrom("");
setTo("");
setError("");
return;
}
if (to == tm) {
setTo("");
setError("");
return;
}
if (from == "") {
setFrom(e.target.innerText);
setError("");
} else {
setTo(e.target.innerText);
// Only allow setting 'to' if after 'from'
const formattedDate =
moment(selectedDate).format("MM/DD/YY");
const fromTime = new Date(
formattedDate + " " + from
);
const toTime = new Date(formattedDate + " " + tm);
if (toTime <= fromTime) {
setError("End time must be after start time.");
return;
} else {
setError("");
setTo(e.target.innerText);
}
}
}}
disabled={(() => {
@@ -152,11 +268,17 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen
// disable if time is < current time
if (slotTime < new Date()) return true;
// --- NEW: Disable if slot is booked ---
if (slotBooked) return true;
if (custom_slots.length > 0) {
var shouldDisable = false;
for (let i = 0; i < custom_slots.length; i++) {
const slot = custom_slots[i];
if (slot.fromTime <= slotTime && slot.toTime >= slotTime) {
if (
slot.fromTime <= slotTime &&
slot.toTime >= slotTime
) {
shouldDisable = false;
break;
} else {
@@ -164,12 +286,14 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen
}
}
if (shouldDisable) return true;
}
else {
} else {
var shouldDisable = false;
for (let i = 0; i < template_slots.length; i++) {
const slot = template_slots[i];
if (slot.fromTime <= slotTimeOnly && slot.toTime >= slotTimeOnly) {
if (
slot.fromTime <= slotTimeOnly &&
slot.toTime >= slotTimeOnly
) {
shouldDisable = false;
break;
} else {
@@ -185,18 +309,22 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen
);
})}
</div>
<div className="mt-8 px-6">
{/* Error message for invalid time range */}
{error && (
<div className='mt-2 text-sm text-red-500'>{error}</div>
)}
<div className='mt-8 px-6'>
<button
type="button"
className="login-btn-gradient w-[152px] text-center py-[8px] rounded-sm text-white"
disabled={from == "" || to == ""}
type='button'
className='login-btn-gradient w-[152px] rounded-sm py-[8px] text-center text-white'
disabled={from == "" || to == "" || !isTimeRangeValid()}
onClick={onApply}
>
Apply
</button>
</div>
<div className="md:hidden flex px-1 py-1 mt-2 cursor-default text-left">
<p className="min-w-[150px]">From - {from}</p>
<div className='mt-2 flex cursor-default px-1 py-1 text-left md:hidden'>
<p className='min-w-[150px]'>From - {from}</p>
<p>Until - {to}</p>
</div>
</div>
@@ -208,4 +336,4 @@ const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalen
);
};
export default DateTimePicker;
export default DateTimePicker;