tasq/supabase/migrations/20260322190000_pm_oncall_companion_swap.sql
Marc Rejohn Castillano 049ab2c794 Added My Schedule tab in attendance screen
Allow 1 Day, Whole Week and Date Range swapping
2026-03-22 11:52:25 +08:00

228 lines
8.6 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Business rule: PM Duty always comes with ON_CALL (consecutive overnight
-- shift, same person, same calendar day). Accepting a swap that involves a
-- PM schedule must also atomically transfer the companion on_call duty
-- schedule to the new PM holder so the pairing stays intact.
--
-- Handles BOTH swap directions:
-- A) target_shift_id is PM → requester receives PM; transfer recipient's
-- companion ON_CALL to requester.
-- B) shift_id is PM → recipient receives PM; transfer requester's
-- companion ON_CALL to recipient.
--
-- Example (direction A — Normal user initiates):
-- Before: User A owns Normal (08:0017:00), User B owns PM (15:0023:00)
-- and ON_CALL (23:0007:00) on the same calendar day.
-- After accept: User A owns PM + ON_CALL, User B owns Normal.
--
-- Example (direction B — PM user initiates):
-- Before: User B owns PM + ON_CALL, User A owns Normal.
-- After accept: same result — User A owns PM + ON_CALL, User B owns Normal.
--
-- Supersedes 20260322180000_auto_reject_orphaned_swaps.sql.
CREATE OR REPLACE FUNCTION public.respond_shift_swap(p_swap_id uuid, p_action text)
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE
v_swap RECORD;
v_target_type text;
v_target_date date;
v_requester_type text;
v_requester_date date;
v_companion_id uuid;
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;
-- Idempotency guard: already terminal → nothing to do.
IF v_swap.status IN ('accepted', 'rejected') THEN
RETURN;
END IF;
-- ── ACCEPTED ──────────────────────────────────────────────────────────────
IF p_action = 'accepted' THEN
-- Permission: recipient or admin/dispatcher
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;
-- Ownership validation 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;
-- Read both schedules' types and dates BEFORE the swap so we can decide
-- companion direction independently of ownership changes.
SELECT shift_type,
DATE(start_time AT TIME ZONE 'Asia/Manila')
INTO v_target_type, v_target_date
FROM public.duty_schedules
WHERE id = v_swap.target_shift_id;
SELECT shift_type,
DATE(start_time AT TIME ZONE 'Asia/Manila')
INTO v_requester_type, v_requester_date
FROM public.duty_schedules
WHERE id = v_swap.shift_id;
-- Primary swap: transfer both schedules atomically
UPDATE public.duty_schedules
SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id
WHERE id = v_swap.shift_id; -- requester's schedule → recipient
UPDATE public.duty_schedules
SET user_id = v_swap.requester_id, swap_request_id = p_swap_id
WHERE id = v_swap.target_shift_id; -- recipient's schedule → requester
-- ── Companion ON_CALL transfer ────────────────────────────────────────
-- Direction A: target is PM → requester is the new PM holder.
-- Find companion ON_CALL previously owned by the RECIPIENT and give it
-- to the REQUESTER.
IF v_target_type = 'pm' THEN
SELECT id INTO v_companion_id
FROM public.duty_schedules
WHERE user_id = v_swap.recipient_id
AND shift_type = 'on_call'
AND DATE(start_time AT TIME ZONE 'Asia/Manila') = v_target_date
ORDER BY start_time
LIMIT 1;
IF v_companion_id IS NOT NULL THEN
UPDATE public.duty_schedules
SET user_id = v_swap.requester_id, swap_request_id = p_swap_id
WHERE id = v_companion_id;
UPDATE public.swap_requests
SET status = 'rejected', updated_at = now()
WHERE id <> p_swap_id
AND status IN ('pending', 'admin_review')
AND (
shift_id = v_companion_id
OR target_shift_id = v_companion_id
);
END IF;
-- Direction B: requester's schedule is PM → recipient is the new PM holder.
-- Find companion ON_CALL previously owned by the REQUESTER and give it
-- to the RECIPIENT.
ELSIF v_requester_type = 'pm' THEN
SELECT id INTO v_companion_id
FROM public.duty_schedules
WHERE user_id = v_swap.requester_id
AND shift_type = 'on_call'
AND DATE(start_time AT TIME ZONE 'Asia/Manila') = v_requester_date
ORDER BY start_time
LIMIT 1;
IF v_companion_id IS NOT NULL THEN
UPDATE public.duty_schedules
SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id
WHERE id = v_companion_id;
UPDATE public.swap_requests
SET status = 'rejected', updated_at = now()
WHERE id <> p_swap_id
AND status IN ('pending', 'admin_review')
AND (
shift_id = v_companion_id
OR target_shift_id = v_companion_id
);
END IF;
END IF;
-- ─────────────────────────────────────────────────────────────────────
-- Accept the swap request
UPDATE public.swap_requests
SET status = 'accepted', updated_at = now()
WHERE id = p_swap_id;
-- Auto-reject all other pending swaps referencing either primary schedule
UPDATE public.swap_requests
SET status = 'rejected', updated_at = now()
WHERE id <> p_swap_id
AND status IN ('pending', 'admin_review')
AND (
shift_id = v_swap.shift_id
OR shift_id = v_swap.target_shift_id
OR target_shift_id = v_swap.shift_id
OR target_shift_id = v_swap.target_shift_id
);
INSERT INTO public.swap_request_participants (swap_request_id, user_id, role)
VALUES (p_swap_id, auth.uid(), 'approver')
ON CONFLICT DO NOTHING;
INSERT INTO public.notifications (user_id, actor_id, type, created_at)
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
-- ── REJECTED ──────────────────────────────────────────────────────────────
ELSIF p_action = 'rejected' THEN
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;
INSERT INTO public.notifications (user_id, actor_id, type, created_at)
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
-- ── ADMIN_REVIEW ──────────────────────────────────────────────────────────
ELSE
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;
INSERT INTO public.notifications (user_id, actor_id, type, created_at)
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
END IF;
END;
$$;
-- Re-grant after SECURITY DEFINER replace
GRANT EXECUTE ON FUNCTION public.respond_shift_swap(uuid, text) TO authenticated;