-- Add target_shift_id + snapshots to swap_requests, extend RPCs to support two-shift swaps ALTER TABLE public.swap_requests ADD COLUMN IF NOT EXISTS target_shift_id uuid; ALTER TABLE public.swap_requests ADD COLUMN IF NOT EXISTS target_shift_type text; ALTER TABLE public.swap_requests ADD COLUMN IF NOT EXISTS target_shift_start_time timestamptz; ALTER TABLE public.swap_requests ADD COLUMN IF NOT EXISTS target_reliever_ids uuid[] DEFAULT '{}'::uuid[]; -- Replace request_shift_swap to accept a target shift id and insert a notification CREATE OR REPLACE FUNCTION public.request_shift_swap( p_shift_id uuid, p_target_shift_id uuid, p_recipient_id uuid ) RETURNS uuid LANGUAGE plpgsql AS $$ DECLARE v_shift_record RECORD; v_target_shift RECORD; v_recipient RECORD; v_new_id uuid; BEGIN -- shift must exist and be owned by caller SELECT * INTO v_shift_record FROM public.duty_schedules WHERE id = p_shift_id; IF NOT FOUND THEN RAISE EXCEPTION 'shift not found'; END IF; IF v_shift_record.user_id <> auth.uid() THEN RAISE EXCEPTION 'permission denied: only shift owner may request swap'; END IF; -- recipient must exist and be it_staff SELECT id, role INTO v_recipient FROM public.profiles WHERE id = p_recipient_id; IF NOT FOUND THEN RAISE EXCEPTION 'recipient not found'; END IF; IF v_recipient.role <> 'it_staff' THEN RAISE EXCEPTION 'recipient must be it_staff'; END IF; -- target shift must exist and be owned by recipient SELECT * INTO v_target_shift FROM public.duty_schedules WHERE id = p_target_shift_id; IF NOT FOUND THEN RAISE EXCEPTION 'target shift not found'; END IF; IF v_target_shift.user_id <> p_recipient_id THEN RAISE EXCEPTION 'target shift not owned by recipient'; END IF; INSERT INTO public.swap_requests( requester_id, recipient_id, shift_id, target_shift_id, status, created_at, updated_at, shift_type, shift_start_time, reliever_ids, target_shift_type, target_shift_start_time, target_reliever_ids ) VALUES ( auth.uid(), p_recipient_id, p_shift_id, p_target_shift_id, 'pending', now(), now(), v_shift_record.shift_type, v_shift_record.start_time, v_shift_record.reliever_ids, v_target_shift.shift_type, v_target_shift.start_time, v_target_shift.reliever_ids ) RETURNING id INTO v_new_id; -- notify recipient about incoming swap request INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (p_recipient_id, auth.uid(), 'swap_request', now()); RETURN v_new_id; END; $$; -- Replace respond_shift_swap to swap both duty_schedules atomically on acceptance CREATE OR REPLACE FUNCTION public.respond_shift_swap(p_swap_id uuid, p_action text) RETURNS void LANGUAGE plpgsql AS $$ DECLARE v_swap RECORD; BEGIN SELECT * INTO v_swap FROM public.swap_requests WHERE id = p_swap_id; IF NOT FOUND THEN RAISE EXCEPTION 'swap request not found'; END IF; IF p_action NOT IN ('accepted','rejected','admin_review') THEN RAISE EXCEPTION 'invalid action'; END IF; IF p_action = 'accepted' THEN -- only recipient or admin/dispatcher can accept IF NOT ( v_swap.recipient_id = auth.uid() OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')) ) THEN RAISE EXCEPTION 'permission denied'; END IF; -- ensure both shifts are still owned by the expected users before swapping IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.shift_id AND user_id = v_swap.requester_id) THEN RAISE EXCEPTION 'requester shift ownership changed, cannot accept swap'; END IF; IF v_swap.target_shift_id IS NULL THEN RAISE EXCEPTION 'target shift missing'; END IF; IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.target_shift_id AND user_id = v_swap.recipient_id) THEN RAISE EXCEPTION 'target shift ownership changed, cannot accept swap'; END IF; -- perform the swap (atomic within function) UPDATE public.duty_schedules SET user_id = v_swap.recipient_id WHERE id = v_swap.shift_id; UPDATE public.duty_schedules SET user_id = v_swap.requester_id WHERE id = v_swap.target_shift_id; UPDATE public.swap_requests SET status = 'accepted', updated_at = now() WHERE id = p_swap_id; -- record approver/participant INSERT INTO public.swap_request_participants(swap_request_id, user_id, role) VALUES (p_swap_id, auth.uid(), 'approver') ON CONFLICT DO NOTHING; -- notify requester about approval INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); ELSIF p_action = 'rejected' THEN -- only recipient or admin/dispatcher can reject IF NOT ( v_swap.recipient_id = auth.uid() OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')) ) THEN RAISE EXCEPTION 'permission denied'; END IF; UPDATE public.swap_requests SET status = 'rejected', updated_at = now() WHERE id = p_swap_id; INSERT INTO public.swap_request_participants(swap_request_id, user_id, role) VALUES (p_swap_id, auth.uid(), 'approver') ON CONFLICT DO NOTHING; -- notify requester about rejection INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); ELSE -- admin_review -- only requester may escalate for admin review IF NOT (v_swap.requester_id = auth.uid()) THEN RAISE EXCEPTION 'permission denied'; END IF; UPDATE public.swap_requests SET status = 'admin_review', updated_at = now() WHERE id = p_swap_id; -- notify recipient/requester about status change INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); END IF; END; $$;