feat: add database schema migrations and seed data
8 SQL migration files covering all 10 tables, RLS policies with SECURITY DEFINER helpers, and seed data for SCW1 site + 3 zones. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWxyMibCuGGtSQSqfajDQ7
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
-- supabase/migrations/20260709000001_sites_zones.sql
|
||||||
|
CREATE TABLE sites (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
address TEXT,
|
||||||
|
region TEXT,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE zones (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
qr_code_token TEXT NOT NULL UNIQUE DEFAULT gen_random_uuid()::TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX zones_site_id_idx ON zones(site_id);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- supabase/migrations/20260709000002_users.sql
|
||||||
|
CREATE TYPE user_role AS ENUM (
|
||||||
|
'reporter', 'supervisor', 'hse', 'capa_owner', 'management', 'admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL DEFAULT '',
|
||||||
|
email TEXT NOT NULL DEFAULT '',
|
||||||
|
phone TEXT,
|
||||||
|
role user_role NOT NULL DEFAULT 'reporter',
|
||||||
|
department TEXT,
|
||||||
|
site_id UUID REFERENCES sites(id),
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Auto-create users row when auth user signs up
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_auth_user()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.users (id, email, name)
|
||||||
|
VALUES (
|
||||||
|
NEW.id,
|
||||||
|
NEW.email,
|
||||||
|
COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.email)
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.handle_new_auth_user();
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
-- supabase/migrations/20260709000003_incidents.sql
|
||||||
|
CREATE TYPE incident_type AS ENUM (
|
||||||
|
'injury', 'near_miss', 'hazard', 'asset_damage', 'environmental', 'security', 'fire'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE incident_status AS ENUM (
|
||||||
|
'reported', 'triaged', 'investigating', 'capa_pending', 'verification', 'closed'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE medical_status AS ENUM (
|
||||||
|
'none', 'first_aid', 'medical_treatment', 'lti'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE incidents (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
reference_no TEXT UNIQUE,
|
||||||
|
incident_type incident_type NOT NULL,
|
||||||
|
site_id UUID NOT NULL REFERENCES sites(id),
|
||||||
|
zone_id UUID REFERENCES zones(id),
|
||||||
|
reported_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
reported_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
severity SMALLINT CHECK (severity BETWEEN 1 AND 5),
|
||||||
|
status incident_status NOT NULL DEFAULT 'reported',
|
||||||
|
injury_involved BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
asset_involved BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
medical_status medical_status,
|
||||||
|
lost_days INT CHECK (lost_days >= 0),
|
||||||
|
closed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Auto-generate reference_no: SITECODE-YYYYMM-#### (e.g. SCW1-202607-0001)
|
||||||
|
CREATE OR REPLACE FUNCTION public.generate_incident_reference()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_site_code TEXT;
|
||||||
|
v_month TEXT;
|
||||||
|
v_seq INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT UPPER(REGEXP_REPLACE(SUBSTRING(name, 1, 6), '[^A-Za-z0-9]', '', 'g'))
|
||||||
|
INTO v_site_code
|
||||||
|
FROM sites
|
||||||
|
WHERE id = NEW.site_id;
|
||||||
|
|
||||||
|
v_month := TO_CHAR(NEW.reported_at, 'YYYYMM');
|
||||||
|
|
||||||
|
SELECT COUNT(*) + 1
|
||||||
|
INTO v_seq
|
||||||
|
FROM incidents
|
||||||
|
WHERE site_id = NEW.site_id
|
||||||
|
AND TO_CHAR(reported_at, 'YYYYMM') = v_month;
|
||||||
|
|
||||||
|
NEW.reference_no := v_site_code || '-' || v_month || '-' || LPAD(v_seq::TEXT, 4, '0');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER set_incident_reference
|
||||||
|
BEFORE INSERT ON incidents
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.reference_no IS NULL)
|
||||||
|
EXECUTE FUNCTION public.generate_incident_reference();
|
||||||
|
|
||||||
|
CREATE INDEX incidents_site_id_idx ON incidents(site_id);
|
||||||
|
CREATE INDEX incidents_reported_by_idx ON incidents(reported_by);
|
||||||
|
CREATE INDEX incidents_status_idx ON incidents(status);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- supabase/migrations/20260709000004_evidence_investigations.sql
|
||||||
|
CREATE TYPE evidence_stage AS ENUM (
|
||||||
|
'report', 'response', 'investigation', 'capa', 'verification'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE evidence_files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE RESTRICT,
|
||||||
|
stage evidence_stage NOT NULL,
|
||||||
|
file_url TEXT NOT NULL,
|
||||||
|
file_type TEXT NOT NULL,
|
||||||
|
file_hash TEXT NOT NULL, -- immutable after upload (DOSH audit integrity)
|
||||||
|
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT false -- NEVER hard-delete; use this flag
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE rca_method AS ENUM ('five_why', 'fishbone', 'other');
|
||||||
|
|
||||||
|
CREATE TABLE investigations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE RESTRICT,
|
||||||
|
investigator_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
method rca_method NOT NULL DEFAULT 'five_why',
|
||||||
|
findings_text TEXT,
|
||||||
|
root_cause_summary TEXT,
|
||||||
|
alcohol_test_result TEXT,
|
||||||
|
witness_statement_refs TEXT[],
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX evidence_files_incident_idx ON evidence_files(incident_id);
|
||||||
|
CREATE INDEX investigations_incident_idx ON investigations(incident_id);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- supabase/migrations/20260709000005_capa_dosh.sql
|
||||||
|
CREATE TYPE capa_priority AS ENUM ('low', 'med', 'high');
|
||||||
|
|
||||||
|
CREATE TYPE capa_status AS ENUM (
|
||||||
|
'open', 'in_progress', 'overdue', 'pending_verification',
|
||||||
|
'verified', 'reopened', 'closed'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE capa_actions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE RESTRICT,
|
||||||
|
root_cause_ref TEXT,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
owner_user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
department TEXT NOT NULL,
|
||||||
|
due_date DATE NOT NULL,
|
||||||
|
priority capa_priority NOT NULL DEFAULT 'med',
|
||||||
|
status capa_status NOT NULL DEFAULT 'open',
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
verified_by UUID REFERENCES users(id),
|
||||||
|
verified_at TIMESTAMPTZ,
|
||||||
|
effectiveness_recheck_date DATE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE dosh_form_type AS ENUM ('jkkp6', 'jkkp7', 'jkkp8');
|
||||||
|
CREATE TYPE dosh_status AS ENUM ('not_required', 'pending', 'submitted');
|
||||||
|
|
||||||
|
CREATE TABLE dosh_reports (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE RESTRICT,
|
||||||
|
form_type dosh_form_type NOT NULL,
|
||||||
|
status dosh_status NOT NULL DEFAULT 'not_required',
|
||||||
|
submitted_at TIMESTAMPTZ,
|
||||||
|
submitted_by UUID REFERENCES users(id),
|
||||||
|
file_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX capa_actions_incident_idx ON capa_actions(incident_id);
|
||||||
|
CREATE INDEX capa_actions_owner_idx ON capa_actions(owner_user_id);
|
||||||
|
CREATE INDEX capa_actions_status_idx ON capa_actions(status);
|
||||||
|
CREATE INDEX capa_actions_due_date_idx ON capa_actions(due_date);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- supabase/migrations/20260709000006_notifications_audit.sql
|
||||||
|
CREATE TYPE notification_channel AS ENUM ('email', 'whatsapp', 'in_app');
|
||||||
|
|
||||||
|
CREATE TABLE notifications_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
incident_id UUID REFERENCES incidents(id),
|
||||||
|
capa_id UUID REFERENCES capa_actions(id),
|
||||||
|
channel notification_channel NOT NULL,
|
||||||
|
recipient TEXT NOT NULL,
|
||||||
|
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
status TEXT NOT NULL DEFAULT 'sent'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE audit_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
table_name TEXT NOT NULL,
|
||||||
|
record_id UUID NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
changed_by UUID REFERENCES users(id),
|
||||||
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
old_value JSONB,
|
||||||
|
new_value JSONB
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX audit_log_table_record_idx ON audit_log(table_name, record_id);
|
||||||
|
CREATE INDEX audit_log_changed_by_idx ON audit_log(changed_by);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
-- supabase/migrations/20260709000007_rls_policies.sql
|
||||||
|
|
||||||
|
-- Enable RLS on all application tables
|
||||||
|
ALTER TABLE sites ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE zones ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE incidents ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE evidence_files ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE investigations ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE capa_actions ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE dosh_reports ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE notifications_log ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Helper functions (SECURITY DEFINER avoids recursion through RLS)
|
||||||
|
CREATE OR REPLACE FUNCTION public.auth_user_role()
|
||||||
|
RETURNS user_role
|
||||||
|
LANGUAGE sql
|
||||||
|
SECURITY DEFINER
|
||||||
|
STABLE
|
||||||
|
AS $$ SELECT role FROM public.users WHERE id = auth.uid() $$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.auth_user_site_id()
|
||||||
|
RETURNS UUID
|
||||||
|
LANGUAGE sql
|
||||||
|
SECURITY DEFINER
|
||||||
|
STABLE
|
||||||
|
AS $$ SELECT site_id FROM public.users WHERE id = auth.uid() $$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.auth_user_department()
|
||||||
|
RETURNS TEXT
|
||||||
|
LANGUAGE sql
|
||||||
|
SECURITY DEFINER
|
||||||
|
STABLE
|
||||||
|
AS $$ SELECT department FROM public.users WHERE id = auth.uid() $$;
|
||||||
|
|
||||||
|
-- sites: everyone reads active sites; only admin manages
|
||||||
|
CREATE POLICY "sites_read" ON sites FOR SELECT USING (active = true);
|
||||||
|
CREATE POLICY "sites_admin_all" ON sites FOR ALL USING (auth_user_role() = 'admin');
|
||||||
|
|
||||||
|
-- zones: everyone reads; only admin manages
|
||||||
|
CREATE POLICY "zones_read" ON zones FOR SELECT USING (true);
|
||||||
|
CREATE POLICY "zones_admin_all" ON zones FOR ALL USING (auth_user_role() = 'admin');
|
||||||
|
|
||||||
|
-- users: own profile always readable; elevated roles see all; admin manages
|
||||||
|
CREATE POLICY "users_read_own" ON users FOR SELECT USING (id = auth.uid());
|
||||||
|
CREATE POLICY "users_read_elevated" ON users FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin', 'management', 'supervisor'));
|
||||||
|
CREATE POLICY "users_update_own" ON users FOR UPDATE USING (id = auth.uid());
|
||||||
|
CREATE POLICY "users_admin_all" ON users FOR ALL USING (auth_user_role() = 'admin');
|
||||||
|
|
||||||
|
-- incidents: reporter sees own; supervisor sees own site; elevated sees all
|
||||||
|
CREATE POLICY "incidents_insert" ON incidents FOR INSERT
|
||||||
|
WITH CHECK (reported_by = auth.uid());
|
||||||
|
CREATE POLICY "incidents_read_reporter" ON incidents FOR SELECT
|
||||||
|
USING (reported_by = auth.uid());
|
||||||
|
CREATE POLICY "incidents_read_supervisor" ON incidents FOR SELECT
|
||||||
|
USING (auth_user_role() = 'supervisor' AND site_id = auth_user_site_id());
|
||||||
|
CREATE POLICY "incidents_read_elevated" ON incidents FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin', 'management'));
|
||||||
|
CREATE POLICY "incidents_update_elevated" ON incidents FOR UPDATE
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin', 'supervisor'));
|
||||||
|
|
||||||
|
-- evidence_files: inserter or incident owner sees own; elevated sees all; no delete
|
||||||
|
CREATE POLICY "evidence_insert" ON evidence_files FOR INSERT
|
||||||
|
WITH CHECK (uploaded_by = auth.uid());
|
||||||
|
CREATE POLICY "evidence_read_uploader" ON evidence_files FOR SELECT
|
||||||
|
USING (uploaded_by = auth.uid());
|
||||||
|
CREATE POLICY "evidence_read_elevated" ON evidence_files FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin', 'management', 'supervisor'));
|
||||||
|
-- No DELETE policy -- files are never deleted (soft-delete only via `deleted` flag)
|
||||||
|
|
||||||
|
-- investigations: hse/admin full access; supervisor read-only
|
||||||
|
CREATE POLICY "investigations_hse_admin" ON investigations FOR ALL
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin'));
|
||||||
|
CREATE POLICY "investigations_read_supervisor" ON investigations FOR SELECT
|
||||||
|
USING (auth_user_role() = 'supervisor');
|
||||||
|
|
||||||
|
-- capa_actions: owner/dept reads and updates own; elevated reads all; hse/admin manages
|
||||||
|
CREATE POLICY "capa_read_owner" ON capa_actions FOR SELECT
|
||||||
|
USING (owner_user_id = auth.uid() OR department = auth_user_department());
|
||||||
|
CREATE POLICY "capa_update_owner" ON capa_actions FOR UPDATE
|
||||||
|
USING (owner_user_id = auth.uid() OR department = auth_user_department());
|
||||||
|
CREATE POLICY "capa_read_elevated" ON capa_actions FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin', 'management', 'supervisor'));
|
||||||
|
CREATE POLICY "capa_hse_admin_all" ON capa_actions FOR ALL
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin'));
|
||||||
|
|
||||||
|
-- dosh_reports: hse/admin only
|
||||||
|
CREATE POLICY "dosh_hse_admin" ON dosh_reports FOR ALL
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin'));
|
||||||
|
|
||||||
|
-- notifications_log: hse/admin read
|
||||||
|
CREATE POLICY "notifications_read_elevated" ON notifications_log FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin'));
|
||||||
|
|
||||||
|
-- audit_log: hse/admin read only -- no writes from app (writes via SECURITY DEFINER functions)
|
||||||
|
CREATE POLICY "audit_read_elevated" ON audit_log FOR SELECT
|
||||||
|
USING (auth_user_role() IN ('hse', 'admin'));
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- supabase/migrations/20260709000008_seed.sql
|
||||||
|
-- Test site: Setia Corp Warehouse 1
|
||||||
|
INSERT INTO sites (id, name, address, region, active)
|
||||||
|
VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000001'::UUID,
|
||||||
|
'SCW1',
|
||||||
|
'No. 1, Jalan Industri 1, Shah Alam, Selangor',
|
||||||
|
'Central',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Zones for SCW1
|
||||||
|
INSERT INTO zones (id, site_id, name, qr_code_token)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000010'::UUID,
|
||||||
|
'00000000-0000-0000-0000-000000000001'::UUID,
|
||||||
|
'Dock A',
|
||||||
|
'scw1-dock-a-qr-2026'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000011'::UUID,
|
||||||
|
'00000000-0000-0000-0000-000000000001'::UUID,
|
||||||
|
'Cold Storage',
|
||||||
|
'scw1-cold-storage-qr-2026'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'00000000-0000-0000-0000-000000000012'::UUID,
|
||||||
|
'00000000-0000-0000-0000-000000000001'::UUID,
|
||||||
|
'Loading Bay',
|
||||||
|
'scw1-loading-bay-qr-2026'
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
Reference in New Issue
Block a user