Migrations

This commit is contained in:
Marc Rejohn Castillano 2026-03-08 16:58:05 +08:00
parent d87b5e73d7
commit ce82a88e04
19 changed files with 1526 additions and 0 deletions

View File

@ -0,0 +1,15 @@
-- Add religion column to profiles with PH-common defaults
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS religion text NOT NULL DEFAULT 'catholic';
-- Add tracking consent column
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS allow_tracking boolean NOT NULL DEFAULT false;
-- Seed Ramadan mode setting
INSERT INTO app_settings (key, value)
VALUES ('ramadan_mode', '{"enabled": false, "auto_detect": true}'::jsonb)
ON CONFLICT (key) DO NOTHING;
-- RLS: all authenticated users can read religion; users can update their own;
-- admins can update anyone's religion.
-- (Existing profile RLS already allows select; we only need to ensure update
-- policies cover the new columns.)

View File

@ -0,0 +1,46 @@
-- Chat messages for swap request bargaining
CREATE TABLE IF NOT EXISTS chat_messages (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
thread_id uuid NOT NULL,
sender_id uuid NOT NULL REFERENCES auth.users(id),
body text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_chat_messages_thread ON chat_messages (thread_id, created_at);
-- Auto-generate chat_thread_id on swap_requests if null
ALTER TABLE swap_requests ALTER COLUMN chat_thread_id SET DEFAULT gen_random_uuid();
-- Backfill existing swap_requests that have no chat_thread_id
UPDATE swap_requests SET chat_thread_id = gen_random_uuid() WHERE chat_thread_id IS NULL;
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE chat_messages;
-- RLS
ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY;
-- Users can read messages for swap requests where they are requester or recipient
CREATE POLICY "chat_messages_select" ON chat_messages FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM swap_requests sr
WHERE sr.chat_thread_id = chat_messages.thread_id
AND (sr.requester_id = auth.uid() OR sr.recipient_id = auth.uid())
)
OR EXISTS (
SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')
)
);
-- Users can insert messages for swap requests where they are requester or recipient
CREATE POLICY "chat_messages_insert" ON chat_messages FOR INSERT TO authenticated
WITH CHECK (
sender_id = auth.uid()
AND EXISTS (
SELECT 1 FROM swap_requests sr
WHERE sr.chat_thread_id = chat_messages.thread_id
AND (sr.requester_id = auth.uid() OR sr.recipient_id = auth.uid())
)
);

View File

@ -0,0 +1,44 @@
-- Pass slip system for duty excusals
CREATE TABLE IF NOT EXISTS pass_slips (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id),
duty_schedule_id uuid NOT NULL REFERENCES duty_schedules(id),
reason text NOT NULL,
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'completed')),
requested_at timestamptz NOT NULL DEFAULT now(),
approved_by uuid REFERENCES auth.users(id),
approved_at timestamptz,
slip_start timestamptz,
slip_end timestamptz
);
CREATE INDEX idx_pass_slips_user ON pass_slips (user_id, requested_at DESC);
CREATE INDEX idx_pass_slips_status ON pass_slips (status) WHERE status IN ('pending', 'approved');
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE pass_slips;
-- RLS
ALTER TABLE pass_slips ENABLE ROW LEVEL SECURITY;
-- Users can see their own pass slips; admin/dispatcher can see all
CREATE POLICY "pass_slips_select" ON pass_slips FOR SELECT TO authenticated
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')
)
);
-- Users can insert their own pass slips
CREATE POLICY "pass_slips_insert" ON pass_slips FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
-- Admins can update pass slips (approve/reject); users can complete their own
CREATE POLICY "pass_slips_update" ON pass_slips FOR UPDATE TO authenticated
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')
)
);

View File

