feat: CRM Clinicas SaaS - MVP completo
- Auth: Login/Register con creacion de clinica - Dashboard: KPIs reales, graficas recharts - Pacientes: CRUD completo con busqueda - Agenda: FullCalendar, drag-and-drop, vista recepcion - Expediente: Notas SOAP, signos vitales, CIE-10 - Facturacion: Facturas con IVA, campos CFDI SAT - Inventario: Productos, stock, movimientos, alertas - Configuracion: Clinica, equipo, catalogo servicios - Supabase self-hosted: 18 tablas con RLS multi-tenant - Docker + Nginx para produccion Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
69
supabase/migrations/001_clinics_and_users.sql
Normal file
69
supabase/migrations/001_clinics_and_users.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE TABLE clinics (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
logo_url TEXT,
|
||||
address TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
rfc TEXT,
|
||||
razon_social TEXT,
|
||||
regimen_fiscal TEXT,
|
||||
codigo_postal TEXT,
|
||||
pac_provider TEXT CHECK (pac_provider IN ('facturama', 'sw_sapien')),
|
||||
pac_api_key TEXT,
|
||||
subscription_plan TEXT NOT NULL DEFAULT 'trial' CHECK (subscription_plan IN ('basico', 'pro', 'enterprise', 'trial')),
|
||||
subscription_status TEXT NOT NULL DEFAULT 'trial' CHECK (subscription_status IN ('active', 'trial', 'suspended', 'cancelled')),
|
||||
trial_ends_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '14 days'),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
full_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'receptionist' CHECK (role IN ('owner', 'admin', 'doctor', 'receptionist')),
|
||||
specialty TEXT,
|
||||
license_number TEXT,
|
||||
color TEXT DEFAULT '#3B82F6',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_clinic ON users(clinic_id);
|
||||
|
||||
CREATE TABLE branches (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
address TEXT,
|
||||
phone TEXT,
|
||||
is_main BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_branches_clinic ON branches(clinic_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION auth.clinic_id()
|
||||
RETURNS UUID AS $$
|
||||
SELECT clinic_id FROM public.users WHERE id = auth.uid()
|
||||
$$ LANGUAGE SQL STABLE SECURITY DEFINER;
|
||||
|
||||
ALTER TABLE clinics ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Users can view own clinic" ON clinics FOR SELECT USING (id = auth.clinic_id());
|
||||
CREATE POLICY "Owners can update own clinic" ON clinics FOR UPDATE USING (id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Users can view clinic members" ON users FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Owners can insert users" ON users FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Owners can update users" ON users FOR UPDATE USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE branches ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Users can view clinic branches" ON branches FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Users can manage branches" ON branches FOR ALL USING (clinic_id = auth.clinic_id());
|
||||
28
supabase/migrations/002_patients.sql
Normal file
28
supabase/migrations/002_patients.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE patients (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
date_of_birth DATE,
|
||||
gender TEXT CHECK (gender IN ('M', 'F', 'otro')),
|
||||
curp TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
address JSONB,
|
||||
blood_type TEXT,
|
||||
allergies TEXT[] DEFAULT '{}',
|
||||
emergency_contact JSONB,
|
||||
notes TEXT,
|
||||
source TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_patients_clinic ON patients(clinic_id);
|
||||
CREATE INDEX idx_patients_name ON patients(clinic_id, last_name, first_name);
|
||||
CREATE INDEX idx_patients_phone ON patients(clinic_id, phone);
|
||||
|
||||
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Users can view clinic patients" ON patients FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Users can insert clinic patients" ON patients FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Users can update clinic patients" ON patients FOR UPDATE USING (clinic_id = auth.clinic_id());
|
||||
40
supabase/migrations/003_appointments.sql
Normal file
40
supabase/migrations/003_appointments.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
CREATE TABLE doctor_schedules (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
doctor_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
slot_duration INT NOT NULL DEFAULT 30,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE(doctor_id, day_of_week)
|
||||
);
|
||||
|
||||
CREATE TABLE appointments (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
|
||||
doctor_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
ends_at TIMESTAMPTZ NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled','confirmed','in_progress','completed','cancelled','no_show')),
|
||||
type TEXT NOT NULL DEFAULT 'seguimiento' CHECK (type IN ('primera_vez','seguimiento','urgencia')),
|
||||
reason TEXT,
|
||||
notes TEXT,
|
||||
reminder_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_appointments_doctor_date ON appointments(doctor_id, starts_at);
|
||||
CREATE INDEX idx_appointments_patient ON appointments(patient_id);
|
||||
CREATE INDEX idx_appointments_status ON appointments(clinic_id, status);
|
||||
|
||||
ALTER TABLE doctor_schedules ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic schedules" ON doctor_schedules FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Manage clinic schedules" ON doctor_schedules FOR ALL USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE appointments ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic appointments" ON appointments FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic appointments" ON appointments FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Update clinic appointments" ON appointments FOR UPDATE USING (clinic_id = auth.clinic_id());
|
||||
75
supabase/migrations/004_medical_records.sql
Normal file
75
supabase/migrations/004_medical_records.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
CREATE TABLE medical_records (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
|
||||
family_history JSONB DEFAULT '[]',
|
||||
personal_history JSONB DEFAULT '[]',
|
||||
surgical_history JSONB DEFAULT '[]',
|
||||
current_medications JSONB DEFAULT '[]',
|
||||
immunizations JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(patient_id)
|
||||
);
|
||||
|
||||
CREATE TABLE consultation_notes (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
|
||||
doctor_id UUID NOT NULL REFERENCES users(id),
|
||||
appointment_id UUID REFERENCES appointments(id),
|
||||
subjective TEXT,
|
||||
objective TEXT,
|
||||
assessment TEXT,
|
||||
plan TEXT,
|
||||
vital_signs JSONB,
|
||||
diagnoses JSONB DEFAULT '[]',
|
||||
is_signed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
signed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_consultations_patient ON consultation_notes(patient_id, created_at DESC);
|
||||
CREATE INDEX idx_consultations_doctor ON consultation_notes(doctor_id, created_at DESC);
|
||||
|
||||
CREATE TABLE prescriptions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
consultation_id UUID NOT NULL REFERENCES consultation_notes(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id),
|
||||
doctor_id UUID NOT NULL REFERENCES users(id),
|
||||
items JSONB NOT NULL DEFAULT '[]',
|
||||
pharmacy_notes TEXT,
|
||||
pdf_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE medical_files (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
|
||||
consultation_id UUID REFERENCES consultation_notes(id),
|
||||
file_name TEXT NOT NULL,
|
||||
file_type TEXT NOT NULL DEFAULT 'other' CHECK (file_type IN ('lab_result','imaging','referral','other')),
|
||||
storage_path TEXT NOT NULL,
|
||||
uploaded_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE medical_records ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic records" ON medical_records FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic records" ON medical_records FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Update clinic records" ON medical_records FOR UPDATE USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE consultation_notes ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic notes" ON consultation_notes FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic notes" ON consultation_notes FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Update own notes" ON consultation_notes FOR UPDATE USING (clinic_id = auth.clinic_id() AND doctor_id = auth.uid());
|
||||
|
||||
ALTER TABLE prescriptions ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic prescriptions" ON prescriptions FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic prescriptions" ON prescriptions FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE medical_files ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic files" ON medical_files FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic files" ON medical_files FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
78
supabase/migrations/005_billing.sql
Normal file
78
supabase/migrations/005_billing.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
CREATE TABLE services_catalog (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
sat_product_key TEXT,
|
||||
sat_unit_key TEXT DEFAULT 'E48',
|
||||
tax_rate DECIMAL(4,2) NOT NULL DEFAULT 0.16,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE invoices (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
patient_id UUID NOT NULL REFERENCES patients(id),
|
||||
invoice_number TEXT NOT NULL,
|
||||
cfdi_uuid TEXT,
|
||||
cfdi_status TEXT NOT NULL DEFAULT 'draft' CHECK (cfdi_status IN ('draft','stamped','cancelled')),
|
||||
cfdi_xml_url TEXT,
|
||||
cfdi_pdf_url TEXT,
|
||||
uso_cfdi TEXT DEFAULT 'S01',
|
||||
forma_pago TEXT DEFAULT '01',
|
||||
metodo_pago TEXT DEFAULT 'PUE',
|
||||
subtotal DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
total DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
payment_status TEXT NOT NULL DEFAULT 'pending' CHECK (payment_status IN ('pending','partial','paid')),
|
||||
paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
due_date DATE,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(clinic_id, invoice_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_invoices_patient ON invoices(patient_id);
|
||||
CREATE INDEX idx_invoices_status ON invoices(clinic_id, payment_status);
|
||||
|
||||
CREATE TABLE invoice_items (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
service_id UUID REFERENCES services_catalog(id),
|
||||
product_id UUID,
|
||||
description TEXT NOT NULL,
|
||||
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
|
||||
unit_price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
tax_rate DECIMAL(4,2) NOT NULL DEFAULT 0.16,
|
||||
total DECIMAL(10,2) NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE payments (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
payment_method TEXT NOT NULL DEFAULT 'cash' CHECK (payment_method IN ('cash','card','transfer','other')),
|
||||
reference TEXT,
|
||||
received_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE services_catalog ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic services" ON services_catalog FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Manage clinic services" ON services_catalog FOR ALL USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic invoices" ON invoices FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic invoices" ON invoices FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Update clinic invoices" ON invoices FOR UPDATE USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE invoice_items ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View invoice items" ON invoice_items FOR SELECT USING (EXISTS (SELECT 1 FROM invoices WHERE invoices.id = invoice_items.invoice_id AND invoices.clinic_id = auth.clinic_id()));
|
||||
CREATE POLICY "Manage invoice items" ON invoice_items FOR ALL USING (EXISTS (SELECT 1 FROM invoices WHERE invoices.id = invoice_items.invoice_id AND invoices.clinic_id = auth.clinic_id()));
|
||||
|
||||
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic payments" ON payments FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic payments" ON payments FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
64
supabase/migrations/006_inventory.sql
Normal file
64
supabase/migrations/006_inventory.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
sku TEXT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL DEFAULT 'insumo' CHECK (category IN ('medicamento','insumo','material','equipo')),
|
||||
unit TEXT NOT NULL DEFAULT 'pieza',
|
||||
purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
sale_price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
min_stock INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE invoice_items ADD CONSTRAINT fk_invoice_items_product FOREIGN KEY (product_id) REFERENCES products(id);
|
||||
|
||||
CREATE TABLE inventory_stock (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
current_stock DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(clinic_id, product_id)
|
||||
);
|
||||
|
||||
CREATE TABLE inventory_movements (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL CHECK (type IN ('entrada','salida','ajuste','merma')),
|
||||
quantity DECIMAL(10,2) NOT NULL,
|
||||
reason TEXT,
|
||||
reference_id TEXT,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_movements_product ON inventory_movements(product_id, created_at DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_inventory_stock()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO inventory_stock (clinic_id, product_id, current_stock, last_updated)
|
||||
VALUES (NEW.clinic_id, NEW.product_id, NEW.quantity, NOW())
|
||||
ON CONFLICT (clinic_id, product_id)
|
||||
DO UPDATE SET current_stock = inventory_stock.current_stock + NEW.quantity, last_updated = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trg_update_stock AFTER INSERT ON inventory_movements FOR EACH ROW EXECUTE FUNCTION update_inventory_stock();
|
||||
|
||||
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic products" ON products FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Manage clinic products" ON products FOR ALL USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE inventory_stock ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic stock" ON inventory_stock FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Manage clinic stock" ON inventory_stock FOR ALL USING (clinic_id = auth.clinic_id());
|
||||
|
||||
ALTER TABLE inventory_movements ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "View clinic movements" ON inventory_movements FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
CREATE POLICY "Insert clinic movements" ON inventory_movements FOR INSERT WITH CHECK (clinic_id = auth.clinic_id());
|
||||
41
supabase/migrations/007_audit_log.sql
Normal file
41
supabase/migrations/007_audit_log.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
CREATE TABLE audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
clinic_id UUID REFERENCES clinics(id),
|
||||
user_id UUID,
|
||||
table_name TEXT NOT NULL,
|
||||
record_id UUID,
|
||||
action TEXT NOT NULL CHECK (action IN ('INSERT','UPDATE','DELETE')),
|
||||
old_data JSONB,
|
||||
new_data JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_clinic ON audit_log(clinic_id, created_at DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_trigger_func()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO audit_log (clinic_id, user_id, table_name, record_id, action, new_data)
|
||||
VALUES (NEW.clinic_id, auth.uid(), TG_TABLE_NAME, NEW.id, 'INSERT', to_jsonb(NEW));
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
INSERT INTO audit_log (clinic_id, user_id, table_name, record_id, action, old_data, new_data)
|
||||
VALUES (NEW.clinic_id, auth.uid(), TG_TABLE_NAME, NEW.id, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW));
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
INSERT INTO audit_log (clinic_id, user_id, table_name, record_id, action, old_data)
|
||||
VALUES (OLD.clinic_id, auth.uid(), TG_TABLE_NAME, OLD.id, 'DELETE', to_jsonb(OLD));
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER audit_patients AFTER INSERT OR UPDATE OR DELETE ON patients FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
|
||||
CREATE TRIGGER audit_appointments AFTER INSERT OR UPDATE OR DELETE ON appointments FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
|
||||
CREATE TRIGGER audit_consultation_notes AFTER INSERT OR UPDATE OR DELETE ON consultation_notes FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
|
||||
CREATE TRIGGER audit_invoices AFTER INSERT OR UPDATE OR DELETE ON invoices FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
|
||||
CREATE TRIGGER audit_inventory_movements AFTER INSERT ON inventory_movements FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
|
||||
|
||||
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "Admins view audit" ON audit_log FOR SELECT USING (clinic_id = auth.clinic_id());
|
||||
22
supabase/migrations/008_registration_policies.sql
Normal file
22
supabase/migrations/008_registration_policies.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Fix: Allow new users to create clinics and profiles during registration
|
||||
-- The original RLS policies required a clinic_id which doesn't exist yet at signup time
|
||||
|
||||
-- Allow any authenticated user to create a clinic (for registration flow)
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies WHERE policyname = 'Authenticated users can create clinics' AND tablename = 'clinics'
|
||||
) THEN
|
||||
CREATE POLICY "Authenticated users can create clinics" ON clinics
|
||||
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Allow users to insert their own profile row
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policies WHERE policyname = 'Users can insert own profile' AND tablename = 'users'
|
||||
) THEN
|
||||
CREATE POLICY "Users can insert own profile" ON users
|
||||
FOR INSERT WITH CHECK (id = auth.uid());
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user