Task #710: Weekly availability intervals (Calendly-style) for protocol booking slots
- DB: protocol_booking_slots.days_of_week integer[] NOT NULL default {0..6} (0=Sun..6=Sat, Riyadh); pushed via drizzle-kit, legacy rows keep all-days behaviour.
- API (routes/protocol.ts): slot create/patch accept daysOfWeek (validated, deduped, sorted); new POST /protocol/booking-slots/generate (requireManageRooms) generates stepped slots in [from,to); duplicate rule is day-scoped: same start time on a different day merges the days into the existing slot (existing duration kept), fully covered times skipped; returns {created,merged,skipped,slots}; manual POST create also merges disjoint days instead of 409; matchesActiveSlot and public availability filter by Riyadh weekday.
- UI (protocol.tsx): interval-generation form (from/to/duration + day toggle pills + workdays/all-days presets), toast shows created/merged/skipped, slot list shows day names, booking dialog filters slots by chosen date weekday with stale-selection reset and empty-day hint. i18n keys added (ar/en).
- Tests: 13/13 pass incl. generation, day-scoped merge (same time different day is NOT a duplicate), duplicate skipping, validation, weekday filtering in availability + booking rejection, viewer 403 on generate.
This commit is contained in:
@@ -513,8 +513,31 @@ test("interval generation creates slots, skips duplicates, validates input", asy
|
||||
assert.equal(again.status, 201);
|
||||
const b2 = await again.json();
|
||||
assert.equal(b2.created, 0);
|
||||
assert.equal(b2.merged, 0);
|
||||
assert.equal(b2.skipped, 3);
|
||||
|
||||
// Same start times but a *different* day: not a duplicate — the day is
|
||||
// merged into the existing slots instead of being skipped.
|
||||
const otherDayGen = await api(
|
||||
adminCookie,
|
||||
"POST",
|
||||
"/protocol/booking-slots/generate",
|
||||
{
|
||||
startTime: "18:07",
|
||||
endTime: "19:37",
|
||||
durationMinutes: 30,
|
||||
daysOfWeek: [otherWeekday],
|
||||
},
|
||||
);
|
||||
assert.equal(otherDayGen.status, 201);
|
||||
const b2b = await otherDayGen.json();
|
||||
assert.equal(b2b.created, 0);
|
||||
assert.equal(b2b.merged, 3, "different day must merge, not skip");
|
||||
assert.equal(b2b.skipped, 0);
|
||||
for (const s of b2b.slots) {
|
||||
assert.deepEqual(s.daysOfWeek, [dayWeekday, otherWeekday].sort((a, b) => a - b));
|
||||
}
|
||||
|
||||
// Overlapping an existing start time (SLOT_A) only creates the free ones.
|
||||
const overlap = await api(adminCookie, "POST", "/protocol/booking-slots/generate", {
|
||||
startTime: SLOT_A,
|
||||
@@ -524,7 +547,8 @@ test("interval generation creates slots, skips duplicates, validates input", asy
|
||||
});
|
||||
assert.equal(overlap.status, 201);
|
||||
const b3 = await overlap.json();
|
||||
assert.equal(b3.skipped, 1, "existing SLOT_A start must be skipped");
|
||||
assert.equal(b3.skipped, 1, "SLOT_A already covers all days → skipped");
|
||||
assert.equal(b3.merged, 0);
|
||||
assert.equal(b3.created, 1);
|
||||
|
||||
// Invalid inputs are rejected.
|
||||
@@ -599,4 +623,26 @@ test("availability and booking validation filter slots by weekday", async () =>
|
||||
});
|
||||
assert.equal(wrongDay.status, 400);
|
||||
assert.equal((await wrongDay.json()).code, "invalid_slot");
|
||||
|
||||
// Manual create at the same start time: overlapping day → 409; a
|
||||
// disjoint day is merged into the existing slot instead.
|
||||
const dup = await api(adminCookie, "POST", "/protocol/booking-slots", {
|
||||
startTime: "21:11",
|
||||
durationMinutes: DURATION,
|
||||
daysOfWeek: [otherWeekday],
|
||||
});
|
||||
assert.equal(dup.status, 409, "same time + same day is a duplicate");
|
||||
const mergeDay = (otherWeekday + 1) % 7;
|
||||
const mergedRes = await api(adminCookie, "POST", "/protocol/booking-slots", {
|
||||
startTime: "21:11",
|
||||
durationMinutes: DURATION,
|
||||
daysOfWeek: [mergeDay],
|
||||
});
|
||||
assert.equal(mergedRes.status, 201, "same time + new day merges");
|
||||
const mergedRow = await mergedRes.json();
|
||||
assert.equal(mergedRow.id, offDay.id, "must merge into the existing slot");
|
||||
assert.deepEqual(
|
||||
mergedRow.daysOfWeek,
|
||||
[otherWeekday, mergeDay].sort((a, b) => a - b),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user