@ -0,0 +1,163 @@
-- Attendance log book for check-in / check-out
CREATE TABLE IF NOT EXISTS attendance_logs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id),
duty_schedule_id uuid NOT NULL REFERENCES duty_schedules(id),
check_in_at timestamptz NOT NULL DEFAULT now(),
check_in_lat double precision NOT NULL,
check_in_lng double precision NOT NULL,
check_out_at timestamptz,
check_out_lat double precision,
check_out_lng double precision
);
CREATE INDEX idx_attendance_logs_user ON attendance_logs (user_id, check_in_at DESC);
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE attendance_logs;
-- RLS
ALTER TABLE attendance_logs ENABLE ROW LEVEL SECURITY;
-- Users can see their own logs; admin/dispatcher/it_staff can see all
CREATE POLICY "attendance_logs_select" ON attendance_logs FOR SELECT TO authenticated
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher', 'it_staff')
)
);
-- Users can insert their own logs
CREATE POLICY "attendance_logs_insert" ON attendance_logs FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
-- Users can update their own logs (for check-out)
CREATE POLICY "attendance_logs_update" ON attendance_logs FOR UPDATE TO authenticated
USING (user_id = auth.uid());
-- RPC: attendance_check_in — validates geofence server-side, checks 2-hour window
CREATE OR REPLACE FUNCTION public.attendance_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_geofence jsonb;
v_log_id uuid;
v_now timestamptz := now();
v_status text;
BEGIN
-- Fetch the duty schedule
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
-- Check 2-hour window
IF v_now < (v_schedule.start_time - interval '2 hours') THEN
RAISE EXCEPTION 'Too early to check in (2-hour window)';
END IF;
IF v_now > v_schedule.end_time THEN
RAISE EXCEPTION 'Duty has already ended';
END IF;
-- Determine status
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
-- Insert attendance log
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng)
RETURNING id INTO v_log_id;
-- Update duty schedule
UPDATE duty_schedules
SET check_in_at = v_now,
check_in_location = jsonb_build_object('latitude', p_lat, 'longitude', p_lng),
status = v_status
WHERE id = p_duty_id;
RETURN v_log_id;
END;
$$;
-- RPC: attendance_check_out
CREATE OR REPLACE FUNCTION public.attendance_check_out(
p_attendance_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_log attendance_logs%ROWTYPE;
BEGIN
SELECT * INTO v_log FROM attendance_logs WHERE id = p_attendance_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Attendance log not found';
END IF;
IF v_log.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your attendance log';
END IF;
IF v_log.check_out_at IS NOT NULL THEN
RAISE EXCEPTION 'Already checked out';
END IF;
UPDATE attendance_logs
SET check_out_at = now(),
check_out_lat = p_lat,
check_out_lng = p_lng
WHERE id = p_attendance_id;
END;
$$;
-- Also create the missing duty_check_in RPC that workforce_screen.dart calls
CREATE OR REPLACE FUNCTION public.duty_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS text
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_now timestamptz := now();
v_status text;
BEGIN
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
UPDATE duty_schedules
SET check_in_at = v_now,
check_in_location = jsonb_build_object('latitude', p_lat, 'longitude', p_lng),
status = v_status
WHERE id = p_duty_id;
-- Also create an attendance log entry
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng);
RETURN v_status;
END;
$$;

View File

@ -0,0 +1,85 @@
-- Live position tracking for whereabouts map
CREATE TABLE IF NOT EXISTS live_positions (
user_id uuid PRIMARY KEY REFERENCES auth.users(id),
lat double precision NOT NULL,
lng double precision NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
in_premise boolean NOT NULL DEFAULT false
);
-- Enable realtime
ALTER PUBLICATION supabase_realtime ADD TABLE live_positions;
-- RLS
ALTER TABLE live_positions ENABLE ROW LEVEL SECURITY;
-- All authenticated users can read live positions
CREATE POLICY "live_positions_select" ON live_positions FOR SELECT TO authenticated
USING (true);
-- Users can only upsert their own position
CREATE POLICY "live_positions_insert" ON live_positions FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
CREATE POLICY "live_positions_update" ON live_positions FOR UPDATE TO authenticated
USING (user_id = auth.uid());
-- RPC: update_live_position — upserts position, computes in_premise via geofence
CREATE OR REPLACE FUNCTION public.update_live_position(
p_lat double precision,
p_lng double precision
) RETURNS boolean
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_geofence jsonb;
v_in_premise boolean := false;
v_polygon jsonb;
v_point_count int;
v_i int;
v_j int;
v_xi double precision;
v_yi double precision;
v_xj double precision;
v_yj double precision;
v_inside boolean := false;
BEGIN
-- Fetch geofence config
SELECT value INTO v_geofence FROM app_settings WHERE key = 'geofence';
IF v_geofence IS NOT NULL THEN
v_polygon := COALESCE(v_geofence->'polygon', v_geofence->'points');
IF v_polygon IS NOT NULL AND jsonb_array_length(v_polygon) > 2 THEN
-- Ray-casting point-in-polygon
v_point_count := jsonb_array_length(v_polygon);
v_j := v_point_count - 1;
FOR v_i IN 0..(v_point_count - 1) LOOP
v_xi := (v_polygon->v_i->>'lng')::double precision;
v_yi := (v_polygon->v_i->>'lat')::double precision;
v_xj := (v_polygon->v_j->>'lng')::double precision;
v_yj := (v_polygon->v_j->>'lat')::double precision;
IF ((v_yi > p_lat) != (v_yj > p_lat)) AND
(p_lng < (v_xj - v_xi) * (p_lat - v_yi) / (v_yj - v_yi) + v_xi) THEN
v_inside := NOT v_inside;
END IF;
v_j := v_i;
END LOOP;
v_in_premise := v_inside;
END IF;
END IF;
INSERT INTO live_positions (user_id, lat, lng, updated_at, in_premise)
VALUES (auth.uid(), p_lat, p_lng, now(), v_in_premise)
ON CONFLICT (user_id)
DO UPDATE SET lat = p_lat, lng = p_lng, updated_at = now(), in_premise = v_in_premise;
RETURN v_in_premise;
END;
$$;
-- Update the duty_schedules RLS to allow all authenticated users to SELECT
-- (previously only admin/dispatcher could see all; now all roles can see all schedules)
DROP POLICY IF EXISTS "duty_schedules_select" ON duty_schedules;
CREATE POLICY "duty_schedules_select" ON duty_schedules FOR SELECT TO authenticated
USING (true);

View File

@ -0,0 +1,98 @@
-- Fix check_in_location to use PostGIS geography instead of JSONB
-- Update attendance_check_in RPC to use geography point
CREATE OR REPLACE FUNCTION public.attendance_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_geofence jsonb;
v_log_id uuid;
v_now timestamptz := now();
v_status text;
BEGIN
-- Fetch the duty schedule
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
-- Check 2-hour window
IF v_now < (v_schedule.start_time - interval '2 hours') THEN
RAISE EXCEPTION 'Too early to check in (2-hour window)';
END IF;
IF v_now > v_schedule.end_time THEN
RAISE EXCEPTION 'Duty has already ended';
END IF;
-- Determine status
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
-- Insert attendance log
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng)
RETURNING id INTO v_log_id;
-- Update duty schedule with geography point (longitude, latitude order)
UPDATE duty_schedules
SET check_in_at = v_now,
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
status = v_status::duty_status
WHERE id = p_duty_id;
RETURN v_log_id;
END;
$$;
-- Update duty_check_in RPC to use geography point
CREATE OR REPLACE FUNCTION public.duty_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS text
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_now timestamptz := now();
v_status text;
BEGIN
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
-- Update duty schedule with geography point (longitude, latitude order)
UPDATE duty_schedules
SET check_in_at = v_now,
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
status = v_status::duty_status
WHERE id = p_duty_id;
-- Also create an attendance log entry
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng);
RETURN v_status;
END;
$$;

View File

@ -0,0 +1,49 @@
-- Add justification column to attendance_logs for overtime check-ins
ALTER TABLE attendance_logs ADD COLUMN IF NOT EXISTS justification text;
-- Overtime check-in RPC: creates overtime duty_schedule + attendance_log in one call.
-- Returns the attendance log ID.
CREATE OR REPLACE FUNCTION public.overtime_check_in(
p_lat double precision,
p_lng double precision,
p_justification text DEFAULT NULL
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_uid uuid := auth.uid();
v_now timestamptz := now();
v_schedule_id uuid;
v_log_id uuid;
v_end_time timestamptz;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
-- Overtime shift: start now, end after 8 hours (can be checked out earlier)
v_end_time := v_now + interval '8 hours';
-- Create overtime duty schedule
INSERT INTO duty_schedules (user_id, shift_type, start_time, end_time, status, check_in_at, check_in_location)
VALUES (
v_uid,
-- Store as a normal shift_type to satisfy enum constraints;
-- overtime semantics are captured by this RPC + log justification.
'normal',
v_now,
v_end_time,
'arrival'::duty_status,
v_now,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
)
RETURNING id INTO v_schedule_id;
-- Create attendance log
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng, justification)
VALUES (v_uid, v_schedule_id, v_now, p_lat, p_lng, p_justification)
RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$;

View File

@ -0,0 +1,42 @@
-- Fix overtime_check_in RPC: use valid shift_type enum label.
-- Some environments reject 'overtime' for duty_schedules.shift_type.
CREATE OR REPLACE FUNCTION public.overtime_check_in(
p_lat double precision,
p_lng double precision,
p_justification text DEFAULT NULL
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_uid uuid := auth.uid();
v_now timestamptz := now();
v_schedule_id uuid;
v_log_id uuid;
v_end_time timestamptz;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
v_end_time := v_now + interval '8 hours';
INSERT INTO duty_schedules (user_id, shift_type, start_time, end_time, status, check_in_at, check_in_location)
VALUES (
v_uid,
'normal',
v_now,
v_end_time,
'arrival'::duty_status,
v_now,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
)
RETURNING id INTO v_schedule_id;
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng, justification)
VALUES (v_uid, v_schedule_id, v_now, p_lat, p_lng, p_justification)
RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$;

View File

@ -0,0 +1,100 @@
-- Allow multiple check-in/out within a single duty schedule.
-- Preserves first check-in time & arrival/late status on subsequent re-check-ins.
CREATE OR REPLACE FUNCTION public.attendance_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_log_id uuid;
v_now timestamptz := now();
v_status text;
BEGIN
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
IF v_now < (v_schedule.start_time - interval '2 hours') THEN
RAISE EXCEPTION 'Too early to check in (2-hour window)';
END IF;
IF v_now > v_schedule.end_time THEN
RAISE EXCEPTION 'Duty has already ended';
END IF;
-- Determine status (used only when first check-in)
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
-- Insert a new attendance log row for each check-in
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng)
RETURNING id INTO v_log_id;
-- Only update duty schedule on first check-in (preserve original arrival status)
UPDATE duty_schedules
SET check_in_at = COALESCE(check_in_at, v_now),
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
status = CASE
WHEN check_in_at IS NULL THEN v_status::duty_status
ELSE status
END
WHERE id = p_duty_id;
RETURN v_log_id;
END;
$$;
-- Also update duty_check_in for consistency
CREATE OR REPLACE FUNCTION public.duty_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS text
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_now timestamptz := now();
v_status text;
BEGIN
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
UPDATE duty_schedules
SET check_in_at = COALESCE(check_in_at, v_now),
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
status = CASE
WHEN check_in_at IS NULL THEN v_status::duty_status
ELSE status
END
WHERE id = p_duty_id;
-- Create attendance log entry
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng);
RETURN v_status;
END;
$$;

View File

@ -0,0 +1,147 @@
-- Leave of Absence table
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'leave_type') THEN
CREATE TYPE leave_type AS ENUM (
'emergency_leave',
'parental_leave',
'sick_leave',
'vacation_leave'
);
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'leave_status') THEN
CREATE TYPE leave_status AS ENUM (
'pending',
'approved',
'rejected',
'cancelled'
);
END IF;
END $$;
CREATE TABLE IF NOT EXISTS leave_of_absence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
leave_type leave_type NOT NULL,
justification TEXT NOT NULL,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
status leave_status NOT NULL DEFAULT 'pending',
filed_by UUID NOT NULL REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT valid_time_range CHECK (end_time > start_time)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_leave_user_id ON leave_of_absence(user_id);
CREATE INDEX IF NOT EXISTS idx_leave_start_time ON leave_of_absence(start_time);
CREATE INDEX IF NOT EXISTS idx_leave_status ON leave_of_absence(status);
-- RLS
ALTER TABLE leave_of_absence ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Privileged users can view all leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Users can view own leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Privileged users can file own leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Users can file own leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Admins can approve or reject leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Users can cancel own future approved leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Admins can cancel future approved leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Privileged users can update leaves" ON leave_of_absence;
DROP POLICY IF EXISTS "Users can cancel own pending leaves" ON leave_of_absence;
-- Admin/dispatcher/it_staff can see all leaves
CREATE POLICY "Privileged users can view all leaves"
ON leave_of_absence FOR SELECT
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role IN ('admin', 'dispatcher', 'it_staff')
)
);
-- All users can see their own leaves
CREATE POLICY "Users can view own leaves"
ON leave_of_absence FOR SELECT
USING (user_id = auth.uid());
-- Only admin/dispatcher/it_staff can file leaves (for themselves)
CREATE POLICY "Privileged users can file own leaves"
ON leave_of_absence FOR INSERT
WITH CHECK (
user_id = auth.uid()
AND filed_by = auth.uid()
AND EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role IN ('admin', 'dispatcher', 'it_staff')
)
);
-- Only admins can approve/reject pending leaves
CREATE POLICY "Admins can approve or reject leaves"
ON leave_of_absence FOR UPDATE
USING (
status = 'pending'
AND EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
)
WITH CHECK (
status IN ('approved', 'rejected')
);
-- Users can cancel their own future approved leaves
CREATE POLICY "Users can cancel own future approved leaves"
ON leave_of_absence FOR UPDATE
USING (
user_id = auth.uid()
AND status = 'approved'
AND start_time > now()
)
WITH CHECK (
status = 'cancelled'
);
-- Admins can cancel future approved leaves
CREATE POLICY "Admins can cancel future approved leaves"
ON leave_of_absence FOR UPDATE
USING (
status = 'approved'
AND start_time > now()
AND EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
)
WITH CHECK (
status = 'cancelled'
);
-- Drop legacy reason column if it exists from an earlier migration
ALTER TABLE leave_of_absence DROP COLUMN IF EXISTS reason;
-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_leave_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_leave_updated_at ON leave_of_absence;
CREATE TRIGGER trigger_leave_updated_at
BEFORE UPDATE ON leave_of_absence
FOR EACH ROW
EXECUTE FUNCTION update_leave_updated_at();

View File

@ -0,0 +1,158 @@
-- ───────────────────────────────────────────────────────────
-- Profile: avatar + face enrollment
-- ───────────────────────────────────────────────────────────
-- avatar_url and face_photo_url may already exist; ADD IF NOT EXISTS
-- Postgres 11+ doesn't have ADD COLUMN IF NOT EXISTS in a clean way,
-- so we wrap each in a DO block.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'profiles' AND column_name = 'avatar_url'
) THEN
ALTER TABLE profiles ADD COLUMN avatar_url TEXT;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'profiles' AND column_name = 'face_photo_url'
) THEN
ALTER TABLE profiles ADD COLUMN face_photo_url TEXT;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'profiles' AND column_name = 'face_enrolled_at'
) THEN
ALTER TABLE profiles ADD COLUMN face_enrolled_at TIMESTAMPTZ;
END IF;
END $$;
-- ───────────────────────────────────────────────────────────
-- Attendance logs: verification status + selfie
-- ───────────────────────────────────────────────────────────
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'verification_status') THEN
CREATE TYPE verification_status AS ENUM (
'pending',
'verified',
'unverified',
'skipped'
);
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'attendance_logs' AND column_name = 'verification_status'
) THEN
ALTER TABLE attendance_logs
ADD COLUMN verification_status verification_status NOT NULL DEFAULT 'pending';
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'attendance_logs' AND column_name = 'verification_photo_url'
) THEN
ALTER TABLE attendance_logs ADD COLUMN verification_photo_url TEXT;
END IF;
END $$;
-- ───────────────────────────────────────────────────────────
-- Storage buckets (idempotent via INSERT ... ON CONFLICT)
-- ───────────────────────────────────────────────────────────
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true)
ON CONFLICT (id) DO NOTHING;
INSERT INTO storage.buckets (id, name, public)
VALUES ('face-enrollment', 'face-enrollment', false)
ON CONFLICT (id) DO NOTHING;
INSERT INTO storage.buckets (id, name, public)
VALUES ('attendance-verification', 'attendance-verification', false)
ON CONFLICT (id) DO NOTHING;
-- Storage policies: avatars (public read, auth write own)
DROP POLICY IF EXISTS "Public can view avatars" ON storage.objects;
CREATE POLICY "Public can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
DROP POLICY IF EXISTS "Users can upload own avatar" ON storage.objects;
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
DROP POLICY IF EXISTS "Users can update own avatar" ON storage.objects;
CREATE POLICY "Users can update own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- Storage policies: face-enrollment (private, owner read/write, admin read)
DROP POLICY IF EXISTS "Users can upload own face" ON storage.objects;
CREATE POLICY "Users can upload own face"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'face-enrollment'
AND (storage.foldername(name))[1] = auth.uid()::text
);
DROP POLICY IF EXISTS "Users can view own face" ON storage.objects;
CREATE POLICY "Users can view own face"
ON storage.objects FOR SELECT
USING (
bucket_id = 'face-enrollment'
AND (
(storage.foldername(name))[1] = auth.uid()::text
OR EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid() AND profiles.role = 'admin'
)
)
);
-- Storage policies: attendance-verification (owner + admin read, owner write)
DROP POLICY IF EXISTS "Users can upload verification photo" ON storage.objects;
CREATE POLICY "Users can upload verification photo"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'attendance-verification'
AND (storage.foldername(name))[1] = auth.uid()::text
);
DROP POLICY IF EXISTS "Users and admins can view verification photos" ON storage.objects;
CREATE POLICY "Users and admins can view verification photos"
ON storage.objects FOR SELECT
USING (
bucket_id = 'attendance-verification'
AND (
(storage.foldername(name))[1] = auth.uid()::text
OR EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role IN ('admin', 'dispatcher', 'it_staff')
)
)
);

