-- ============================================================================ -- DENTAL SCHEMA - Especialización de ERP-Clínicas -- Clínica Dental -- ============================================================================ -- Fecha: 2026-01-04 -- Versión: 1.0 -- Hereda de: erp-clinicas FASE-8 -- ============================================================================ -- Schema CREATE SCHEMA IF NOT EXISTS dental; -- ============================================================================ -- ENUMS -- ============================================================================ DO $$ BEGIN CREATE TYPE dental.estado_pieza AS ENUM ( 'sano', 'caries', 'obturacion', 'endodoncia', 'corona', 'puente', 'implante', 'ausente', 'extraccion_indicada', 'diente_temporal', 'fractura', 'movilidad' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE dental.cara_dental AS ENUM ( 'mesial', 'distal', 'oclusal', 'incisal', 'vestibular', 'bucal', 'lingual', 'palatino' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE dental.estado_tratamiento AS ENUM ( 'pendiente', 'en_proceso', 'completado', 'cancelado' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE dental.tipo_ortodoncia AS ENUM ( 'brackets_metalicos', 'brackets_esteticos', 'brackets_linguales', 'alineadores', 'removible', 'retenedor' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ============================================================================ -- CATÁLOGOS -- ============================================================================ -- Piezas dentales CREATE TABLE IF NOT EXISTS dental.piezas_dentales ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), numero VARCHAR(10) NOT NULL UNIQUE, -- '11', '12', ... '48', '51'...'85' nombre VARCHAR(50) NOT NULL, cuadrante INTEGER NOT NULL CHECK (cuadrante BETWEEN 1 AND 8), es_temporal BOOLEAN DEFAULT false, descripcion TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.piezas_dentales IS 'Catálogo de piezas dentales (nomenclatura FDI)'; -- Tratamientos dentales CREATE TABLE IF NOT EXISTS dental.tratamientos_catalogo ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, codigo VARCHAR(20) NOT NULL, nombre VARCHAR(100) NOT NULL, categoria VARCHAR(50), -- 'prevencion', 'restauracion', 'endodoncia', etc. descripcion TEXT, duracion_minutos INTEGER DEFAULT 30, precio_base NUMERIC(10,2), requiere_rx BOOLEAN DEFAULT false, requiere_anestesia BOOLEAN DEFAULT false, active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT uq_tratamientos_tenant_codigo UNIQUE(tenant_id, codigo) ); COMMENT ON TABLE dental.tratamientos_catalogo IS 'Catálogo de tratamientos dentales'; -- ============================================================================ -- TABLAS PRINCIPALES -- ============================================================================ -- Odontograma CREATE TABLE IF NOT EXISTS dental.odontogramas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, -- Referencia a clinica.patients fecha_creacion DATE NOT NULL DEFAULT CURRENT_DATE, fecha_actualizacion DATE, notas TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.odontogramas IS 'Odontogramas de pacientes'; -- Estado de piezas dentales por odontograma CREATE TABLE IF NOT EXISTS dental.odontograma_piezas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, odontograma_id UUID NOT NULL REFERENCES dental.odontogramas(id) ON DELETE CASCADE, pieza_id UUID NOT NULL REFERENCES dental.piezas_dentales(id), -- Estado general estado dental.estado_pieza DEFAULT 'sano', -- Estados por cara (JSONB para flexibilidad) caras_afectadas JSONB, -- {"mesial": "caries", "oclusal": "obturacion"} -- Notas observaciones TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT uq_odontograma_pieza UNIQUE(odontograma_id, pieza_id) ); COMMENT ON TABLE dental.odontograma_piezas IS 'Estado de cada pieza dental en el odontograma'; -- Tratamientos de paciente CREATE TABLE IF NOT EXISTS dental.tratamientos_paciente ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, odontograma_id UUID REFERENCES dental.odontogramas(id), tratamiento_id UUID REFERENCES dental.tratamientos_catalogo(id), odontologo_id UUID, -- Referencia a clinica.doctors consultation_id UUID, -- Referencia a clinica.consultations -- Pieza(s) tratada(s) pieza_id UUID REFERENCES dental.piezas_dentales(id), caras_tratadas dental.cara_dental[], -- Datos del tratamiento fecha_inicio DATE NOT NULL DEFAULT CURRENT_DATE, fecha_fin DATE, estado dental.estado_tratamiento DEFAULT 'pendiente', -- Costo precio NUMERIC(10,2), descuento NUMERIC(5,2) DEFAULT 0, precio_final NUMERIC(10,2), -- Notas notas TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.tratamientos_paciente IS 'Tratamientos realizados a pacientes'; -- Ortodoncia CREATE TABLE IF NOT EXISTS dental.ortodoncia ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, odontologo_id UUID, -- Tipo de tratamiento tipo dental.tipo_ortodoncia NOT NULL, marca VARCHAR(100), -- Fechas fecha_inicio DATE NOT NULL, fecha_estimada_fin DATE, fecha_real_fin DATE, -- Estado estado dental.estado_tratamiento DEFAULT 'en_proceso', meses_estimados INTEGER, -- Costo costo_total NUMERIC(10,2), enganche NUMERIC(10,2), mensualidad NUMERIC(10,2), -- Notas notas TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.ortodoncia IS 'Tratamientos de ortodoncia'; -- Citas de ortodoncia (seguimiento) CREATE TABLE IF NOT EXISTS dental.ortodoncia_citas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, ortodoncia_id UUID NOT NULL REFERENCES dental.ortodoncia(id) ON DELETE CASCADE, appointment_id UUID, -- Referencia a clinica.appointments -- Datos de la cita fecha DATE NOT NULL, numero_cita INTEGER, -- Procedimiento procedimiento TEXT, arco_superior VARCHAR(50), arco_inferior VARCHAR(50), ligas VARCHAR(50), -- Observaciones observaciones TEXT, proxima_cita DATE, -- Control created_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.ortodoncia_citas IS 'Citas de seguimiento de ortodoncia'; -- Prótesis CREATE TABLE IF NOT EXISTS dental.protesis ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, odontologo_id UUID, -- Tipo tipo VARCHAR(50) NOT NULL, -- 'corona', 'puente', 'parcial', 'total', 'implante' -- Piezas involucradas piezas_involucradas TEXT[], -- ['11', '12', '13'] -- Laboratorio laboratorio_id UUID, -- Referencia a proveedor fecha_envio_lab DATE, fecha_recepcion_lab DATE, -- Material material VARCHAR(100), color VARCHAR(50), -- Estado estado dental.estado_tratamiento DEFAULT 'en_proceso', fecha_colocacion DATE, -- Garantía tiene_garantia BOOLEAN DEFAULT false, meses_garantia INTEGER, -- Costo costo_laboratorio NUMERIC(10,2), precio_paciente NUMERIC(10,2), -- Notas notas TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.protesis IS 'Registro de prótesis dentales'; -- Radiografías CREATE TABLE IF NOT EXISTS dental.radiografias ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, consultation_id UUID, -- Tipo tipo VARCHAR(50) NOT NULL, -- 'periapical', 'panoramica', 'cefalometrica', 'oclusal' pieza_id UUID REFERENCES dental.piezas_dentales(id), -- Archivo fecha DATE NOT NULL DEFAULT CURRENT_DATE, url_imagen VARCHAR(255), -- Interpretación interpretacion TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.radiografias IS 'Registro de radiografías dentales'; -- Presupuestos CREATE TABLE IF NOT EXISTS dental.presupuestos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, patient_id UUID NOT NULL, odontologo_id UUID, -- Datos numero VARCHAR(20), fecha DATE NOT NULL DEFAULT CURRENT_DATE, fecha_vencimiento DATE, -- Estado estado VARCHAR(20) DEFAULT 'pendiente', -- 'pendiente', 'aprobado', 'rechazado', 'vencido' -- Totales subtotal NUMERIC(12,2) DEFAULT 0, descuento_porcentaje NUMERIC(5,2) DEFAULT 0, descuento_monto NUMERIC(12,2) DEFAULT 0, total NUMERIC(12,2) DEFAULT 0, -- Plan de pago requiere_financiamiento BOOLEAN DEFAULT false, enganche NUMERIC(12,2), numero_pagos INTEGER, monto_pago NUMERIC(12,2), -- Notas notas TEXT, -- Control created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.presupuestos IS 'Presupuestos de tratamiento'; -- Líneas de presupuesto CREATE TABLE IF NOT EXISTS dental.presupuesto_lineas ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, presupuesto_id UUID NOT NULL REFERENCES dental.presupuestos(id) ON DELETE CASCADE, tratamiento_id UUID REFERENCES dental.tratamientos_catalogo(id), -- Pieza pieza_id UUID REFERENCES dental.piezas_dentales(id), descripcion TEXT, -- Cantidades cantidad INTEGER DEFAULT 1, precio_unitario NUMERIC(10,2), descuento NUMERIC(5,2) DEFAULT 0, subtotal NUMERIC(10,2), -- Secuencia sequence INTEGER DEFAULT 10, -- Control created_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE dental.presupuesto_lineas IS 'Líneas de presupuesto'; -- ============================================================================ -- EXTENSIONES A TABLAS DE ERP-CLINICAS -- ============================================================================ DO $$ BEGIN -- Extensión a clinica.patients IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'clinica' AND table_name = 'patients') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'clinica' AND table_name = 'patients' AND column_name = 'odontograma_activo_id') THEN ALTER TABLE clinica.patients ADD COLUMN odontograma_activo_id UUID; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'clinica' AND table_name = 'patients' AND column_name = 'tiene_ortodoncia') THEN ALTER TABLE clinica.patients ADD COLUMN tiene_ortodoncia BOOLEAN DEFAULT false; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'clinica' AND table_name = 'patients' AND column_name = 'tiene_protesis') THEN ALTER TABLE clinica.patients ADD COLUMN tiene_protesis BOOLEAN DEFAULT false; END IF; END IF; END $$; -- ============================================================================ -- SEED: Piezas dentales (catálogo global) -- ============================================================================ INSERT INTO dental.piezas_dentales (numero, nombre, cuadrante, es_temporal) VALUES -- Cuadrante 1 - Superior derecho (permanentes) ('18', 'Tercer molar superior derecho', 1, false), ('17', 'Segundo molar superior derecho', 1, false), ('16', 'Primer molar superior derecho', 1, false), ('15', 'Segundo premolar superior derecho', 1, false), ('14', 'Primer premolar superior derecho', 1, false), ('13', 'Canino superior derecho', 1, false), ('12', 'Incisivo lateral superior derecho', 1, false), ('11', 'Incisivo central superior derecho', 1, false), -- Cuadrante 2 - Superior izquierdo (permanentes) ('21', 'Incisivo central superior izquierdo', 2, false), ('22', 'Incisivo lateral superior izquierdo', 2, false), ('23', 'Canino superior izquierdo', 2, false), ('24', 'Primer premolar superior izquierdo', 2, false), ('25', 'Segundo premolar superior izquierdo', 2, false), ('26', 'Primer molar superior izquierdo', 2, false), ('27', 'Segundo molar superior izquierdo', 2, false), ('28', 'Tercer molar superior izquierdo', 2, false), -- Cuadrante 3 - Inferior izquierdo (permanentes) ('31', 'Incisivo central inferior izquierdo', 3, false), ('32', 'Incisivo lateral inferior izquierdo', 3, false), ('33', 'Canino inferior izquierdo', 3, false), ('34', 'Primer premolar inferior izquierdo', 3, false), ('35', 'Segundo premolar inferior izquierdo', 3, false), ('36', 'Primer molar inferior izquierdo', 3, false), ('37', 'Segundo molar inferior izquierdo', 3, false), ('38', 'Tercer molar inferior izquierdo', 3, false), -- Cuadrante 4 - Inferior derecho (permanentes) ('41', 'Incisivo central inferior derecho', 4, false), ('42', 'Incisivo lateral inferior derecho', 4, false), ('43', 'Canino inferior derecho', 4, false), ('44', 'Primer premolar inferior derecho', 4, false), ('45', 'Segundo premolar inferior derecho', 4, false), ('46', 'Primer molar inferior derecho', 4, false), ('47', 'Segundo molar inferior derecho', 4, false), ('48', 'Tercer molar inferior derecho', 4, false), -- Cuadrante 5 - Superior derecho (temporales) ('55', 'Segundo molar temporal superior derecho', 5, true), ('54', 'Primer molar temporal superior derecho', 5, true), ('53', 'Canino temporal superior derecho', 5, true), ('52', 'Incisivo lateral temporal superior derecho', 5, true), ('51', 'Incisivo central temporal superior derecho', 5, true), -- Cuadrante 6 - Superior izquierdo (temporales) ('61', 'Incisivo central temporal superior izquierdo', 6, true), ('62', 'Incisivo lateral temporal superior izquierdo', 6, true), ('63', 'Canino temporal superior izquierdo', 6, true), ('64', 'Primer molar temporal superior izquierdo', 6, true), ('65', 'Segundo molar temporal superior izquierdo', 6, true), -- Cuadrante 7 - Inferior izquierdo (temporales) ('71', 'Incisivo central temporal inferior izquierdo', 7, true), ('72', 'Incisivo lateral temporal inferior izquierdo', 7, true), ('73', 'Canino temporal inferior izquierdo', 7, true), ('74', 'Primer molar temporal inferior izquierdo', 7, true), ('75', 'Segundo molar temporal inferior izquierdo', 7, true), -- Cuadrante 8 - Inferior derecho (temporales) ('81', 'Incisivo central temporal inferior derecho', 8, true), ('82', 'Incisivo lateral temporal inferior derecho', 8, true), ('83', 'Canino temporal inferior derecho', 8, true), ('84', 'Primer molar temporal inferior derecho', 8, true), ('85', 'Segundo molar temporal inferior derecho', 8, true) ON CONFLICT (numero) DO NOTHING; -- ============================================================================ -- ÍNDICES -- ============================================================================ CREATE INDEX IF NOT EXISTS idx_tratamientos_catalogo_tenant ON dental.tratamientos_catalogo(tenant_id); CREATE INDEX IF NOT EXISTS idx_tratamientos_catalogo_categoria ON dental.tratamientos_catalogo(tenant_id, categoria); CREATE INDEX IF NOT EXISTS idx_odontogramas_tenant ON dental.odontogramas(tenant_id); CREATE INDEX IF NOT EXISTS idx_odontogramas_patient ON dental.odontogramas(patient_id); CREATE INDEX IF NOT EXISTS idx_odontograma_piezas_odontograma ON dental.odontograma_piezas(odontograma_id); CREATE INDEX IF NOT EXISTS idx_odontograma_piezas_pieza ON dental.odontograma_piezas(pieza_id); CREATE INDEX IF NOT EXISTS idx_tratamientos_paciente_tenant ON dental.tratamientos_paciente(tenant_id); CREATE INDEX IF NOT EXISTS idx_tratamientos_paciente_patient ON dental.tratamientos_paciente(patient_id); CREATE INDEX IF NOT EXISTS idx_tratamientos_paciente_estado ON dental.tratamientos_paciente(tenant_id, estado); CREATE INDEX IF NOT EXISTS idx_ortodoncia_tenant ON dental.ortodoncia(tenant_id); CREATE INDEX IF NOT EXISTS idx_ortodoncia_patient ON dental.ortodoncia(patient_id); CREATE INDEX IF NOT EXISTS idx_ortodoncia_estado ON dental.ortodoncia(tenant_id, estado); CREATE INDEX IF NOT EXISTS idx_ortodoncia_citas_ortodoncia ON dental.ortodoncia_citas(ortodoncia_id); CREATE INDEX IF NOT EXISTS idx_protesis_tenant ON dental.protesis(tenant_id); CREATE INDEX IF NOT EXISTS idx_protesis_patient ON dental.protesis(patient_id); CREATE INDEX IF NOT EXISTS idx_radiografias_tenant ON dental.radiografias(tenant_id); CREATE INDEX IF NOT EXISTS idx_radiografias_patient ON dental.radiografias(patient_id); CREATE INDEX IF NOT EXISTS idx_presupuestos_tenant ON dental.presupuestos(tenant_id); CREATE INDEX IF NOT EXISTS idx_presupuestos_patient ON dental.presupuestos(patient_id); CREATE INDEX IF NOT EXISTS idx_presupuestos_estado ON dental.presupuestos(tenant_id, estado); CREATE INDEX IF NOT EXISTS idx_presupuesto_lineas_presupuesto ON dental.presupuesto_lineas(presupuesto_id); -- ============================================================================ -- RLS -- ============================================================================ ALTER TABLE dental.tratamientos_catalogo ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.odontogramas ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.odontograma_piezas ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.tratamientos_paciente ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.ortodoncia ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.ortodoncia_citas ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.protesis ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.radiografias ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.presupuestos ENABLE ROW LEVEL SECURITY; ALTER TABLE dental.presupuesto_lineas ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation_tratamientos_cat ON dental.tratamientos_catalogo; CREATE POLICY tenant_isolation_tratamientos_cat ON dental.tratamientos_catalogo USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_odontogramas ON dental.odontogramas; CREATE POLICY tenant_isolation_odontogramas ON dental.odontogramas USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_odontograma_piezas ON dental.odontograma_piezas; CREATE POLICY tenant_isolation_odontograma_piezas ON dental.odontograma_piezas USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_tratamientos_pac ON dental.tratamientos_paciente; CREATE POLICY tenant_isolation_tratamientos_pac ON dental.tratamientos_paciente USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_ortodoncia ON dental.ortodoncia; CREATE POLICY tenant_isolation_ortodoncia ON dental.ortodoncia USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_ortodoncia_citas ON dental.ortodoncia_citas; CREATE POLICY tenant_isolation_ortodoncia_citas ON dental.ortodoncia_citas USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_protesis ON dental.protesis; CREATE POLICY tenant_isolation_protesis ON dental.protesis USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_radiografias ON dental.radiografias; CREATE POLICY tenant_isolation_radiografias ON dental.radiografias USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_presupuestos ON dental.presupuestos; CREATE POLICY tenant_isolation_presupuestos ON dental.presupuestos USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); DROP POLICY IF EXISTS tenant_isolation_presupuesto_lineas ON dental.presupuesto_lineas; CREATE POLICY tenant_isolation_presupuesto_lineas ON dental.presupuesto_lineas USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- ============================================================================ -- FIN DENTAL SCHEMA -- ============================================================================