Added My Schedule tab in attendance screen

Allow 1 Day, Whole Week and Date Range swapping
This commit is contained in:
2026-03-22 11:52:25 +08:00
parent ba155885c0
commit 049ab2c794
17 changed files with 2515 additions and 305 deletions
@@ -0,0 +1,227 @@
-- 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;