[MULTI-REPO] Convertir a estructura multi-repo con submodules -v2

- backend → clinica-veterinaria-backend-v2.git
- database → clinica-veterinaria-database-v2.git
- frontend → clinica-veterinaria-frontend-v2.git

Estandarización arquitectura multi-repo

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-16 08:23:55 -06:00
parent e99d051743
commit f5a37ca344
7 changed files with 14 additions and 997 deletions

11
.gitmodules vendored Normal file
View File

@ -0,0 +1,11 @@
[submodule "backend"]
path = backend
url = git@gitea-server:rckrdmrd/clinica-veterinaria-backend-v2.git
[submodule "database"]
path = database
url = git@gitea-server:rckrdmrd/clinica-veterinaria-database-v2.git
[submodule "frontend"]
path = frontend
url = git@gitea-server:rckrdmrd/clinica-veterinaria-frontend-v2.git

1
backend Submodule

@ -0,0 +1 @@
Subproject commit 25d59cd031f9577af1a3e3fd7870887ca6db74be

1
database Submodule

@ -0,0 +1 @@
Subproject commit afa4f1a6fcb4a12c8e1eed7112972a5323cdad6e

View File

@ -1,387 +0,0 @@
-- ============================================================================
-- VETERINARIA SCHEMA - Especialización de ERP-Clínicas
-- Clínica Veterinaria
-- ============================================================================
-- Fecha: 2026-01-04
-- Versión: 1.0
-- Hereda de: erp-clinicas FASE-8
-- ============================================================================
-- Schema
CREATE SCHEMA IF NOT EXISTS veterinaria;
-- ============================================================================
-- ENUMS
-- ============================================================================
DO $$ BEGIN
CREATE TYPE veterinaria.sexo_animal AS ENUM ('macho', 'hembra', 'desconocido');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE veterinaria.estado_hospitalizacion AS ENUM (
'ingresado', 'en_tratamiento', 'estable', 'critico', 'alta', 'fallecido'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- ============================================================================
-- CATÁLOGOS
-- ============================================================================
-- Especies
CREATE TABLE IF NOT EXISTS veterinaria.especies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
nombre VARCHAR(50) NOT NULL,
nombre_cientifico VARCHAR(100),
descripcion TEXT,
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.especies IS 'Catálogo de especies animales';
-- Razas
CREATE TABLE IF NOT EXISTS veterinaria.razas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
especie_id UUID NOT NULL REFERENCES veterinaria.especies(id) ON DELETE CASCADE,
nombre VARCHAR(100) NOT NULL,
descripcion TEXT,
tamanio_promedio VARCHAR(20), -- 'pequeño', 'mediano', 'grande', 'gigante'
peso_promedio_kg NUMERIC(5,2),
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.razas IS 'Catálogo de razas por especie';
-- Vacunas
CREATE TABLE IF NOT EXISTS veterinaria.vacunas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
nombre VARCHAR(100) NOT NULL,
descripcion TEXT,
especie_id UUID REFERENCES veterinaria.especies(id),
laboratorio VARCHAR(100),
dosis_ml NUMERIC(5,2),
intervalo_refuerzo_dias INTEGER,
es_obligatoria BOOLEAN DEFAULT false,
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.vacunas IS 'Catálogo de vacunas veterinarias';
-- ============================================================================
-- TABLAS PRINCIPALES
-- ============================================================================
-- Propietarios (dueños de mascotas)
CREATE TABLE IF NOT EXISTS veterinaria.propietarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
partner_id UUID, -- Referencia opcional a core.partners
nombre VARCHAR(100) NOT NULL,
apellidos VARCHAR(100),
telefono VARCHAR(20),
telefono_emergencia VARCHAR(20),
email VARCHAR(100),
direccion TEXT,
rfc VARCHAR(13),
-- Control
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.propietarios IS 'Propietarios/dueños de mascotas';
-- Mascotas (pacientes)
CREATE TABLE IF NOT EXISTS veterinaria.mascotas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
propietario_id UUID NOT NULL REFERENCES veterinaria.propietarios(id),
especie_id UUID NOT NULL REFERENCES veterinaria.especies(id),
raza_id UUID REFERENCES veterinaria.razas(id),
-- Datos básicos
nombre VARCHAR(100) NOT NULL,
sexo veterinaria.sexo_animal DEFAULT 'desconocido',
fecha_nacimiento DATE,
edad_aproximada VARCHAR(50), -- "3 años", "6 meses"
color VARCHAR(50),
peso_kg NUMERIC(6,2),
-- Identificación
numero_chip VARCHAR(50),
tiene_chip BOOLEAN DEFAULT false,
-- Estado
esterilizado BOOLEAN DEFAULT false,
fecha_esterilizacion DATE,
-- Notas
alergias TEXT,
condiciones_especiales TEXT,
notas TEXT,
-- Foto
foto_url VARCHAR(255),
-- Control
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.mascotas IS 'Mascotas/pacientes de la clínica veterinaria';
-- Cartilla de vacunación
CREATE TABLE IF NOT EXISTS veterinaria.cartilla_vacunacion (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id) ON DELETE CASCADE,
vacuna_id UUID NOT NULL REFERENCES veterinaria.vacunas(id),
veterinario_id UUID, -- Referencia a clinica.doctors
-- Datos de aplicación
fecha_aplicacion DATE NOT NULL,
fecha_proximo_refuerzo DATE,
lote VARCHAR(50),
laboratorio VARCHAR(100),
-- Notas
observaciones TEXT,
-- Control
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.cartilla_vacunacion IS 'Historial de vacunación de mascotas';
-- Desparasitaciones
CREATE TABLE IF NOT EXISTS veterinaria.desparasitaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id) ON DELETE CASCADE,
veterinario_id UUID,
-- Datos
tipo VARCHAR(50) NOT NULL, -- 'interna', 'externa', 'ambas'
producto VARCHAR(100) NOT NULL,
dosis VARCHAR(50),
via_administracion VARCHAR(50),
fecha_aplicacion DATE NOT NULL,
fecha_proxima DATE,
-- Notas
observaciones TEXT,
-- Control
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.desparasitaciones IS 'Historial de desparasitaciones';
-- Hospitalización
CREATE TABLE IF NOT EXISTS veterinaria.hospitalizacion (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id),
veterinario_id UUID,
consultation_id UUID, -- Referencia a clinica.consultations
-- Datos de ingreso
fecha_ingreso TIMESTAMPTZ NOT NULL DEFAULT NOW(),
motivo_ingreso TEXT NOT NULL,
diagnostico_ingreso TEXT,
-- Ubicación
area VARCHAR(50), -- 'jaula_pequena', 'jaula_grande', 'quirofano', 'uci'
numero_jaula VARCHAR(20),
-- Estado
estado veterinaria.estado_hospitalizacion DEFAULT 'ingresado',
-- Alta
fecha_alta TIMESTAMPTZ,
diagnostico_alta TEXT,
instrucciones_alta TEXT,
-- Control
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.hospitalizacion IS 'Registro de hospitalizaciones';
-- Monitoreo de hospitalización
CREATE TABLE IF NOT EXISTS veterinaria.hospitalizacion_monitoreo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
hospitalizacion_id UUID NOT NULL REFERENCES veterinaria.hospitalizacion(id) ON DELETE CASCADE,
-- Signos vitales
fecha_hora TIMESTAMPTZ NOT NULL DEFAULT NOW(),
peso_kg NUMERIC(6,2),
temperatura NUMERIC(4,1),
frecuencia_cardiaca INTEGER,
frecuencia_respiratoria INTEGER,
-- Alimentación
comio BOOLEAN,
bebio_agua BOOLEAN,
-- Eliminación
orino BOOLEAN,
defeco BOOLEAN,
consistencia_heces VARCHAR(50),
-- Estado
estado_animo VARCHAR(50),
nivel_dolor INTEGER CHECK (nivel_dolor BETWEEN 0 AND 10),
-- Notas
observaciones TEXT,
registrado_por UUID, -- employee_id
-- Control
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.hospitalizacion_monitoreo IS 'Monitoreo durante hospitalización';
-- Servicios de estética
CREATE TABLE IF NOT EXISTS veterinaria.estetica (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
mascota_id UUID NOT NULL REFERENCES veterinaria.mascotas(id),
estilista_id UUID, -- employee_id
-- Servicios
fecha_servicio TIMESTAMPTZ NOT NULL DEFAULT NOW(),
servicios TEXT[], -- ['baño', 'corte', 'limpieza_oidos', 'corte_unas']
tipo_corte VARCHAR(50),
shampoo_usado VARCHAR(100),
-- Estado
estado VARCHAR(20) DEFAULT 'pendiente', -- 'pendiente', 'en_proceso', 'terminado'
hora_inicio TIME,
hora_fin TIME,
-- Notas
observaciones TEXT,
observaciones_piel TEXT,
-- Precio
precio NUMERIC(10,2),
-- Control
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.estetica IS 'Servicios de estética/grooming';
-- ============================================================================
-- EXTENSIONES A TABLAS DE ERP-CLINICAS
-- ============================================================================
-- Extensión a clinica.consultations (si existe)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema = 'clinica' AND table_name = 'consultations') THEN
-- mascota_id
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'clinica' AND table_name = 'consultations'
AND column_name = 'mascota_id') THEN
ALTER TABLE clinica.consultations ADD COLUMN mascota_id UUID
REFERENCES veterinaria.mascotas(id);
END IF;
-- peso_actual
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'clinica' AND table_name = 'consultations'
AND column_name = 'peso_actual') THEN
ALTER TABLE clinica.consultations ADD COLUMN peso_actual NUMERIC(6,2);
END IF;
-- temperatura
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'clinica' AND table_name = 'consultations'
AND column_name = 'temperatura') THEN
ALTER TABLE clinica.consultations ADD COLUMN temperatura NUMERIC(4,1);
END IF;
END IF;
END $$;
-- ============================================================================
-- ÍNDICES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_especies_tenant ON veterinaria.especies(tenant_id);
CREATE INDEX IF NOT EXISTS idx_razas_tenant ON veterinaria.razas(tenant_id);
CREATE INDEX IF NOT EXISTS idx_razas_especie ON veterinaria.razas(especie_id);
CREATE INDEX IF NOT EXISTS idx_vacunas_tenant ON veterinaria.vacunas(tenant_id);
CREATE INDEX IF NOT EXISTS idx_vacunas_especie ON veterinaria.vacunas(especie_id);
CREATE INDEX IF NOT EXISTS idx_propietarios_tenant ON veterinaria.propietarios(tenant_id);
CREATE INDEX IF NOT EXISTS idx_propietarios_telefono ON veterinaria.propietarios(telefono);
CREATE INDEX IF NOT EXISTS idx_mascotas_tenant ON veterinaria.mascotas(tenant_id);
CREATE INDEX IF NOT EXISTS idx_mascotas_propietario ON veterinaria.mascotas(propietario_id);
CREATE INDEX IF NOT EXISTS idx_mascotas_especie ON veterinaria.mascotas(especie_id);
CREATE INDEX IF NOT EXISTS idx_mascotas_chip ON veterinaria.mascotas(numero_chip) WHERE numero_chip IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_cartilla_tenant ON veterinaria.cartilla_vacunacion(tenant_id);
CREATE INDEX IF NOT EXISTS idx_cartilla_mascota ON veterinaria.cartilla_vacunacion(mascota_id);
CREATE INDEX IF NOT EXISTS idx_cartilla_fecha ON veterinaria.cartilla_vacunacion(fecha_proximo_refuerzo);
CREATE INDEX IF NOT EXISTS idx_desparasitaciones_tenant ON veterinaria.desparasitaciones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_desparasitaciones_mascota ON veterinaria.desparasitaciones(mascota_id);
CREATE INDEX IF NOT EXISTS idx_hospitalizacion_tenant ON veterinaria.hospitalizacion(tenant_id);
CREATE INDEX IF NOT EXISTS idx_hospitalizacion_mascota ON veterinaria.hospitalizacion(mascota_id);
CREATE INDEX IF NOT EXISTS idx_hospitalizacion_estado ON veterinaria.hospitalizacion(tenant_id, estado);
CREATE INDEX IF NOT EXISTS idx_hospitalizacion_monitoreo_hosp ON veterinaria.hospitalizacion_monitoreo(hospitalizacion_id);
CREATE INDEX IF NOT EXISTS idx_estetica_tenant ON veterinaria.estetica(tenant_id);
CREATE INDEX IF NOT EXISTS idx_estetica_mascota ON veterinaria.estetica(mascota_id);
CREATE INDEX IF NOT EXISTS idx_estetica_fecha ON veterinaria.estetica(fecha_servicio);
-- ============================================================================
-- RLS
-- ============================================================================
ALTER TABLE veterinaria.especies ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.razas ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.vacunas ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.propietarios ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.mascotas ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.cartilla_vacunacion ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.desparasitaciones ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.hospitalizacion ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.hospitalizacion_monitoreo ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.estetica ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation_especies ON veterinaria.especies;
CREATE POLICY tenant_isolation_especies ON veterinaria.especies
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_razas ON veterinaria.razas;
CREATE POLICY tenant_isolation_razas ON veterinaria.razas
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_vacunas ON veterinaria.vacunas;
CREATE POLICY tenant_isolation_vacunas ON veterinaria.vacunas
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_propietarios ON veterinaria.propietarios;
CREATE POLICY tenant_isolation_propietarios ON veterinaria.propietarios
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_mascotas ON veterinaria.mascotas;
CREATE POLICY tenant_isolation_mascotas ON veterinaria.mascotas
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_cartilla ON veterinaria.cartilla_vacunacion;
CREATE POLICY tenant_isolation_cartilla ON veterinaria.cartilla_vacunacion
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_desparasitaciones ON veterinaria.desparasitaciones;
CREATE POLICY tenant_isolation_desparasitaciones ON veterinaria.desparasitaciones
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_hospitalizacion ON veterinaria.hospitalizacion;
CREATE POLICY tenant_isolation_hospitalizacion ON veterinaria.hospitalizacion
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_hosp_monitoreo ON veterinaria.hospitalizacion_monitoreo;
CREATE POLICY tenant_isolation_hosp_monitoreo ON veterinaria.hospitalizacion_monitoreo
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_estetica ON veterinaria.estetica;
CREATE POLICY tenant_isolation_estetica ON veterinaria.estetica
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- ============================================================================
-- FIN VETERINARIA SCHEMA
-- ============================================================================

