Swap Accept, Reject and Escalate
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
-- Migration kept minimal because `swap_requests` & participants already exist in many deployments.
|
||||
-- This migration ensures the RPCs are present and aligns behavior with the existing schema
|
||||
-- (no `approved_by` column on `swap_requests`; approvals are tracked in `swap_request_participants`).
|
||||
|
||||
-- Ensure chat_thread_id column exists (no-op if already present)
|
||||
ALTER TABLE public.swap_requests
|
||||
ADD COLUMN IF NOT EXISTS chat_thread_id uuid;
|
||||
|
||||
-- Idempotent RPC: request_shift_swap(shift_id, recipient_id) -> uuid
|
||||
CREATE OR REPLACE FUNCTION public.request_shift_swap(p_shift_id uuid, p_recipient_id uuid)
|
||||
RETURNS uuid LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_shift_record 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;
|
||||
|
||||
INSERT INTO public.swap_requests(requester_id, recipient_id, shift_id, status, created_at, updated_at)
|
||||
VALUES (auth.uid(), p_recipient_id, p_shift_id, 'pending', now(), now())
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
RETURN v_new_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Idempotent RPC: respond_shift_swap(p_swap_id, p_action)
|
||||
-- Updates status and records approver in swap_request_participants (no approved_by column required)
|
||||
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 the shift is still owned by the requester before swapping
|
||||
UPDATE public.duty_schedules
|
||||
SET user_id = v_swap.recipient_id
|
||||
WHERE id = v_swap.shift_id AND user_id = v_swap.requester_id;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'shift ownership changed, cannot accept swap';
|
||||
END IF;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,44 @@
|
||||
-- RLS policies for swap_request_participants
|
||||
-- Allow participants, swap owners and admins/dispatchers to view/insert participant rows
|
||||
|
||||
ALTER TABLE public.swap_request_participants ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- SELECT: participants, swap requester/recipient, admins/dispatchers
|
||||
DROP POLICY IF EXISTS "Swap participants: select" ON public.swap_request_participants;
|
||||
CREATE POLICY "Swap participants: select" ON public.swap_request_participants
|
||||
FOR SELECT
|
||||
USING (
|
||||
user_id = auth.uid()
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.swap_requests s WHERE s.id = swap_request_id AND (s.requester_id = auth.uid() OR s.recipient_id = auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- INSERT: allow user to insert their own participant row, or allow admins/dispatchers
|
||||
DROP POLICY IF EXISTS "Swap participants: insert" ON public.swap_request_participants;
|
||||
CREATE POLICY "Swap participants: insert" ON public.swap_request_participants
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
user_id = auth.uid()
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')
|
||||
)
|
||||
);
|
||||
|
||||
-- UPDATE/DELETE: only admins can modify or remove participant rows
|
||||
DROP POLICY IF EXISTS "Swap participants: admin manage" ON public.swap_request_participants;
|
||||
CREATE POLICY "Swap participants: admin manage" ON public.swap_request_participants
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role = 'admin'
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
-- Add shift snapshot columns to swap_requests and update request_shift_swap RPC
|
||||
|
||||
ALTER TABLE public.swap_requests
|
||||
ADD COLUMN IF NOT EXISTS shift_type text;
|
||||
|
||||
ALTER TABLE public.swap_requests
|
||||
ADD COLUMN IF NOT EXISTS shift_start_time timestamptz;
|
||||
|
||||
ALTER TABLE public.swap_requests
|
||||
ADD COLUMN IF NOT EXISTS reliever_ids uuid[] DEFAULT '{}'::uuid[];
|
||||
|
||||
-- Update the request_shift_swap RPC so inserted swap_requests include a snapshot
|
||||
-- of the referenced duty schedule (so UI can render shift info even after ownership changes)
|
||||
CREATE OR REPLACE FUNCTION public.request_shift_swap(p_shift_id uuid, p_recipient_id uuid)
|
||||
RETURNS uuid LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_shift_record 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;
|
||||
|
||||
INSERT INTO public.swap_requests(
|
||||
requester_id, recipient_id, shift_id, status, created_at, updated_at,
|
||||
shift_type, shift_start_time, reliever_ids
|
||||
)
|
||||
VALUES (
|
||||
auth.uid(), p_recipient_id, p_shift_id, 'pending', now(), now(),
|
||||
v_shift_record.shift_type, v_shift_record.start_time, v_shift_record.reliever_ids
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
RETURN v_new_id;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,168 @@
|
||||
-- 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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user