Migrations
This commit is contained in:
parent
d87b5e73d7
commit
ce82a88e04
15
supabase/migrations/20260306090000_religion_and_ramadan.sql
Normal file
15
supabase/migrations/20260306090000_religion_and_ramadan.sql
Normal 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.)
|
||||||
46
supabase/migrations/20260306090100_chat_messages.sql
Normal file
46
supabase/migrations/20260306090100_chat_messages.sql
Normal 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())
|
||||||
|
)
|
||||||
|
);
|
||||||
44
supabase/migrations/20260306090200_pass_slips.sql
Normal file
44
supabase/migrations/20260306090200_pass_slips.sql
Normal 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')
|
||||||
|
)
|
||||||
|
);
|
||||||
163
supabase/migrations/20260306090300_attendance_logs.sql
Normal file
163
supabase/migrations/20260306090300_attendance_logs.sql
Normal 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;
|
||||||
|
$$;
|
||||||
85
supabase/migrations/20260306090400_live_positions.sql
Normal file
85
supabase/migrations/20260306090400_live_positions.sql
Normal 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);
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
49
supabase/migrations/20260307100000_overtime_check_in.sql
Normal file
49
supabase/migrations/20260307100000_overtime_check_in.sql
Normal 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;
|
||||||
|
$$;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
100
supabase/migrations/20260307110000_multi_checkin_early_out.sql
Normal file
100
supabase/migrations/20260307110000_multi_checkin_early_out.sql
Normal 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;
|
||||||
|
$$;
|
||||||
147
supabase/migrations/20260307120000_leave_of_absence.sql
Normal file
147
supabase/migrations/20260307120000_leave_of_absence.sql
Normal 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();
|
||||||
158
supabase/migrations/20260307130000_avatar_face_verification.sql
Normal file
158
supabase/migrations/20260307130000_avatar_face_verification.sql
Normal 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')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
43
supabase/migrations/20260307140000_verification_sessions.sql
Normal file
43
supabase/migrations/20260307140000_verification_sessions.sql
Normal 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);
|
||||||
|
|
@ -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);
|
||||||
41
supabase/migrations/20260307160000_fix_storage_policies.sql
Normal file
41
supabase/migrations/20260307160000_fix_storage_policies.sql
Normal 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
|
||||||
|
);
|
||||||
59
supabase/migrations/20260307170000_overtime_shift_type.sql
Normal file
59
supabase/migrations/20260307170000_overtime_shift_type.sql
Normal 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
|
||||||
|
|
@ -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';
|
||||||
260
supabase/migrations/20260308090000_add_it_service_requests.sql
Normal file
260
supabase/migrations/20260308090000_add_it_service_requests.sql
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
|
$$;
|
||||||
2
supabase/migrations/20260308120000_add_team_color.sql
Normal file
2
supabase/migrations/20260308120000_add_team_color.sql
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user