View File

@ -1,464 +0,0 @@
-- ============================================================================
-- VETERINARIA SCHEMA - Farmacia (VET-006)
-- Sistema de gestion de farmacia veterinaria
-- ============================================================================
-- Fecha: 2026-01-07
-- Version: 1.0
-- Basado en: VET-006-farmacia.md
-- ============================================================================
-- ============================================================================
-- ENUMS
-- ============================================================================
DO $$ BEGIN
CREATE TYPE veterinaria.categoria_medicamento AS ENUM (
'antibiotico',
'antiparasitario',
'analgesico',
'antiinflamatorio',
'vacuna',
'vitamina',
'dermatologico',
'oftalmico',
'cardiaco',
'digestivo',
'otro'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE veterinaria.tipo_movimiento_farmacia AS ENUM (
'entrada',
'salida',
'ajuste_positivo',
'ajuste_negativo',
'devolucion',
'merma'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE veterinaria.fraccion_controlada AS ENUM (
'no_controlado',
'fraccion_i',
'fraccion_ii',
'fraccion_iii',
'fraccion_iv'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- ============================================================================
-- TABLAS PRINCIPALES
-- ============================================================================
-- Catalogo de medicamentos
CREATE TABLE IF NOT EXISTS veterinaria.medicamentos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Identificacion
codigo VARCHAR(50),
nombre VARCHAR(150) NOT NULL,
nombre_comercial VARCHAR(150),
principio_activo VARCHAR(200),
-- Clasificacion
categoria veterinaria.categoria_medicamento DEFAULT 'otro',
-- Presentacion
presentacion VARCHAR(100), -- 'tabletas', 'inyectable', 'suspension', etc.
concentracion VARCHAR(50), -- '500mg', '100mg/ml'
contenido VARCHAR(50), -- '30 tabletas', '100ml'
-- Fabricante
laboratorio VARCHAR(100),
-- Control
requiere_receta BOOLEAN DEFAULT false,
controlado BOOLEAN DEFAULT false,
fraccion_controlada veterinaria.fraccion_controlada DEFAULT 'no_controlado',
-- Stock
stock_minimo INTEGER DEFAULT 10,
stock_actual INTEGER DEFAULT 0,
-- Precios
precio_compra NUMERIC(10,2),
precio_venta NUMERIC(10,2),
-- Especies aplicables (NULL = todas)
especies_aplicables UUID[], -- Array de especie_id
-- Estado
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.medicamentos IS 'Catalogo de medicamentos veterinarios';
-- Lotes de medicamentos (control de caducidad)
CREATE TABLE IF NOT EXISTS veterinaria.medicamentos_lotes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id) ON DELETE CASCADE,
-- Lote
numero_lote VARCHAR(50) NOT NULL,
fecha_caducidad DATE NOT NULL,
-- Cantidades
cantidad_inicial INTEGER NOT NULL,
cantidad_actual INTEGER NOT NULL,
-- Compra
precio_compra NUMERIC(10,2),
factura_compra VARCHAR(50),
proveedor VARCHAR(100),
fecha_recepcion DATE DEFAULT CURRENT_DATE,
-- Estado
bloqueado BOOLEAN DEFAULT false, -- Bloquear si esta vencido
motivo_bloqueo TEXT,
-- Control
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.medicamentos_lotes IS 'Lotes de medicamentos con control de caducidad';
-- Dispensaciones de medicamentos
CREATE TABLE IF NOT EXISTS veterinaria.dispensaciones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Referencias
medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id),
lote_id UUID NOT NULL REFERENCES veterinaria.medicamentos_lotes(id),
mascota_id UUID REFERENCES veterinaria.mascotas(id),
veterinario_id UUID, -- Referencia a clinica.doctors
receta_id UUID, -- Referencia a clinica.prescriptions si existe
consultation_id UUID, -- Referencia a clinica.consultations
-- Dispensacion
cantidad INTEGER NOT NULL,
fecha_dispensacion TIMESTAMPTZ DEFAULT NOW(),
-- Instrucciones
dosis VARCHAR(100), -- '1 tableta cada 8 horas'
duracion_tratamiento VARCHAR(50), -- '7 dias'
instrucciones TEXT,
-- Control
dispensado_por UUID, -- employee_id
notas TEXT,
-- Auditoria
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.dispensaciones IS 'Registro de dispensacion de medicamentos';
-- Movimientos de inventario (kardex)
CREATE TABLE IF NOT EXISTS veterinaria.movimientos_farmacia (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id),
lote_id UUID REFERENCES veterinaria.medicamentos_lotes(id),
-- Movimiento
tipo veterinaria.tipo_movimiento_farmacia NOT NULL,
cantidad INTEGER NOT NULL,
stock_anterior INTEGER NOT NULL,
stock_posterior INTEGER NOT NULL,
-- Referencia
referencia_tipo VARCHAR(50), -- 'dispensacion', 'compra', 'ajuste'
referencia_id UUID,
-- Detalles
motivo TEXT,
documento VARCHAR(100), -- Factura, nota, etc.
-- Auditoria
usuario_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.movimientos_farmacia IS 'Kardex de movimientos de inventario de farmacia';
-- Bitacora de medicamentos controlados
CREATE TABLE IF NOT EXISTS veterinaria.bitacora_controlados (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
medicamento_id UUID NOT NULL REFERENCES veterinaria.medicamentos(id),
lote_id UUID REFERENCES veterinaria.medicamentos_lotes(id),
dispensacion_id UUID REFERENCES veterinaria.dispensaciones(id),
-- Movimiento
tipo_movimiento veterinaria.tipo_movimiento_farmacia NOT NULL,
cantidad INTEGER NOT NULL,
-- Paciente
mascota_id UUID REFERENCES veterinaria.mascotas(id),
propietario_nombre VARCHAR(200), -- Snapshot del nombre
-- Prescripcion
receta_id UUID,
veterinario_id UUID,
veterinario_cedula VARCHAR(50),
-- Justificacion
justificacion TEXT NOT NULL,
diagnostico TEXT,
-- Auditoria
fecha_registro TIMESTAMPTZ DEFAULT NOW(),
registrado_por UUID NOT NULL,
ip_address VARCHAR(45),
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE veterinaria.bitacora_controlados IS 'Bitacora de movimientos de medicamentos controlados (requerido por COFEPRIS)';
-- ============================================================================
-- INDICES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_medicamentos_tenant ON veterinaria.medicamentos(tenant_id);
CREATE INDEX IF NOT EXISTS idx_medicamentos_codigo ON veterinaria.medicamentos(tenant_id, codigo);
CREATE INDEX IF NOT EXISTS idx_medicamentos_nombre ON veterinaria.medicamentos(tenant_id, nombre);
CREATE INDEX IF NOT EXISTS idx_medicamentos_categoria ON veterinaria.medicamentos(tenant_id, categoria);
CREATE INDEX IF NOT EXISTS idx_medicamentos_controlado ON veterinaria.medicamentos(tenant_id, controlado) WHERE controlado = true;
CREATE INDEX IF NOT EXISTS idx_medicamentos_stock_bajo ON veterinaria.medicamentos(tenant_id)
WHERE stock_actual <= stock_minimo;
CREATE INDEX IF NOT EXISTS idx_lotes_tenant ON veterinaria.medicamentos_lotes(tenant_id);
CREATE INDEX IF NOT EXISTS idx_lotes_medicamento ON veterinaria.medicamentos_lotes(medicamento_id);
CREATE INDEX IF NOT EXISTS idx_lotes_caducidad ON veterinaria.medicamentos_lotes(fecha_caducidad);
CREATE INDEX IF NOT EXISTS idx_lotes_numero ON veterinaria.medicamentos_lotes(tenant_id, numero_lote);
CREATE INDEX IF NOT EXISTS idx_lotes_proximos_caducar ON veterinaria.medicamentos_lotes(tenant_id, fecha_caducidad)
WHERE cantidad_actual > 0 AND bloqueado = false;
CREATE INDEX IF NOT EXISTS idx_dispensaciones_tenant ON veterinaria.dispensaciones(tenant_id);
CREATE INDEX IF NOT EXISTS idx_dispensaciones_medicamento ON veterinaria.dispensaciones(medicamento_id);
CREATE INDEX IF NOT EXISTS idx_dispensaciones_mascota ON veterinaria.dispensaciones(mascota_id);
CREATE INDEX IF NOT EXISTS idx_dispensaciones_fecha ON veterinaria.dispensaciones(fecha_dispensacion);
CREATE INDEX IF NOT EXISTS idx_dispensaciones_veterinario ON veterinaria.dispensaciones(veterinario_id);
CREATE INDEX IF NOT EXISTS idx_movimientos_tenant ON veterinaria.movimientos_farmacia(tenant_id);
CREATE INDEX IF NOT EXISTS idx_movimientos_medicamento ON veterinaria.movimientos_farmacia(medicamento_id);
CREATE INDEX IF NOT EXISTS idx_movimientos_fecha ON veterinaria.movimientos_farmacia(created_at);
CREATE INDEX IF NOT EXISTS idx_bitacora_tenant ON veterinaria.bitacora_controlados(tenant_id);
CREATE INDEX IF NOT EXISTS idx_bitacora_medicamento ON veterinaria.bitacora_controlados(medicamento_id);
CREATE INDEX IF NOT EXISTS idx_bitacora_fecha ON veterinaria.bitacora_controlados(fecha_registro);
-- ============================================================================
-- RLS
-- ============================================================================
ALTER TABLE veterinaria.medicamentos ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.medicamentos_lotes ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.dispensaciones ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.movimientos_farmacia ENABLE ROW LEVEL SECURITY;
ALTER TABLE veterinaria.bitacora_controlados ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation_medicamentos ON veterinaria.medicamentos;
CREATE POLICY tenant_isolation_medicamentos ON veterinaria.medicamentos
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_lotes ON veterinaria.medicamentos_lotes;
CREATE POLICY tenant_isolation_lotes ON veterinaria.medicamentos_lotes
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_dispensaciones ON veterinaria.dispensaciones;
CREATE POLICY tenant_isolation_dispensaciones ON veterinaria.dispensaciones
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_movimientos ON veterinaria.movimientos_farmacia;
CREATE POLICY tenant_isolation_movimientos ON veterinaria.movimientos_farmacia
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
DROP POLICY IF EXISTS tenant_isolation_bitacora ON veterinaria.bitacora_controlados;
CREATE POLICY tenant_isolation_bitacora ON veterinaria.bitacora_controlados
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- ============================================================================
-- TRIGGERS
-- ============================================================================
-- Trigger para actualizar stock_actual en medicamentos
CREATE OR REPLACE FUNCTION veterinaria.actualizar_stock_medicamento()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
-- Actualizar stock del medicamento
UPDATE veterinaria.medicamentos
SET stock_actual = stock_actual + NEW.cantidad_inicial,
updated_at = NOW()
WHERE id = NEW.medicamento_id;
ELSIF TG_OP = 'UPDATE' THEN
-- Si cambia cantidad_actual
UPDATE veterinaria.medicamentos
SET stock_actual = (
SELECT COALESCE(SUM(cantidad_actual), 0)
FROM veterinaria.medicamentos_lotes
WHERE medicamento_id = NEW.medicamento_id
AND bloqueado = false
),
updated_at = NOW()
WHERE id = NEW.medicamento_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_actualizar_stock ON veterinaria.medicamentos_lotes;
CREATE TRIGGER trg_actualizar_stock
AFTER INSERT OR UPDATE OF cantidad_actual ON veterinaria.medicamentos_lotes
FOR EACH ROW
EXECUTE FUNCTION veterinaria.actualizar_stock_medicamento();
COMMENT ON FUNCTION veterinaria.actualizar_stock_medicamento() IS 'Actualiza stock_actual en medicamentos cuando cambian los lotes';
-- Trigger para registrar movimiento en dispensacion
CREATE OR REPLACE FUNCTION veterinaria.registrar_movimiento_dispensacion()
RETURNS TRIGGER AS $$
DECLARE
v_stock_anterior INTEGER;
BEGIN
-- Obtener stock anterior
SELECT stock_actual INTO v_stock_anterior
FROM veterinaria.medicamentos
WHERE id = NEW.medicamento_id;
-- Descontar del lote
UPDATE veterinaria.medicamentos_lotes
SET cantidad_actual = cantidad_actual - NEW.cantidad,
updated_at = NOW()
WHERE id = NEW.lote_id;
-- Registrar movimiento
INSERT INTO veterinaria.movimientos_farmacia (
tenant_id, medicamento_id, lote_id,
tipo, cantidad, stock_anterior, stock_posterior,
referencia_tipo, referencia_id, usuario_id
) VALUES (
NEW.tenant_id, NEW.medicamento_id, NEW.lote_id,
'salida', NEW.cantidad, v_stock_anterior, v_stock_anterior - NEW.cantidad,
'dispensacion', NEW.id, NEW.dispensado_por
);
-- Si es controlado, registrar en bitacora
IF EXISTS (
SELECT 1 FROM veterinaria.medicamentos
WHERE id = NEW.medicamento_id AND controlado = true
) THEN
INSERT INTO veterinaria.bitacora_controlados (
tenant_id, medicamento_id, lote_id, dispensacion_id,
tipo_movimiento, cantidad, mascota_id,
propietario_nombre, receta_id, veterinario_id,
justificacion, registrado_por
)
SELECT
NEW.tenant_id, NEW.medicamento_id, NEW.lote_id, NEW.id,
'salida', NEW.cantidad, NEW.mascota_id,
CONCAT(p.nombre, ' ', COALESCE(p.apellidos, '')),
NEW.receta_id, NEW.veterinario_id,
COALESCE(NEW.notas, 'Dispensacion de medicamento'),
NEW.dispensado_por
FROM veterinaria.mascotas m
JOIN veterinaria.propietarios p ON m.propietario_id = p.id
WHERE m.id = NEW.mascota_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_registrar_dispensacion ON veterinaria.dispensaciones;
CREATE TRIGGER trg_registrar_dispensacion
AFTER INSERT ON veterinaria.dispensaciones
FOR EACH ROW
EXECUTE FUNCTION veterinaria.registrar_movimiento_dispensacion();
COMMENT ON FUNCTION veterinaria.registrar_movimiento_dispensacion() IS 'Registra movimiento y bitacora al dispensar medicamentos';
-- ============================================================================
-- FUNCIONES DE CONSULTA
-- ============================================================================
-- Funcion para obtener lotes proximos a caducar
CREATE OR REPLACE FUNCTION veterinaria.get_lotes_proximos_caducar(
p_tenant_id UUID,
p_dias INTEGER DEFAULT 30
)
RETURNS TABLE (
lote_id UUID,
medicamento_id UUID,
medicamento_nombre VARCHAR,
numero_lote VARCHAR,
fecha_caducidad DATE,
dias_restantes INTEGER,
cantidad_actual INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
l.id AS lote_id,
m.id AS medicamento_id,
m.nombre AS medicamento_nombre,
l.numero_lote,
l.fecha_caducidad,
(l.fecha_caducidad - CURRENT_DATE)::INTEGER AS dias_restantes,
l.cantidad_actual
FROM veterinaria.medicamentos_lotes l
JOIN veterinaria.medicamentos m ON l.medicamento_id = m.id
WHERE l.tenant_id = p_tenant_id
AND l.cantidad_actual > 0
AND l.bloqueado = false
AND l.fecha_caducidad <= CURRENT_DATE + p_dias
ORDER BY l.fecha_caducidad ASC;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION veterinaria.get_lotes_proximos_caducar(UUID, INTEGER) IS 'Obtiene lotes que caducaran en los proximos N dias';
-- Funcion para obtener medicamentos con stock bajo
CREATE OR REPLACE FUNCTION veterinaria.get_medicamentos_stock_bajo(p_tenant_id UUID)
RETURNS TABLE (
medicamento_id UUID,
codigo VARCHAR,
nombre VARCHAR,
stock_actual INTEGER,
stock_minimo INTEGER,
diferencia INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
m.id AS medicamento_id,
m.codigo,
m.nombre,
m.stock_actual,
m.stock_minimo,
(m.stock_minimo - m.stock_actual) AS diferencia
FROM veterinaria.medicamentos m
WHERE m.tenant_id = p_tenant_id
AND m.active = true
AND m.stock_actual <= m.stock_minimo
ORDER BY diferencia DESC;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION veterinaria.get_medicamentos_stock_bajo(UUID) IS 'Obtiene medicamentos con stock igual o menor al minimo';
-- Funcion para seleccionar lote FEFO (First Expired, First Out)
CREATE OR REPLACE FUNCTION veterinaria.seleccionar_lote_fefo(
p_medicamento_id UUID,
p_cantidad INTEGER
)
RETURNS UUID AS $$
DECLARE
v_lote_id UUID;
BEGIN
SELECT id INTO v_lote_id
FROM veterinaria.medicamentos_lotes
WHERE medicamento_id = p_medicamento_id
AND cantidad_actual >= p_cantidad
AND bloqueado = false
AND fecha_caducidad > CURRENT_DATE
ORDER BY fecha_caducidad ASC
LIMIT 1;
IF v_lote_id IS NULL THEN
RAISE EXCEPTION 'No hay lotes disponibles con stock suficiente para el medicamento %', p_medicamento_id;
END IF;
RETURN v_lote_id;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION veterinaria.seleccionar_lote_fefo(UUID, INTEGER) IS 'Selecciona el lote con fecha de caducidad mas proxima (FEFO)';
-- ============================================================================
-- FIN VETERINARIA FARMACIA SCHEMA
-- ============================================================================

View File

@ -1,146 +0,0 @@
-- ============================================================================
-- SEED DATA: Catálogos de Veterinaria
-- Especialización de ERP-Clínicas
-- ============================================================================
-- NOTA: Ejecutar después de SET app.current_tenant_id = 'UUID-DEL-TENANT';
-- ============================================================================
-- Especies
INSERT INTO veterinaria.especies (tenant_id, nombre, nombre_cientifico)
SELECT current_setting('app.current_tenant_id', true)::UUID, nombre, nombre_cientifico
FROM (VALUES
('Perro', 'Canis lupus familiaris'),
('Gato', 'Felis silvestris catus'),
('Ave', NULL),
('Reptil', NULL),
('Roedor', NULL),
('Conejo', 'Oryctolagus cuniculus'),
('Pez', NULL),
('Hurón', 'Mustela putorius furo'),
('Otro', NULL)
) AS t(nombre, nombre_cientifico)
WHERE current_setting('app.current_tenant_id', true) IS NOT NULL
AND current_setting('app.current_tenant_id', true) != ''
ON CONFLICT DO NOTHING;
-- Razas de perro
INSERT INTO veterinaria.razas (tenant_id, especie_id, nombre, tamanio_promedio, peso_promedio_kg)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
e.id,
r.nombre,
r.tamanio,
r.peso
FROM veterinaria.especies e
CROSS JOIN (VALUES
('Mestizo', 'mediano', 15.0),
('Chihuahua', 'pequeño', 2.5),
('Poodle', 'pequeño', 5.0),
('Bulldog Francés', 'pequeño', 12.0),
('Beagle', 'mediano', 12.0),
('Labrador Retriever', 'grande', 30.0),
('Golden Retriever', 'grande', 32.0),
('Pastor Alemán', 'grande', 35.0),
('Rottweiler', 'grande', 45.0),
('Husky Siberiano', 'grande', 25.0),
('Pug', 'pequeño', 8.0),
('Yorkshire Terrier', 'pequeño', 3.0),
('Schnauzer', 'mediano', 7.0),
('Boxer', 'grande', 30.0),
('Pitbull', 'mediano', 25.0)
) AS r(nombre, tamanio, peso)
WHERE e.nombre = 'Perro'
AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;
-- Razas de gato
INSERT INTO veterinaria.razas (tenant_id, especie_id, nombre, tamanio_promedio, peso_promedio_kg)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
e.id,
r.nombre,
r.tamanio,
r.peso
FROM veterinaria.especies e
CROSS JOIN (VALUES
('Mestizo', 'mediano', 4.0),
('Siamés', 'mediano', 4.5),
('Persa', 'mediano', 5.0),
('Maine Coon', 'grande', 8.0),
('Bengalí', 'mediano', 5.5),
('Ragdoll', 'grande', 7.0),
('British Shorthair', 'mediano', 6.0),
('Angora', 'mediano', 4.5),
('Sphynx', 'mediano', 4.0),
('Abisinio', 'mediano', 4.0)
) AS r(nombre, tamanio, peso)
WHERE e.nombre = 'Gato'
AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;
-- Vacunas para perros
INSERT INTO veterinaria.vacunas (tenant_id, especie_id, nombre, descripcion, intervalo_refuerzo_dias, es_obligatoria)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
e.id,
v.nombre,
v.descripcion,
v.intervalo,
v.obligatoria
FROM veterinaria.especies e
CROSS JOIN (VALUES
('Parvovirus', 'Protege contra parvovirus canino', 365, false),
('Moquillo', 'Protege contra distemper canino', 365, false),
('Hepatitis', 'Protege contra hepatitis infecciosa canina', 365, false),
('Rabia', 'Vacuna antirrábica - OBLIGATORIA', 365, true),
('Leptospirosis', 'Protege contra leptospirosis', 365, false),
('Bordetella', 'Protege contra tos de las perreras', 180, false),
('Cuádruple', 'Moquillo, Hepatitis, Parvo, Parainfluenza', 365, false),
('Séxtuple', 'Cuádruple + Coronavirus + Leptospira', 365, false)
) AS v(nombre, descripcion, intervalo, obligatoria)
WHERE e.nombre = 'Perro'
AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;
-- Vacunas para gatos
INSERT INTO veterinaria.vacunas (tenant_id, especie_id, nombre, descripcion, intervalo_refuerzo_dias, es_obligatoria)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
e.id,
v.nombre,
v.descripcion,
v.intervalo,
v.obligatoria
FROM veterinaria.especies e
CROSS JOIN (VALUES
('Triple Felina', 'Rinotraqueitis, Calicivirus, Panleucopenia', 365, false),
('Leucemia Felina', 'Protege contra FeLV', 365, false),
('Rabia', 'Vacuna antirrábica', 365, true),
('PIF', 'Peritonitis Infecciosa Felina', 365, false)
) AS v(nombre, descripcion, intervalo, obligatoria)
WHERE e.nombre = 'Gato'
AND e.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;
-- Skills específicos veterinarios
INSERT INTO hr.skills (tenant_id, skill_type_id, name, requiere_cedula)
SELECT
current_setting('app.current_tenant_id', true)::UUID,
st.id,
unnest(ARRAY[
'Medicina Veterinaria General',
'Cirugía Veterinaria',
'Dermatología Veterinaria',
'Cardiología Veterinaria',
'Oftalmología Veterinaria',
'Ortopedia Veterinaria',
'Oncología Veterinaria',
'Medicina de Exóticos',
'Anestesiología Veterinaria',
'Imagenología Veterinaria'
]),
true
FROM hr.skill_types st
WHERE st.name = 'Especialidad Médica'
AND st.tenant_id = current_setting('app.current_tenant_id', true)::UUID
ON CONFLICT DO NOTHING;

1
frontend Submodule

@ -0,0 +1 @@
Subproject commit 35f98c825860ee86b4de996385f7f3d22e50e7cc