Set buffer time before/after event type (#2015)
* before and after buffer added to handleAvailableSlots function * --WIP * added migration * pull buffer data from DB * cleanup * added buffer input in form * removed unused functions in controller field * improved the buffer time check * fixed default value and added preceding event afterbuffer consideration * fixed e2e test issue Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
b77923fc65
commit
eeb0cd7e4d
10 changed files with 164 additions and 2 deletions
|
@ -14,6 +14,8 @@ import Loader from "@components/Loader";
|
||||||
type AvailableTimesProps = {
|
type AvailableTimesProps = {
|
||||||
timeFormat: string;
|
timeFormat: string;
|
||||||
minimumBookingNotice: number;
|
minimumBookingNotice: number;
|
||||||
|
beforeBufferTime: number;
|
||||||
|
afterBufferTime: number;
|
||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
eventLength: number;
|
eventLength: number;
|
||||||
slotInterval: number | null;
|
slotInterval: number | null;
|
||||||
|
@ -33,6 +35,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
timeFormat,
|
timeFormat,
|
||||||
users,
|
users,
|
||||||
schedulingType,
|
schedulingType,
|
||||||
|
beforeBufferTime,
|
||||||
|
afterBufferTime,
|
||||||
}) => {
|
}) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -45,6 +49,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
schedulingType,
|
schedulingType,
|
||||||
users,
|
users,
|
||||||
minimumBookingNotice,
|
minimumBookingNotice,
|
||||||
|
beforeBufferTime,
|
||||||
|
afterBufferTime,
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -241,6 +241,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
users={eventType.users}
|
users={eventType.users}
|
||||||
schedulingType={eventType.schedulingType ?? null}
|
schedulingType={eventType.schedulingType ?? null}
|
||||||
|
beforeBufferTime={eventType.beforeEventBuffer}
|
||||||
|
afterBufferTime={eventType.afterEventBuffer}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,10 +30,21 @@ type UseSlotsProps = {
|
||||||
date: Dayjs;
|
date: Dayjs;
|
||||||
users: { username: string | null }[];
|
users: { username: string | null }[];
|
||||||
schedulingType: SchedulingType | null;
|
schedulingType: SchedulingType | null;
|
||||||
|
beforeBufferTime?: number;
|
||||||
|
afterBufferTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSlots = (props: UseSlotsProps) => {
|
export const useSlots = (props: UseSlotsProps) => {
|
||||||
const { slotInterval, eventLength, minimumBookingNotice = 0, date, users, eventTypeId } = props;
|
const {
|
||||||
|
slotInterval,
|
||||||
|
eventLength,
|
||||||
|
minimumBookingNotice = 0,
|
||||||
|
beforeBufferTime = 0,
|
||||||
|
afterBufferTime = 0,
|
||||||
|
date,
|
||||||
|
users,
|
||||||
|
eventTypeId,
|
||||||
|
} = props;
|
||||||
const [slots, setSlots] = useState<Slot[]>([]);
|
const [slots, setSlots] = useState<Slot[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
@ -124,6 +135,29 @@ export const useSlots = (props: UseSlotsProps) => {
|
||||||
// Check if startTime is between slot
|
// Check if startTime is between slot
|
||||||
else if (startTime.isBetween(times[i], times[i].add(eventLength, "minutes"))) {
|
else if (startTime.isBetween(times[i], times[i].add(eventLength, "minutes"))) {
|
||||||
times.splice(i, 1);
|
times.splice(i, 1);
|
||||||
|
}
|
||||||
|
// Check if time is between afterBufferTime and beforeBufferTime
|
||||||
|
else if (
|
||||||
|
times[i].isBetween(
|
||||||
|
startTime.subtract(beforeBufferTime, "minutes"),
|
||||||
|
endTime.add(afterBufferTime, "minutes")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
times.splice(i, 1);
|
||||||
|
}
|
||||||
|
// considering preceding event's after buffer time
|
||||||
|
else if (
|
||||||
|
i > 0 &&
|
||||||
|
times[i - 1]
|
||||||
|
.add(eventLength + afterBufferTime, "minutes")
|
||||||
|
.isBetween(
|
||||||
|
startTime.subtract(beforeBufferTime, "minutes"),
|
||||||
|
endTime.add(afterBufferTime, "minutes"),
|
||||||
|
null,
|
||||||
|
"[)"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
times.splice(i, 1);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ export type AdvancedOptions = {
|
||||||
requiresConfirmation?: boolean;
|
requiresConfirmation?: boolean;
|
||||||
disableGuests?: boolean;
|
disableGuests?: boolean;
|
||||||
minimumBookingNotice?: number;
|
minimumBookingNotice?: number;
|
||||||
|
beforeBufferTime?: number;
|
||||||
|
afterBufferTime?: number;
|
||||||
slotInterval?: number | null;
|
slotInterval?: number | null;
|
||||||
price?: number;
|
price?: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
|
|
@ -44,6 +44,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
periodCountCalendarDays: true,
|
periodCountCalendarDays: true,
|
||||||
schedulingType: true,
|
schedulingType: true,
|
||||||
minimumBookingNotice: true,
|
minimumBookingNotice: true,
|
||||||
|
beforeEventBuffer: true,
|
||||||
|
afterEventBuffer: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
slotInterval: true,
|
slotInterval: true,
|
||||||
|
|
|
@ -364,6 +364,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
periodCountCalendarDays: "1" | "0";
|
periodCountCalendarDays: "1" | "0";
|
||||||
periodDates: { startDate: Date; endDate: Date };
|
periodDates: { startDate: Date; endDate: Date };
|
||||||
minimumBookingNotice: number;
|
minimumBookingNotice: number;
|
||||||
|
beforeBufferTime: number;
|
||||||
|
afterBufferTime: number;
|
||||||
slotInterval: number | null;
|
slotInterval: number | null;
|
||||||
destinationCalendar: {
|
destinationCalendar: {
|
||||||
integration: string;
|
integration: string;
|
||||||
|
@ -683,7 +685,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={async (values) => {
|
handleSubmit={async (values) => {
|
||||||
const { periodDates, periodCountCalendarDays, smartContractAddress, ...input } = values;
|
const {
|
||||||
|
periodDates,
|
||||||
|
periodCountCalendarDays,
|
||||||
|
smartContractAddress,
|
||||||
|
beforeBufferTime,
|
||||||
|
afterBufferTime,
|
||||||
|
...input
|
||||||
|
} = values;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...input,
|
...input,
|
||||||
availability: availabilityState,
|
availability: availabilityState,
|
||||||
|
@ -691,6 +700,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
periodEndDate: periodDates.endDate,
|
periodEndDate: periodDates.endDate,
|
||||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||||
id: eventType.id,
|
id: eventType.id,
|
||||||
|
beforeEventBuffer: beforeBufferTime,
|
||||||
|
afterEventBuffer: afterBufferTime,
|
||||||
metadata: smartContractAddress
|
metadata: smartContractAddress
|
||||||
? {
|
? {
|
||||||
smartContractAddress,
|
smartContractAddress,
|
||||||
|
@ -1189,6 +1200,98 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className="border-neutral-200" />
|
||||||
|
<div className="block sm:flex">
|
||||||
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
|
<label
|
||||||
|
htmlFor="bufferTime"
|
||||||
|
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
||||||
|
{t("buffer_time")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="inline-flex w-full space-x-2">
|
||||||
|
<div className="w-full">
|
||||||
|
<label
|
||||||
|
htmlFor="beforeBufferTime"
|
||||||
|
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||||
|
{t("before_event")}
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="beforeBufferTime"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={eventType.beforeEventBuffer || 0}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const beforeBufferOptions = [
|
||||||
|
{
|
||||||
|
label: t("event_buffer_default"),
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||||
|
label: minutes + " " + t("minutes"),
|
||||||
|
value: minutes,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) onChange(val.value);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
beforeBufferOptions.find((option) => option.value === value) ||
|
||||||
|
beforeBufferOptions[0]
|
||||||
|
}
|
||||||
|
options={beforeBufferOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<label
|
||||||
|
htmlFor="afterBufferTime"
|
||||||
|
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||||
|
{t("after_event")}
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="afterBufferTime"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={eventType.afterEventBuffer || 0}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const afterBufferOptions = [
|
||||||
|
{
|
||||||
|
label: t("event_buffer_default"),
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...[5, 10, 15, 20, 30, 45, 60].map((minutes) => ({
|
||||||
|
label: minutes + " " + t("minutes"),
|
||||||
|
value: minutes,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) onChange(val.value);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
afterBufferOptions.find((option) => option.value === value) ||
|
||||||
|
afterBufferOptions[0]
|
||||||
|
}
|
||||||
|
options={afterBufferOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr className="border-neutral-200" />
|
<hr className="border-neutral-200" />
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
|
@ -1618,6 +1721,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
disableGuests: true,
|
disableGuests: true,
|
||||||
minimumBookingNotice: true,
|
minimumBookingNotice: true,
|
||||||
|
beforeEventBuffer: true,
|
||||||
|
afterEventBuffer: true,
|
||||||
slotInterval: true,
|
slotInterval: true,
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
|
|
|
@ -61,6 +61,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
periodDays: true,
|
periodDays: true,
|
||||||
periodCountCalendarDays: true,
|
periodCountCalendarDays: true,
|
||||||
minimumBookingNotice: true,
|
minimumBookingNotice: true,
|
||||||
|
beforeEventBuffer: true,
|
||||||
|
afterEventBuffer: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
|
|
|
@ -284,6 +284,10 @@
|
||||||
"hover_over_bold_times_tip": "Tip: Hover over the bold times for a full timestamp",
|
"hover_over_bold_times_tip": "Tip: Hover over the bold times for a full timestamp",
|
||||||
"start_time": "Start time",
|
"start_time": "Start time",
|
||||||
"end_time": "End time",
|
"end_time": "End time",
|
||||||
|
"buffer_time": "Buffer time",
|
||||||
|
"before_event": "Before event",
|
||||||
|
"after_event": "After event",
|
||||||
|
"event_buffer_default": "No buffer time",
|
||||||
"buffer": "Buffer",
|
"buffer": "Buffer",
|
||||||
"your_day_starts_at": "Your day starts at",
|
"your_day_starts_at": "Your day starts at",
|
||||||
"your_day_ends_at": "Your day ends at",
|
"your_day_ends_at": "Your day ends at",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EventType" ADD COLUMN "afterEventBuffer" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN "beforeEventBuffer" INTEGER NOT NULL DEFAULT 0;
|
|
@ -59,6 +59,8 @@ model EventType {
|
||||||
requiresConfirmation Boolean @default(false)
|
requiresConfirmation Boolean @default(false)
|
||||||
disableGuests Boolean @default(false)
|
disableGuests Boolean @default(false)
|
||||||
minimumBookingNotice Int @default(120)
|
minimumBookingNotice Int @default(120)
|
||||||
|
beforeEventBuffer Int @default(0)
|
||||||
|
afterEventBuffer Int @default(0)
|
||||||
schedulingType SchedulingType?
|
schedulingType SchedulingType?
|
||||||
Schedule Schedule[]
|
Schedule Schedule[]
|
||||||
price Int @default(0)
|
price Int @default(0)
|
||||||
|
|
Loading…
Reference in a new issue