Finish slot generation
This commit is contained in:
parent
586b8119e2
commit
5486d89726
7 changed files with 671 additions and 100 deletions
|
|
@ -40,7 +40,8 @@ describe("generateTimeSlots", () => {
|
|||
describe("Basic functionality (no events already booked)", () => {
|
||||
it("should generate time slots for a basic availability window", () => {
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -75,7 +76,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
availability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -100,7 +102,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
disabledAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -117,7 +120,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
shortEventConfig,
|
||||
[],
|
||||
|
|
@ -140,7 +144,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
longEventConfig,
|
||||
[],
|
||||
|
|
@ -162,7 +167,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[dayException],
|
||||
|
|
@ -180,7 +186,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[hoursException],
|
||||
|
|
@ -203,7 +210,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[multipleHoursException],
|
||||
|
|
@ -234,7 +242,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[overlappingHoursException],
|
||||
|
|
@ -264,7 +273,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[multipleOverlappingException],
|
||||
|
|
@ -298,7 +308,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[adjacentRangesException],
|
||||
|
|
@ -320,7 +331,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[differentDateException],
|
||||
|
|
@ -348,7 +360,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -382,7 +395,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -409,7 +423,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -427,28 +442,34 @@ describe("generateTimeSlots", () => {
|
|||
describe("Minimum advance booking", () => {
|
||||
it("should mark past slots as unavailable", () => {
|
||||
// Test date is same as current date
|
||||
const todayDate = new Date("2024-01-15T10:00:00Z"); // Same day as mock current time
|
||||
const todayDate = new Date("2024-01-15T00:00:00Z"); // Date for slot generation
|
||||
|
||||
// Mock current time in CET (11:00 CET = 10:00 UTC in winter)
|
||||
const currentTimeCET = new Date("2024-01-15T11:00:00"); // 11:00 CET
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
todayDate,
|
||||
currentTimeCET, // currentTime
|
||||
todayDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
// Slots before current time (10:00) should be unavailable
|
||||
// Slots before current time (11:00 CET) should be unavailable
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
const slot10_00 = slots.find((s) => s.time === "10:00");
|
||||
const slot10_30 = slots.find((s) => s.time === "10:30");
|
||||
|
||||
console.log(slots);
|
||||
const slot11_00 = slots.find((s) => s.time === "11:00");
|
||||
const slot11_30 = slots.find((s) => s.time === "11:30");
|
||||
|
||||
expect(slot09_00?.available).to.be.false;
|
||||
expect(slot09_30?.available).to.be.false;
|
||||
expect(slot10_00?.available).to.be.true;
|
||||
expect(slot10_30?.available).to.be.true;
|
||||
expect(slot10_00?.available).to.be.false;
|
||||
expect(slot10_30?.available).to.be.false;
|
||||
expect(slot11_00?.available).to.be.true;
|
||||
expect(slot11_30?.available).to.be.true;
|
||||
});
|
||||
|
||||
it("should respect minimum advance booking in hours", () => {
|
||||
|
|
@ -460,10 +481,13 @@ describe("generateTimeSlots", () => {
|
|||
},
|
||||
};
|
||||
|
||||
// Mock current time in CET (10:00 CET)
|
||||
const currentTimeCET = new Date("2024-01-15T10:00:00");
|
||||
const todayDate = new Date("2024-01-15T00:00:00Z");
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
todayDate,
|
||||
currentTimeCET, // currentTime
|
||||
todayDate, // date
|
||||
basicAvailability,
|
||||
configWithAdvanceBooking,
|
||||
[],
|
||||
|
|
@ -487,10 +511,14 @@ describe("generateTimeSlots", () => {
|
|||
},
|
||||
};
|
||||
|
||||
const todayDate = new Date("2024-01-15T00:00:00Z"); // Same day as current time
|
||||
const todayDate = new Date("2024-01-15T00:00:00Z");
|
||||
|
||||
// Mock current time in CET (10:00 CET)
|
||||
const currentTimeCET = new Date("2024-01-15T10:00:00");
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
todayDate,
|
||||
currentTimeCET, // currentTime
|
||||
todayDate, // date
|
||||
basicAvailability,
|
||||
configWithAdvanceBooking,
|
||||
[],
|
||||
|
|
@ -502,10 +530,41 @@ describe("generateTimeSlots", () => {
|
|||
expect(slot.available).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore minimum advance booking if the date is in the future", () => {
|
||||
const configWithAdvanceBooking = {
|
||||
...basicEventTypeConfig,
|
||||
minAdvanceBooking: {
|
||||
value: 2,
|
||||
unit: "hours" as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Mock current time in CET (10:00 CET on Jan 15)
|
||||
const currentTimeCET = new Date("2024-01-15T10:00:00");
|
||||
// Test date is Jan 16 (future date)
|
||||
const futureDate = new Date("2024-01-16T00:00:00Z");
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
currentTimeCET, // currentTime
|
||||
futureDate, // date
|
||||
basicAvailability,
|
||||
configWithAdvanceBooking,
|
||||
[],
|
||||
existingEvents
|
||||
);
|
||||
|
||||
// All slots should be available since we're checking a future date
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
|
||||
expect(slot09_00?.available).to.be.true;
|
||||
expect(slot09_30?.available).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Buffer time", () => {
|
||||
it("should add buffer time between slots", () => {
|
||||
it("should generate all possible slots regardless of buffer time", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
|
|
@ -513,22 +572,226 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithBuffer,
|
||||
[],
|
||||
existingEvents
|
||||
);
|
||||
|
||||
// With 30min duration + 15min buffer = 45min between slots
|
||||
// Should generate slots every 30 minutes (duration), not considering buffer time for slot generation
|
||||
expect(slots).to.have.length(16); // Same as without buffer time
|
||||
expect(slots[0].time).to.equal("09:00");
|
||||
expect(slots[1].time).to.equal("09:45");
|
||||
expect(slots[2].time).to.equal("10:30");
|
||||
expect(slots[3].time).to.equal("11:15");
|
||||
expect(slots[1].time).to.equal("09:30");
|
||||
expect(slots[2].time).to.equal("10:00");
|
||||
expect(slots[3].time).to.equal("10:30");
|
||||
});
|
||||
|
||||
it("should handle buffer time that prevents slots from fitting", () => {
|
||||
it("should apply buffer time around existing events to disable slots", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 15, // 15 minutes buffer before and after events
|
||||
};
|
||||
|
||||
const existingEvent: Tables<"events"> = {
|
||||
id: "1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00",
|
||||
end_time: "10:30",
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "Existing Meeting",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithBuffer,
|
||||
[],
|
||||
[existingEvent]
|
||||
);
|
||||
|
||||
// Find slots around the existing event (10:00-10:30)
|
||||
// Buffer time should disable slots that ends after 09:45 or starts before 10:45
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
const slot10_00 = slots.find((s) => s.time === "10:00");
|
||||
const slot10_30 = slots.find((s) => s.time === "10:30");
|
||||
const slot11_00 = slots.find((s) => s.time === "11:00");
|
||||
|
||||
expect(slot09_00?.available).to.be.true; // Ends before buffer starts
|
||||
expect(slot09_30?.available).to.be.false; // Ends at 10:00, overlaps with buffer (09:45-10:45)
|
||||
expect(slot10_00?.available).to.be.false; // Overlaps with buffered event
|
||||
expect(slot10_30?.available).to.be.false; // Starts at 10:30, overlaps with buffer until 10:45
|
||||
expect(slot11_00?.available).to.be.true; // Starts after buffer ends
|
||||
});
|
||||
|
||||
it("should handle buffer time with events without end_time", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 15,
|
||||
};
|
||||
|
||||
const eventWithoutEndTime: Tables<"events"> = {
|
||||
id: "1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00",
|
||||
end_time: null, // Will use event duration (30 min)
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "Meeting without end time",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithBuffer,
|
||||
[],
|
||||
[eventWithoutEndTime]
|
||||
);
|
||||
|
||||
// Event is 10:00-10:30, with 15min buffer: 09:45-10:45
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
const slot10_00 = slots.find((s) => s.time === "10:00");
|
||||
const slot10_30 = slots.find((s) => s.time === "10:30");
|
||||
const slot11_00 = slots.find((s) => s.time === "11:00");
|
||||
|
||||
expect(slot09_00?.available, "09:00 should be available").to.be.true;
|
||||
expect(slot09_30?.available, "09:30 should not be available").to.be.false;
|
||||
expect(slot10_00?.available, "10:00 should not be unavailable").to.be
|
||||
.false; // Within buffered time
|
||||
expect(slot10_30?.available, "10:30 should not be unavailable").to.be
|
||||
.false; // Within buffered time
|
||||
expect(slot11_00?.available, "11:00 should be available").to.be.true; // After buffered time
|
||||
});
|
||||
|
||||
it("should handle large buffer time that affects multiple slots", () => {
|
||||
const configWithLargeBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 60, // 1 hour buffer
|
||||
};
|
||||
|
||||
const existingEvent: Tables<"events"> = {
|
||||
id: "1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "12:00",
|
||||
end_time: "12:30",
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "Existing Meeting",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithLargeBuffer,
|
||||
[],
|
||||
[existingEvent]
|
||||
);
|
||||
|
||||
// Event is 12:00-12:30, with 60min buffer: 11:00-13:30
|
||||
const slot10_30 = slots.find((s) => s.time === "10:30");
|
||||
const slot11_00 = slots.find((s) => s.time === "11:00");
|
||||
const slot11_30 = slots.find((s) => s.time === "11:30");
|
||||
const slot12_00 = slots.find((s) => s.time === "12:00");
|
||||
const slot12_30 = slots.find((s) => s.time === "12:30");
|
||||
const slot13_00 = slots.find((s) => s.time === "13:00");
|
||||
const slot13_30 = slots.find((s) => s.time === "13:30");
|
||||
const slot14_00 = slots.find((s) => s.time === "14:00");
|
||||
|
||||
expect(slot10_30?.available).to.be.true; // Before buffer
|
||||
expect(slot11_00?.available).to.be.false; // Within buffer
|
||||
expect(slot11_30?.available).to.be.false; // Within buffer
|
||||
expect(slot12_00?.available).to.be.false; // Within buffer
|
||||
expect(slot12_30?.available).to.be.false; // Within buffer
|
||||
expect(slot13_00?.available).to.be.false; // Within buffer (ends at 13:30)
|
||||
expect(slot13_30?.available).to.be.true; // After buffer
|
||||
expect(slot14_00?.available).to.be.true; // After buffer
|
||||
});
|
||||
|
||||
it("should handle multiple events with overlapping buffer times", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 30,
|
||||
};
|
||||
|
||||
const existingEvents: Tables<"events">[] = [
|
||||
{
|
||||
id: "1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "10:00",
|
||||
end_time: "10:30",
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "First Meeting",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "11:30",
|
||||
end_time: "12:00",
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "Second Meeting",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
},
|
||||
];
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithBuffer,
|
||||
[],
|
||||
existingEvents
|
||||
);
|
||||
|
||||
// First event: 10:00-10:30, buffer: 09:30-11:00
|
||||
// Second event: 11:30-12:00, buffer: 11:00-12:30
|
||||
// Combined buffer coverage: 09:30-12:30
|
||||
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
const slot10_00 = slots.find((s) => s.time === "10:00");
|
||||
const slot11_00 = slots.find((s) => s.time === "11:00");
|
||||
const slot12_00 = slots.find((s) => s.time === "12:00");
|
||||
const slot12_30 = slots.find((s) => s.time === "12:30");
|
||||
const slot13_00 = slots.find((s) => s.time === "13:00");
|
||||
|
||||
expect(slot09_00?.available).to.be.true; // Before any buffer
|
||||
expect(slot09_30?.available).to.be.false; // Within first event's buffer
|
||||
expect(slot10_00?.available).to.be.false; // Within first event's buffer
|
||||
expect(slot11_00?.available).to.be.false; // Within second event's buffer
|
||||
expect(slot12_00?.available).to.be.false; // Within second event's buffer
|
||||
expect(slot12_30?.available).to.be.true; // After all buffers
|
||||
expect(slot13_00?.available).to.be.true; // After all buffers
|
||||
});
|
||||
|
||||
it("should not affect slot generation in short availability windows", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 60, // Large buffer
|
||||
|
|
@ -540,16 +803,70 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
shortAvailability,
|
||||
configWithLargeBuffer,
|
||||
configWithBuffer,
|
||||
[],
|
||||
existingEvents
|
||||
);
|
||||
|
||||
// Only one slot should fit (09:00-09:30 + 60min buffer = 10:30)
|
||||
expect(slots).to.have.length(1);
|
||||
// Should generate all possible slots within the availability window
|
||||
expect(slots).to.have.length(3); // 09:00, 09:30, 10:00
|
||||
expect(slots[0].time).to.equal("09:00");
|
||||
expect(slots[1].time).to.equal("09:30");
|
||||
expect(slots[2].time).to.equal("10:00");
|
||||
// All should be available since there are no existing events
|
||||
expect(slots.every((slot) => slot.available)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle buffer time that extends before start of day", () => {
|
||||
const configWithBuffer = {
|
||||
...basicEventTypeConfig,
|
||||
duration: 30,
|
||||
bufferTime: 60, // 1 hour buffer
|
||||
};
|
||||
|
||||
const earlyMorningAvailability = {
|
||||
enabled: true,
|
||||
timeRanges: [{ start: "08:00", end: "12:00" }],
|
||||
};
|
||||
|
||||
const earlyEvent: Tables<"events"> = {
|
||||
id: "1",
|
||||
start_date: "2024-01-16",
|
||||
start_time: "08:30",
|
||||
end_time: "09:00",
|
||||
created_by: "user1",
|
||||
deleted_at: null,
|
||||
title: "Early Meeting",
|
||||
description: null,
|
||||
created_at: "2024-01-15T00:00:00Z",
|
||||
tablo_id: "",
|
||||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
earlyMorningAvailability,
|
||||
configWithBuffer,
|
||||
[],
|
||||
[earlyEvent]
|
||||
);
|
||||
|
||||
// Event is 08:30-09:00, with 60min buffer: 07:30-10:00
|
||||
// Since 07:30 is before availability starts, buffer should start from 08:00
|
||||
const slot08_00 = slots.find((s) => s.time === "08:00");
|
||||
const slot08_30 = slots.find((s) => s.time === "08:30");
|
||||
const slot09_00 = slots.find((s) => s.time === "09:00");
|
||||
const slot09_30 = slots.find((s) => s.time === "09:30");
|
||||
const slot10_00 = slots.find((s) => s.time === "10:00");
|
||||
|
||||
expect(slot08_00?.available).to.be.false; // Within buffer
|
||||
expect(slot08_30?.available).to.be.false; // Within buffer
|
||||
expect(slot09_00?.available).to.be.false; // Within buffer
|
||||
expect(slot09_30?.available).to.be.false; // Within buffer (ends at 10:00)
|
||||
expect(slot10_00?.available).to.be.true; // After buffer
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -588,7 +905,8 @@ describe("generateTimeSlots", () => {
|
|||
];
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithMaxBookings,
|
||||
[],
|
||||
|
|
@ -635,7 +953,8 @@ describe("generateTimeSlots", () => {
|
|||
];
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
configWithMaxBookings,
|
||||
[],
|
||||
|
|
@ -656,7 +975,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
emptyAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -673,7 +993,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
shortRange,
|
||||
basicEventTypeConfig, // 30 minute duration
|
||||
[],
|
||||
|
|
@ -690,7 +1011,8 @@ describe("generateTimeSlots", () => {
|
|||
};
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
lateNightAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -730,7 +1052,8 @@ describe("generateTimeSlots", () => {
|
|||
];
|
||||
|
||||
const slots = generateTimeSlots(
|
||||
testDate,
|
||||
testDate, // currentTime
|
||||
testDate, // date
|
||||
basicAvailability,
|
||||
basicEventTypeConfig,
|
||||
[],
|
||||
|
|
@ -738,7 +1061,7 @@ describe("generateTimeSlots", () => {
|
|||
);
|
||||
|
||||
// Check that all conflicting slots are marked unavailable
|
||||
const conflictingTimes = ["09:30", "10:00", "10:30", "11:00", "11:30"];
|
||||
const conflictingTimes = ["10:00", "10:30", "11:00", "11:30"];
|
||||
conflictingTimes.forEach((time) => {
|
||||
const slot = slots.find((s) => s.time === time);
|
||||
expect(slot?.available).to.be.false;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,34 @@ import {
|
|||
type EventTypeConfig,
|
||||
} from "./slots.js";
|
||||
|
||||
// Helper function to get current time in CET
|
||||
function getCETTime(): Date {
|
||||
const utcNow = new Date();
|
||||
|
||||
// Use Intl.DateTimeFormat to get the correct CET/CEST time
|
||||
const formatter = new Intl.DateTimeFormat("en", {
|
||||
timeZone: "Europe/Paris",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(utcNow);
|
||||
const year = parseInt(parts.find((p) => p.type === "year")?.value || "0");
|
||||
const month =
|
||||
parseInt(parts.find((p) => p.type === "month")?.value || "0") - 1; // Month is 0-indexed
|
||||
const day = parseInt(parts.find((p) => p.type === "day")?.value || "0");
|
||||
const hour = parseInt(parts.find((p) => p.type === "hour")?.value || "0");
|
||||
const minute = parseInt(parts.find((p) => p.type === "minute")?.value || "0");
|
||||
const second = parseInt(parts.find((p) => p.type === "second")?.value || "0");
|
||||
|
||||
return new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
export const publicRouter = new Hono<{
|
||||
Variables: {
|
||||
supabase: SupabaseClient;
|
||||
|
|
@ -73,7 +101,8 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => {
|
|||
const exceptions = (availabilities.exceptions as Exception[]) || [];
|
||||
|
||||
// Get existing events for the next month
|
||||
const now = new Date();
|
||||
// Use CET time for availability calculations
|
||||
const now = getCETTime();
|
||||
const nextMonth = new Date(now);
|
||||
nextMonth.setMonth(now.getMonth() + 1);
|
||||
|
||||
|
|
@ -101,7 +130,8 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => {
|
|||
|
||||
if (dayAvailability) {
|
||||
const daySlots = generateTimeSlots(
|
||||
new Date(currentDate),
|
||||
now, // Pass CET current time as first parameter
|
||||
currentDate,
|
||||
dayAvailability,
|
||||
eventTypeConfig,
|
||||
exceptions,
|
||||
|
|
|
|||
125
api/src/slots.ts
125
api/src/slots.ts
|
|
@ -1,5 +1,34 @@
|
|||
import type { Tables } from "./database.types.js";
|
||||
|
||||
// Helper function to convert UTC date to CET
|
||||
function convertToCET(utcDate: Date): Date {
|
||||
// Create a new date object to avoid mutating the original
|
||||
const cetDate = new Date(utcDate);
|
||||
|
||||
// Use Intl.DateTimeFormat to get the correct CET/CEST offset
|
||||
const formatter = new Intl.DateTimeFormat("en", {
|
||||
timeZone: "Europe/Paris",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(utcDate);
|
||||
const year = parseInt(parts.find((p) => p.type === "year")?.value || "0");
|
||||
const month =
|
||||
parseInt(parts.find((p) => p.type === "month")?.value || "0") - 1; // Month is 0-indexed
|
||||
const day = parseInt(parts.find((p) => p.type === "day")?.value || "0");
|
||||
const hour = parseInt(parts.find((p) => p.type === "hour")?.value || "0");
|
||||
const minute = parseInt(parts.find((p) => p.type === "minute")?.value || "0");
|
||||
const second = parseInt(parts.find((p) => p.type === "second")?.value || "0");
|
||||
|
||||
return new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
// Types for availability calculation
|
||||
type TimeRange = {
|
||||
start: string;
|
||||
|
|
@ -63,8 +92,16 @@ function formatTime(hours: number, minutes: number): string {
|
|||
function addMinutes(timeStr: string, minutesToAdd: number): string {
|
||||
const { hours, minutes } = parseTime(timeStr);
|
||||
const totalMinutes = hours * 60 + minutes + minutesToAdd;
|
||||
const newHours = Math.floor(totalMinutes / 60);
|
||||
const newMinutes = totalMinutes % 60;
|
||||
|
||||
// Handle negative time (before 00:00)
|
||||
if (totalMinutes < 0) {
|
||||
return "00:00"; // Return start of day for negative times
|
||||
}
|
||||
|
||||
// Handle time beyond 24 hours
|
||||
const dayMinutes = totalMinutes % (24 * 60);
|
||||
const newHours = Math.floor(dayMinutes / 60);
|
||||
const newMinutes = dayMinutes % 60;
|
||||
return formatTime(newHours, newMinutes);
|
||||
}
|
||||
|
||||
|
|
@ -109,25 +146,28 @@ function getMinAdvanceBookingDate(
|
|||
config: EventTypeConfig,
|
||||
currentDate: Date
|
||||
): { date: string; time: string } {
|
||||
// Convert current UTC date to CET
|
||||
const cetCurrentDate = convertToCET(currentDate);
|
||||
|
||||
if (!config.minAdvanceBooking) {
|
||||
return {
|
||||
date: getDateString(currentDate),
|
||||
time: formatTime(currentDate.getHours(), currentDate.getMinutes()),
|
||||
date: getDateString(cetCurrentDate),
|
||||
time: formatTime(cetCurrentDate.getHours(), cetCurrentDate.getMinutes()),
|
||||
};
|
||||
}
|
||||
|
||||
const { value, unit } = config.minAdvanceBooking;
|
||||
const advanceDate = new Date(currentDate);
|
||||
const advanceDate = new Date(cetCurrentDate);
|
||||
|
||||
switch (unit) {
|
||||
case "minutes":
|
||||
advanceDate.setMinutes(currentDate.getMinutes() + value);
|
||||
advanceDate.setMinutes(cetCurrentDate.getMinutes() + value);
|
||||
break;
|
||||
case "hours":
|
||||
advanceDate.setHours(currentDate.getHours() + value);
|
||||
advanceDate.setHours(cetCurrentDate.getHours() + value);
|
||||
break;
|
||||
case "days":
|
||||
advanceDate.setDate(currentDate.getDate() + value);
|
||||
advanceDate.setDate(cetCurrentDate.getDate() + value);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +182,8 @@ export function getDateString(date: Date): string {
|
|||
}
|
||||
|
||||
export function generateTimeSlots(
|
||||
date: Date,
|
||||
currentTime: Date, // in CET
|
||||
date: Date, // in CET
|
||||
availability: DayAvailability,
|
||||
eventTypeConfig: EventTypeConfig,
|
||||
exceptions: Exception[],
|
||||
|
|
@ -169,10 +210,11 @@ export function generateTimeSlots(
|
|||
return slots; // Day not available
|
||||
}
|
||||
|
||||
console.log(timeRanges);
|
||||
|
||||
// Check minimum advance booking
|
||||
const minAdvanceBooking = getMinAdvanceBookingDate(eventTypeConfig, date);
|
||||
const minAdvanceBooking = getMinAdvanceBookingDate(
|
||||
eventTypeConfig,
|
||||
currentTime
|
||||
);
|
||||
|
||||
// Generate slots for each time range
|
||||
for (const range of timeRanges) {
|
||||
|
|
@ -188,25 +230,6 @@ export function generateTimeSlots(
|
|||
currentMinutes % 60
|
||||
);
|
||||
|
||||
// Check if this slot conflicts with existing events
|
||||
const slotEndTime = addMinutes(slotTime, eventTypeConfig.duration);
|
||||
const hasConflict = existingEvents.some((event) => {
|
||||
if (event.start_date !== dateStr || event.deleted_at) return false;
|
||||
|
||||
const eventStart = event.start_time;
|
||||
const eventEnd =
|
||||
event.end_time || addMinutes(eventStart, eventTypeConfig.duration);
|
||||
|
||||
// Check for overlap
|
||||
return (
|
||||
(slotTime >= eventStart && slotTime < eventEnd) ||
|
||||
(slotEndTime > eventStart && slotEndTime <= eventEnd) ||
|
||||
(slotTime <= eventStart && slotEndTime >= eventEnd)
|
||||
);
|
||||
});
|
||||
|
||||
console.log(slotTime, minAdvanceBooking.time);
|
||||
|
||||
// Check if slot is in the future (considering minimum advance booking)
|
||||
// Compare dates first, then times if on the same date
|
||||
const isInFuture =
|
||||
|
|
@ -217,12 +240,44 @@ export function generateTimeSlots(
|
|||
slots.push({
|
||||
date: dateStr,
|
||||
time: slotTime,
|
||||
available: !hasConflict && isInFuture,
|
||||
available: isInFuture, // Will be updated later based on conflicts and buffer time
|
||||
});
|
||||
|
||||
// Move to next slot (considering buffer time)
|
||||
const bufferTime = eventTypeConfig.bufferTime || 0;
|
||||
currentMinutes += eventTypeConfig.duration + bufferTime;
|
||||
// Move to next slot (without buffer time - generate all possible slots)
|
||||
currentMinutes += eventTypeConfig.duration;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply buffer time and conflict detection to all generated slots
|
||||
const bufferTime = eventTypeConfig.bufferTime || 0;
|
||||
|
||||
for (const slot of slots) {
|
||||
if (!slot.available) continue; // Skip slots already marked as unavailable
|
||||
|
||||
const slotEndTime = addMinutes(slot.time, eventTypeConfig.duration);
|
||||
|
||||
// Check if this slot conflicts with existing events (including buffer time)
|
||||
const hasConflict = existingEvents.some((event) => {
|
||||
if (event.start_date !== dateStr || event.deleted_at) return false;
|
||||
|
||||
const eventStart = event.start_time;
|
||||
const eventEnd =
|
||||
event.end_time || addMinutes(eventStart, eventTypeConfig.duration);
|
||||
|
||||
// Apply buffer time around the existing event
|
||||
const bufferedEventStart = addMinutes(eventStart, -bufferTime);
|
||||
const bufferedEventEnd = addMinutes(eventEnd, bufferTime);
|
||||
|
||||
// Check for overlap with buffered event time
|
||||
return (
|
||||
(slot.time >= bufferedEventStart && slot.time < bufferedEventEnd) ||
|
||||
(slotEndTime > bufferedEventStart && slotEndTime <= bufferedEventEnd) ||
|
||||
(slot.time <= bufferedEventStart && slotEndTime >= bufferedEventEnd)
|
||||
);
|
||||
});
|
||||
|
||||
if (hasConflict) {
|
||||
slot.available = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
1
api/test_buffer_time.js
Normal file
1
api/test_buffer_time.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -78,7 +78,6 @@ export const EventDetailsModal = ({
|
|||
}
|
||||
};
|
||||
|
||||
console.log(event.tablo_color);
|
||||
return (
|
||||
<CustomModal
|
||||
isOpen={isOpen}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@ export function EventTypeModal({
|
|||
formData.minAdvanceBooking?.unit || "minutes"
|
||||
)}
|
||||
onSelectionChange={(key) => {
|
||||
console.log(key);
|
||||
setFormData({
|
||||
...formData,
|
||||
minAdvanceBooking: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useState } from "react";
|
|||
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
||||
import { Strong, Text } from "@ui/ui-library/text";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { CustomModal } from "@ui/components/CustomModal";
|
||||
import { TextField, Label, Input, FieldError } from "@ui/ui-library/field";
|
||||
import { useTheme } from "@ui/contexts/ThemeContext";
|
||||
import {
|
||||
CalendarIcon,
|
||||
|
|
@ -38,6 +40,21 @@ export function PublicBookingPage() {
|
|||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
|
||||
// Modal state
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedSlot, setSelectedSlot] = useState<{
|
||||
date: Date;
|
||||
slot: TimeSlot;
|
||||
} | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
name: "",
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState({
|
||||
email: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
// Theme
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
|
|
@ -64,15 +81,29 @@ export function PublicBookingPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Helper function to convert date to CET timezone string (YYYY-MM-DD)
|
||||
const formatDateToCET = (date: Date): string => {
|
||||
return date.toLocaleDateString("sv-SE", { timeZone: "Europe/Paris" });
|
||||
};
|
||||
|
||||
// Helper function to get current date in CET timezone
|
||||
const getCurrentDateInCET = (): Date => {
|
||||
const now = new Date();
|
||||
const cetTime = new Date(
|
||||
now.toLocaleString("en-US", { timeZone: "Europe/Paris" })
|
||||
);
|
||||
return cetTime;
|
||||
};
|
||||
|
||||
// Get available time slots for a specific date
|
||||
const getAvailableSlots = (date: Date): TimeSlot[] => {
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
const dateStr = formatDateToCET(date);
|
||||
return slotsData[dateStr]?.filter((slot) => slot.available) || [];
|
||||
};
|
||||
|
||||
// Check if a date has any available slots
|
||||
const hasAvailableSlots = (date: Date): boolean => {
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
const dateStr = formatDateToCET(date);
|
||||
return slotsData[dateStr]?.some((slot) => slot.available) || false;
|
||||
};
|
||||
|
||||
|
|
@ -80,13 +111,21 @@ export function PublicBookingPage() {
|
|||
const getDaysInMonth = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
|
||||
// Create first day of month and get its day of week in CET
|
||||
const firstDayStr = `${year}-${String(month + 1).padStart(2, "0")}-01`;
|
||||
const firstDay = new Date(firstDayStr + "T12:00:00");
|
||||
const firstDayOfWeekInCET = new Date(
|
||||
firstDay.toLocaleString("en-US", { timeZone: "Europe/Paris" })
|
||||
).getDay();
|
||||
|
||||
// Adjust for Monday as first day of week
|
||||
const mondayStartingDay =
|
||||
firstDayOfWeekInCET === 0 ? 6 : firstDayOfWeekInCET - 1;
|
||||
|
||||
// Get number of days in month
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
// Adjust for Monday as first day of week
|
||||
const startingDayOfWeek = firstDay.getDay();
|
||||
const mondayStartingDay =
|
||||
startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1;
|
||||
|
||||
const days = [];
|
||||
|
||||
|
|
@ -97,7 +136,10 @@ export function PublicBookingPage() {
|
|||
|
||||
// Add all days of the month
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
days.push(new Date(year, month, day));
|
||||
const dayStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(
|
||||
day
|
||||
).padStart(2, "0")}`;
|
||||
days.push(new Date(dayStr + "T12:00:00"));
|
||||
}
|
||||
|
||||
return days;
|
||||
|
|
@ -116,14 +158,17 @@ export function PublicBookingPage() {
|
|||
};
|
||||
|
||||
const isToday = (date: Date) => {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
const todayInCET = getCurrentDateInCET();
|
||||
const todayStr = formatDateToCET(todayInCET);
|
||||
const dateStr = formatDateToCET(date);
|
||||
return dateStr === todayStr;
|
||||
};
|
||||
|
||||
const isPastDate = (date: Date) => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return date < today;
|
||||
const todayInCET = getCurrentDateInCET();
|
||||
const todayStr = formatDateToCET(todayInCET);
|
||||
const dateStr = formatDateToCET(date);
|
||||
return dateStr < todayStr;
|
||||
};
|
||||
|
||||
const formatMonthYear = (date: Date) => {
|
||||
|
|
@ -155,6 +200,54 @@ export function PublicBookingPage() {
|
|||
return `${hours}h ${remainingMinutes}min`;
|
||||
};
|
||||
|
||||
// Modal and form handlers
|
||||
const handleSlotClick = (date: Date, slot: TimeSlot) => {
|
||||
setSelectedSlot({ date, slot });
|
||||
setIsModalOpen(true);
|
||||
setFormData({ email: "", name: "" });
|
||||
setFormErrors({ email: "", name: "" });
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedSlot(null);
|
||||
setFormData({ email: "", name: "" });
|
||||
setFormErrors({ email: "", name: "" });
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const errors = { email: "", name: "" };
|
||||
let isValid = true;
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
errors.email = "L'adresse email est requise";
|
||||
isValid = false;
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
errors.email = "Veuillez entrer une adresse email valide";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
errors.name = "Le nom est requis";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validateForm()) {
|
||||
// TODO: Implement account creation logic
|
||||
console.log("Creating account with:", {
|
||||
email: formData.email,
|
||||
name: formData.name,
|
||||
slot: selectedSlot,
|
||||
});
|
||||
handleCloseModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
{/* Header */}
|
||||
|
|
@ -220,9 +313,10 @@ export function PublicBookingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
<ClockIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<div>
|
||||
<Strong className="text-gray-900 dark:text-white text-sm">
|
||||
{formatDuration(eventType.duration)}
|
||||
</Strong>
|
||||
<Text className="text-gray-900 dark:text-white text-sm">
|
||||
Durée:{" "}
|
||||
<Strong>{formatDuration(eventType.duration)}</Strong>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -239,7 +333,6 @@ export function PublicBookingPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{eventType?.location && (
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPinIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
|
|
@ -250,7 +343,6 @@ export function PublicBookingPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{eventType?.requiresApproval && (
|
||||
<div className="flex items-center gap-3">
|
||||
<UserIcon className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />
|
||||
|
|
@ -370,6 +462,7 @@ export function PublicBookingPage() {
|
|||
key={index}
|
||||
variant="outline"
|
||||
className="w-full justify-center py-3 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:border-blue-300 dark:hover:border-blue-600"
|
||||
onPress={() => handleSlotClick(selectedDate, slot)}
|
||||
>
|
||||
{slot.time}
|
||||
</Button>
|
||||
|
|
@ -396,6 +489,77 @@ export function PublicBookingPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Booking Modal */}
|
||||
<CustomModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
title="Créer un compte pour réserver"
|
||||
width="md"
|
||||
>
|
||||
{selectedSlot && (
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-blue-700 dark:text-blue-300">
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
<Text className="font-medium">
|
||||
{selectedSlot.date.toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-blue-700 dark:text-blue-300 mt-1">
|
||||
<ClockIcon className="w-4 h-4" />
|
||||
<Text className="font-medium">{selectedSlot.slot.time}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<TextField isRequired isInvalid={!!formErrors.name}>
|
||||
<Label>Nom complet</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
placeholder="Votre nom complet"
|
||||
/>
|
||||
{formErrors.name && <FieldError>{formErrors.name}</FieldError>}
|
||||
</TextField>
|
||||
|
||||
<TextField isRequired isInvalid={!!formErrors.email}>
|
||||
<Label>Adresse email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, email: e.target.value }))
|
||||
}
|
||||
placeholder="votre@email.com"
|
||||
/>
|
||||
{formErrors.email && <FieldError>{formErrors.email}</FieldError>}
|
||||
</TextField>
|
||||
|
||||
<div className="pt-2">
|
||||
<Text className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Un compte sera créé avec ces informations pour gérer votre
|
||||
réservation.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button variant="outline" onPress={handleCloseModal}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button variant="solid" onPress={handleSubmit}>
|
||||
Créer le compte et réserver
|
||||
</Button>
|
||||
</div>
|
||||
</CustomModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue