Consolidated fix wave for 1 Critical + 6 Important findings from the final review before merge: - Critical: marking a booking completed no longer frees its booked_slots mirror row or the unique-slot index, closing a double-booking hole (new migration 20260812110508, with regression pgTAP tests added first and confirmed failing against the old trigger/index). - uploadSalonImage() now returns the storage-relative path instead of an absolute public URL, so products/services.image stays one consistent format. - The product/service Id field is disabled while editing, preventing accidental duplicate rows from an upsert keyed on a changed id. - Cancel / Mark completed in AdminBookings now require window.confirm, matching the existing delete-confirmation pattern. - /admin and any unmatched /admin/* path now redirect to /admin/products instead of rendering blank. - Public signup is disabled in supabase/config.toml, and ProtectedRoute adds a defense-in-depth check that the session belongs to the admin user id, not just that a session exists. - Product/service delete now surfaces a friendly message for FK-violation errors (23503) via a new friendlyError() helper, instead of a raw Postgres error string. Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
76 lines
3.0 KiB
PL/PgSQL
76 lines
3.0 KiB
PL/PgSQL
-- Follow-up to 20260812105003 (deferred from the final whole-branch review
|
|
-- before merge). Postgres migrations are append-only history, so this
|
|
-- replaces the trigger function and the partial unique index rather than
|
|
-- editing the already-applied 20260812000002/20260812000003 files.
|
|
--
|
|
-- Problem: 20260812105003 widened bookings.status to allow 'completed', and
|
|
-- the admin panel gained a "Mark completed" action, but sync_booked_slot()
|
|
-- (20260812000003) and bookings_active_slot_idx (20260812000002) still only
|
|
-- treat status = 'confirmed' as "slot occupied". As a result:
|
|
-- 1. confirmed -> completed deletes the booked_slots mirror row for that
|
|
-- slot, exactly as if the booking had been cancelled.
|
|
-- 2. The partial unique index (where status = 'confirmed') no longer
|
|
-- blocks a second confirmed booking for a now-completed slot.
|
|
-- A completed appointment did happen and must continue to occupy its slot;
|
|
-- only 'cancelled' should free it.
|
|
--
|
|
-- Fix: every place sync_booked_slot() tested status = 'confirmed' now tests
|
|
-- status in ('confirmed', 'completed'), and the partial unique index is
|
|
-- rebuilt with the same predicate.
|
|
create or replace function public.sync_booked_slot()
|
|
returns trigger
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
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
|
|
insert into public.booked_slots (date, time)
|
|
values (new.date, new.time)
|
|
on conflict do nothing;
|
|
end if;
|
|
return new;
|
|
end if;
|
|
|
|
-- INSERT
|
|
if new.status in ('confirmed', 'completed') then
|
|
insert into public.booked_slots (date, time)
|
|
values (new.date, new.time)
|
|
on conflict do nothing;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
drop index if exists public.bookings_active_slot_idx;
|
|
create unique index bookings_active_slot_idx
|
|
on public.bookings (date, time)
|
|
where status in ('confirmed', 'completed');
|