View File

@ -0,0 +1,43 @@
-- Cross-device face verification sessions.
-- Created when web cannot access a camera and generates a QR code.
-- Mobile scans the QR, performs liveness detection, and completes the session.
CREATE TABLE IF NOT EXISTS verification_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
type TEXT NOT NULL CHECK (type IN ('enrollment', 'verification')),
context_id TEXT, -- e.g. attendance_log_id for verification type
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'expired')),
image_url TEXT, -- filled when mobile completes the session
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + interval '5 minutes')
);
-- Index for quick lookup by user + status
CREATE INDEX idx_verification_sessions_user_status
ON verification_sessions (user_id, status);
-- Enable realtime for this table
ALTER PUBLICATION supabase_realtime ADD TABLE verification_sessions;
-- RLS policies
ALTER TABLE verification_sessions ENABLE ROW LEVEL SECURITY;
-- Users can read their own sessions
CREATE POLICY "Users can read own verification sessions"
ON verification_sessions FOR SELECT
USING (auth.uid() = user_id);
-- Users can create their own sessions
CREATE POLICY "Users can create own verification sessions"
ON verification_sessions FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update their own sessions (complete from mobile)
CREATE POLICY "Users can update own verification sessions"
ON verification_sessions FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete their own sessions (cleanup)
CREATE POLICY "Users can delete own verification sessions"
ON verification_sessions FOR DELETE
USING (auth.uid() = user_id);

View File

@ -0,0 +1,24 @@
-- ───────────────────────────────────────────────────────────
-- Profiles: Enable RLS and allow users to update their own profile
-- ───────────────────────────────────────────────────────────
-- Enable RLS if not already enabled
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Remove recursive admin policy (caused infinite recursion in earlier revision)
DROP POLICY IF EXISTS "Admins can view all profiles" ON profiles;
-- Drop existing policy if it exists
DROP POLICY IF EXISTS "Users can update own profile" ON profiles;
-- Allow users to update their own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
-- Also ensure users can select their own profile
DROP POLICY IF EXISTS "Users can view own profile" ON profiles;
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);

View File

