booked_slots previously recorded only (date, time), so availability checking excluded an exact time match but not a real overlap — a 4-hour service booked at 9:00 AM didn't block 11:00 AM or 1:00 PM. Add duration_minutes to booked_slots (populated by sync_booked_slot() from the booked service's own duration) and switch client-side computeAvailableSlots to a half-open-interval overlap check, also rejecting candidates that wouldn't finish before closing time. Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
85 lines
3.8 KiB
PL/PgSQL
85 lines
3.8 KiB
PL/PgSQL
-- Follow-up to Phase 2C's final whole-branch review: booked_slots only
|
|
-- recorded (date, time), so availability checking only ever excluded an
|
|
-- exact time match. A 4-hour service booked at 9:00 AM did not block
|
|
-- 11:00 AM or 1:00 PM, even though the two would physically overlap. This
|
|
-- was inert while nothing persisted (Phase 2A/2B); Phase 2C is what makes
|
|
-- it reachable by real customers, so it's fixed before that branch merges
|
|
-- rather than shipped as a known gap.
|
|
--
|
|
-- Fix: booked_slots gains duration_minutes, populated from the booked
|
|
-- service's own duration_minutes at sync time. Availability computation
|
|
-- (client-side, src/data/availability.ts) changes from an exact-time-match
|
|
-- check to a real half-open-interval overlap check: a candidate slot
|
|
-- [candidateStart, candidateStart + requestedDuration) is unavailable if it
|
|
-- overlaps ANY existing booked slot's [bookedStart, bookedStart +
|
|
-- bookedDuration) interval. booked_slots keeps exactly one row per active
|
|
-- booking (not one row per slot-grid-interval it spans) -- the overlap math
|
|
-- lives entirely in the client's pure computation function, matching this
|
|
-- project's established preference for a thin client over new DB logic.
|
|
|
|
alter table public.booked_slots add column duration_minutes integer not null default 0;
|
|
|
|
-- The default above only exists so the column can be added without a
|
|
-- backfill value for any pre-existing rows; every row from this point
|
|
-- forward is written by sync_booked_slot() below, which always supplies a
|
|
-- real duration. Drop the default so a future direct insert can't silently
|
|
-- create a zero-duration (and therefore never-overlapping, i.e. bookable
|
|
-- over) row by omitting the column.
|
|
alter table public.booked_slots alter column duration_minutes drop default;
|
|
|
|
create or replace function public.sync_booked_slot()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_duration integer;
|
|
begin
|
|
if tg_op = 'DELETE' then
|
|
if old.status in ('confirmed', 'completed') then
|
|
delete from public.booked_slots
|
|
where date = old.date and time = old.time
|
|
and not exists (
|
|
select 1 from public.bookings b
|
|
where b.date = old.date and b.time = old.time and b.status in ('confirmed', 'completed')
|
|
);
|
|
end if;
|
|
return old;
|
|
end if;
|
|
|
|
if tg_op = 'UPDATE' then
|
|
-- Covers both a status change away from 'confirmed'/'completed'
|
|
-- (cancellation) and a reschedule of a still-occupying booking to a
|
|
-- different (date, time): either way, the OLD slot may no longer have
|
|
-- any confirmed/completed booking holding it and its mirror row needs
|
|
-- to go.
|
|
if old.status in ('confirmed', 'completed')
|
|
and (old.date, old.time, old.status) is distinct from (new.date, new.time, new.status) then
|
|
delete from public.booked_slots
|
|
where date = old.date and time = old.time
|
|
and not exists (
|
|
select 1 from public.bookings b
|
|
where b.date = old.date and b.time = old.time and b.status in ('confirmed', 'completed')
|
|
);
|
|
end if;
|
|
if new.status in ('confirmed', 'completed') then
|
|
select duration_minutes into v_duration from public.services where id = new.service_id;
|
|
insert into public.booked_slots (date, time, duration_minutes)
|
|
values (new.date, new.time, coalesce(v_duration, 0))
|
|
on conflict (date, time) do update set duration_minutes = excluded.duration_minutes;
|
|
end if;
|
|
return new;
|
|
end if;
|
|
|
|
-- INSERT
|
|
if new.status in ('confirmed', 'completed') then
|
|
select duration_minutes into v_duration from public.services where id = new.service_id;
|
|
insert into public.booked_slots (date, time, duration_minutes)
|
|
values (new.date, new.time, coalesce(v_duration, 0))
|
|
on conflict (date, time) do update set duration_minutes = excluded.duration_minutes;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|