@ -0,0 +1,41 @@
-- ───────────────────────────────────────────────────────────
-- Fix storage policies:
-- 1. Add missing UPDATE policy for face-enrollment (needed for upsert)
-- 2. Remove recursive profiles subqueries from SELECT policies
-- ───────────────────────────────────────────────────────────
-- face-enrollment: allow users to UPDATE (overwrite) their own face photo
DROP POLICY IF EXISTS "Users can update own face" ON storage.objects;
CREATE POLICY "Users can update own face"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'face-enrollment'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- face-enrollment: fix SELECT to remove recursive profiles subquery
DROP POLICY IF EXISTS "Users can view own face" ON storage.objects;
CREATE POLICY "Users can view own face"
ON storage.objects FOR SELECT
USING (
bucket_id = 'face-enrollment'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- attendance-verification: allow users to UPDATE (overwrite) their own photo
DROP POLICY IF EXISTS "Users can update verification photo" ON storage.objects;
CREATE POLICY "Users can update verification photo"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'attendance-verification'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- attendance-verification: fix SELECT to remove recursive profiles subquery
DROP POLICY IF EXISTS "Users and admins can view verification photos" ON storage.objects;
CREATE POLICY "Users and admins can view verification photos"
ON storage.objects FOR SELECT
USING (
bucket_id = 'attendance-verification'
AND (storage.foldername(name))[1] = auth.uid()::text
);

View File

@ -0,0 +1,59 @@
-- Step 1 only: add 'overtime' to shift_type.
--
-- IMPORTANT (PostgreSQL enum rule):
-- A newly added enum label cannot be used in the same transaction.
-- Therefore, function/data updates that reference 'overtime' are moved
-- to the next migration file: 20260307170100_overtime_shift_type_apply.sql.
-- 1. Extend the shift_type enum with 'overtime'.
-- If the column uses a PostgreSQL ENUM type use ADD VALUE.
-- Wrapped in a DO block so it is safe to re-run.
DO $$
BEGIN
-- Try adding to an existing enum type first
BEGIN
ALTER TYPE shift_type ADD VALUE IF NOT EXISTS 'overtime';
EXCEPTION WHEN undefined_object THEN
-- Not an enum type fall through to check constraint below
NULL;
END;
END $$;
-- If shift_type is constrained by a CHECK instead of an enum,
-- drop the old CHECK and recreate it with 'overtime' included.
DO $$
DECLARE
_con text;
_is_enum boolean := false;
BEGIN
SELECT (t.typtype = 'e') INTO _is_enum
FROM pg_attribute a
JOIN pg_type t ON t.oid = a.atttypid
WHERE a.attrelid = 'duty_schedules'::regclass
AND a.attname = 'shift_type'
AND NOT a.attisdropped;
-- If the column is enum-backed, do not touch any CHECK constraints here;
-- using the new enum label in this same transaction would trigger 55P04.
IF _is_enum THEN
RETURN;
END IF;
SELECT con.conname INTO _con
FROM pg_constraint con
JOIN pg_attribute att ON att.attnum = ANY(con.conkey)
AND att.attrelid = con.conrelid
WHERE con.conrelid = 'duty_schedules'::regclass
AND att.attname = 'shift_type'
AND con.contype = 'c'; -- CHECK constraint
IF _con IS NOT NULL THEN
EXECUTE format('ALTER TABLE duty_schedules DROP CONSTRAINT %I', _con);
ALTER TABLE duty_schedules
ADD CONSTRAINT duty_schedules_shift_type_check
CHECK (shift_type IN ('normal','am','pm','on_call','weekend','overtime'));
END IF;
END $$;
-- Step 2 is intentionally separated to avoid:
-- ERROR 55P04 unsafe use of new value of enum type shift_type

View File

@ -0,0 +1,54 @@
-- Step 2: apply overtime usage after enum value is committed.
-- This migration must run after 20260307170000_overtime_shift_type.sql.
-- Recreate overtime_check_in to use 'overtime' shift_type.
CREATE OR REPLACE FUNCTION public.overtime_check_in(
p_lat double precision,
p_lng double precision,
p_justification text DEFAULT NULL
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_uid uuid := auth.uid();
v_now timestamptz := now();
v_schedule_id uuid;
v_log_id uuid;
v_end_time timestamptz;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
v_end_time := v_now + interval '8 hours';
INSERT INTO duty_schedules (user_id, shift_type, start_time, end_time, status, check_in_at, check_in_location)
VALUES (
v_uid,
'overtime',
v_now,
v_end_time,
'arrival'::duty_status,
v_now,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
)
RETURNING id INTO v_schedule_id;
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng, justification)
VALUES (v_uid, v_schedule_id, v_now, p_lat, p_lng, p_justification)
RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$;
-- Retroactively fix existing overtime schedules.
-- Any duty_schedule linked to an attendance_log with a non-empty
-- justification was created by overtime_check_in and should be marked overtime.
UPDATE duty_schedules ds
SET shift_type = 'overtime'
FROM attendance_logs al
WHERE al.duty_schedule_id = ds.id
AND al.justification IS NOT NULL
AND TRIM(al.justification) <> ''
AND ds.shift_type <> 'overtime';

View File

@ -0,0 +1,260 @@
-- =============================================================================
-- IT Service Request Module
-- =============================================================================
-- 1. Main table
CREATE TABLE IF NOT EXISTS it_service_requests (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_number text UNIQUE,
-- Services (checkboxes from the form)
services text[] NOT NULL DEFAULT '{}',
services_other text,
-- Event/Activity Details (Quill Delta JSON)
event_name text NOT NULL,
event_details text, -- Quill delta JSON
-- Scheduling
event_date timestamptz,
event_end_date timestamptz,
dry_run_date timestamptz,
dry_run_end_date timestamptz,
-- Contact
contact_person text,
contact_number text,
-- Remarks (Quill Delta JSON)
remarks text,
-- Office (Department in the form)
office_id uuid REFERENCES offices(id) ON DELETE SET NULL,
-- Signatories
requested_by text, -- Name of requester (default: standard user who created)
requested_by_user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
approved_by text, -- Name of approver (admin only)
approved_by_user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
approved_at timestamptz,
-- Status lifecycle: draft → pending_approval → scheduled → in_progress_dry_run → in_progress → completed → cancelled
status text NOT NULL DEFAULT 'draft' CHECK (
status IN ('draft', 'pending_approval', 'scheduled', 'in_progress_dry_run', 'in_progress', 'completed', 'cancelled')
),
-- Geofence override: allow IT staff to check in/out outside CRMC premise
outside_premise_allowed boolean NOT NULL DEFAULT false,
-- Cancellation
cancellation_reason text,
cancelled_at timestamptz,
-- Metadata
creator_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
-- Date/Time Received/Checked from the form
date_time_received timestamptz,
date_time_checked timestamptz
);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_it_service_requests_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER it_service_requests_updated_at_trigger
BEFORE UPDATE ON it_service_requests
FOR EACH ROW
EXECUTE FUNCTION update_it_service_requests_updated_at();
-- 2. IT Service Request Assignments (IT Staff assigned to the request)
CREATE TABLE IF NOT EXISTS it_service_request_assignments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_id uuid NOT NULL REFERENCES it_service_requests(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(request_id, user_id)
);
-- 3. Activity Logs for audit trail
CREATE TABLE IF NOT EXISTS it_service_request_activity_logs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
request_id uuid NOT NULL REFERENCES it_service_requests(id) ON DELETE CASCADE,
actor_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
action_type text NOT NULL,
meta jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 4. Action Taken (separate table with evidence attachments)
CREATE TABLE IF NOT EXISTS it_service_request_actions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_id uuid NOT NULL REFERENCES it_service_requests(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
action_taken text, -- Quill Delta JSON
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Auto-update updated_at for actions
CREATE TRIGGER it_service_request_actions_updated_at_trigger
BEFORE UPDATE ON it_service_request_actions
FOR EACH ROW
EXECUTE FUNCTION update_it_service_requests_updated_at();
-- 5. Evidence Attachments (images only, for action taken)
CREATE TABLE IF NOT EXISTS it_service_request_evidence (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
request_id uuid NOT NULL REFERENCES it_service_requests(id) ON DELETE CASCADE,
action_id uuid REFERENCES it_service_request_actions(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
file_path text NOT NULL,
file_name text NOT NULL,
taken_at timestamptz, -- Extracted from image EXIF metadata
created_at timestamptz NOT NULL DEFAULT now()
);
-- 6. Request Number Generation RPC
CREATE OR REPLACE FUNCTION insert_it_service_request_with_number(
p_event_name text,
p_services text[],
p_creator_id uuid,
p_office_id uuid DEFAULT NULL,
p_requested_by text DEFAULT NULL,
p_requested_by_user_id uuid DEFAULT NULL,
p_status text DEFAULT 'draft'
)
RETURNS TABLE(id uuid, request_number text) AS $$
DECLARE
v_seq int;
v_id uuid;
v_number text;
BEGIN
-- Get next sequence number for the current year
SELECT COALESCE(MAX(
CAST(NULLIF(regexp_replace(r.request_number, '^ISR-\d{4}-', ''), '') AS int)
), 0) + 1
INTO v_seq
FROM it_service_requests r
WHERE r.request_number LIKE 'ISR-' || EXTRACT(YEAR FROM now())::text || '-%';
v_number := 'ISR-' || EXTRACT(YEAR FROM now())::text || '-' || LPAD(v_seq::text, 4, '0');
v_id := gen_random_uuid();
INSERT INTO it_service_requests (id, request_number, event_name, services, creator_id, office_id, requested_by, requested_by_user_id, status)
VALUES (v_id, v_number, p_event_name, p_services, p_creator_id, p_office_id, p_requested_by, p_requested_by_user_id, p_status);
RETURN QUERY SELECT v_id, v_number;
END;
$$ LANGUAGE plpgsql;
-- 7. Indexes
CREATE INDEX idx_it_service_requests_status ON it_service_requests(status);
CREATE INDEX idx_it_service_requests_office_id ON it_service_requests(office_id);
CREATE INDEX idx_it_service_requests_creator_id ON it_service_requests(creator_id);
CREATE INDEX idx_it_service_requests_event_date ON it_service_requests(event_date);
CREATE INDEX idx_it_service_request_assignments_request_id ON it_service_request_assignments(request_id);
CREATE INDEX idx_it_service_request_assignments_user_id ON it_service_request_assignments(user_id);
CREATE INDEX idx_it_service_request_activity_logs_request_id ON it_service_request_activity_logs(request_id);
CREATE INDEX idx_it_service_request_actions_request_id ON it_service_request_actions(request_id);
CREATE INDEX idx_it_service_request_evidence_request_id ON it_service_request_evidence(request_id);
-- 8. Enable RLS
ALTER TABLE it_service_requests ENABLE ROW LEVEL SECURITY;
ALTER TABLE it_service_request_assignments ENABLE ROW LEVEL SECURITY;
ALTER TABLE it_service_request_activity_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE it_service_request_actions ENABLE ROW LEVEL SECURITY;
ALTER TABLE it_service_request_evidence ENABLE ROW LEVEL SECURITY;
-- 9. RLS Policies
-- IT Service Requests: authenticated can read all, insert own, update depending on role
CREATE POLICY "Authenticated users can read it_service_requests"
ON it_service_requests FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert it_service_requests"
ON it_service_requests FOR INSERT TO authenticated
WITH CHECK (auth.uid() = creator_id);
CREATE POLICY "Authenticated users can update it_service_requests"
ON it_service_requests FOR UPDATE TO authenticated
USING (true);
-- Assignments
CREATE POLICY "Authenticated users can read it_service_request_assignments"
ON it_service_request_assignments FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert it_service_request_assignments"
ON it_service_request_assignments FOR INSERT TO authenticated
WITH CHECK (true);
CREATE POLICY "Authenticated users can delete it_service_request_assignments"
ON it_service_request_assignments FOR DELETE TO authenticated
USING (true);
-- Activity Logs
CREATE POLICY "Authenticated users can read it_service_request_activity_logs"
ON it_service_request_activity_logs FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert it_service_request_activity_logs"
ON it_service_request_activity_logs FOR INSERT TO authenticated
WITH CHECK (true);
-- Actions
CREATE POLICY "Authenticated users can read it_service_request_actions"
ON it_service_request_actions FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert it_service_request_actions"
ON it_service_request_actions FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Authenticated users can update it_service_request_actions"
ON it_service_request_actions FOR UPDATE TO authenticated
USING (auth.uid() = user_id);
-- Evidence
CREATE POLICY "Authenticated users can read it_service_request_evidence"
ON it_service_request_evidence FOR SELECT TO authenticated USING (true);
CREATE POLICY "Authenticated users can insert it_service_request_evidence"
ON it_service_request_evidence FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Authenticated users can delete it_service_request_evidence"
ON it_service_request_evidence FOR DELETE TO authenticated
USING (auth.uid() = user_id);
-- 10. Add it_service_request_id to notifications table
ALTER TABLE notifications ADD COLUMN IF NOT EXISTS it_service_request_id uuid REFERENCES it_service_requests(id) ON DELETE CASCADE;
-- 11. Storage bucket for IT service request files
INSERT INTO storage.buckets (id, name, public, file_size_limit)
VALUES ('it_service_attachments', 'it_service_attachments', true, 26214400)
ON CONFLICT (id) DO NOTHING;
-- Storage policies
CREATE POLICY "Authenticated users can upload it_service_attachments"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'it_service_attachments');
CREATE POLICY "Authenticated users can read it_service_attachments"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'it_service_attachments');
CREATE POLICY "Authenticated users can delete it_service_attachments"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'it_service_attachments');
-- 12. Realtime publication
ALTER PUBLICATION supabase_realtime ADD TABLE it_service_requests;
ALTER PUBLICATION supabase_realtime ADD TABLE it_service_request_assignments;
ALTER PUBLICATION supabase_realtime ADD TABLE it_service_request_activity_logs;
ALTER PUBLICATION supabase_realtime ADD TABLE it_service_request_actions;

View File

@ -0,0 +1,96 @@
-- Add shift_type to attendance_logs so each log carries its own shift label.
-- This fixes the display bug where every log showed "Normal Shift"
-- because the column was missing and the Flutter model always fell back to 'normal'.
-- 1. Add the column (text, nullable to avoid breaking existing rows).
ALTER TABLE attendance_logs
ADD COLUMN IF NOT EXISTS shift_type text;
-- 2. Backfill from the related duty_schedule.
UPDATE attendance_logs al
SET shift_type = ds.shift_type
FROM duty_schedules ds
WHERE al.duty_schedule_id = ds.id
AND al.shift_type IS NULL;
-- 3. Set default to 'normal' for any remaining nulls, then lock it in.
UPDATE attendance_logs SET shift_type = 'normal' WHERE shift_type IS NULL;
ALTER TABLE attendance_logs ALTER COLUMN shift_type SET DEFAULT 'normal';
ALTER TABLE attendance_logs ALTER COLUMN shift_type SET NOT NULL;
-- 4. Recreate attendance_check_in to stamp shift_type from its schedule.
CREATE OR REPLACE FUNCTION public.attendance_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_uid uuid := auth.uid();
v_now timestamptz := now();
v_schedule duty_schedules%ROWTYPE;
v_log_id uuid;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
UPDATE duty_schedules
SET status = 'arrival', check_in_at = v_now,
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
WHERE id = p_duty_id;
INSERT INTO attendance_logs (user_id, duty_schedule_id, shift_type, check_in_at, check_in_lat, check_in_lng)
VALUES (v_uid, p_duty_id, v_schedule.shift_type, v_now, p_lat, p_lng)
RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$;
-- 5. Recreate overtime_check_in to stamp shift_type = 'overtime'.
CREATE OR REPLACE FUNCTION public.overtime_check_in(
p_lat double precision,
p_lng double precision,
p_justification text DEFAULT NULL
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_uid uuid := auth.uid();
v_now timestamptz := now();
v_schedule_id uuid;
v_log_id uuid;
v_end_time timestamptz;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
v_end_time := v_now + interval '8 hours';
INSERT INTO duty_schedules (user_id, shift_type, start_time, end_time, status, check_in_at, check_in_location)
VALUES (
v_uid,
'overtime',
v_now,
v_end_time,
'arrival'::duty_status,
v_now,
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
)
RETURNING id INTO v_schedule_id;
INSERT INTO attendance_logs (user_id, duty_schedule_id, shift_type, check_in_at, check_in_lat, check_in_lng, justification)
VALUES (v_uid, v_schedule_id, 'overtime', v_now, p_lat, p_lng, p_justification)
RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$;

View File

@ -0,0 +1,2 @@
-- Add color column to teams table (stores hex color string like 'e53935')
ALTER TABLE public.teams ADD COLUMN IF NOT EXISTS color TEXT;