agregada funcionalidad realtime postgress, api y ui.

This commit is contained in:
2025-06-09 16:35:36 -06:00
parent bec28b74ab
commit 513c305971
9 changed files with 117 additions and 77 deletions

View File

@@ -50,3 +50,10 @@ mcp:
api: api:
cd api && npm install && npm run dev cd api && npm install && npm run dev
# Creates a new Prisma migration in development mode.
# Pass the migration name as an argument, e.g.:
# make prisma-migrate-dev name=my-migration-name
# If no name is provided, it defaults to "new_migration".
prisma-migrate-dev:
cd api && npx prisma migrate deploy

View File

@@ -1,26 +0,0 @@
-- Create function and trigger to notify SSE on planilla changes
CREATE OR REPLACE FUNCTION notify_planilla_change() RETURNS trigger AS $$
DECLARE
payload TEXT;
BEGIN
payload := json_build_object(
'table', TG_TABLE_NAME,
'operation', TG_OP,
'old', row_to_json(OLD),
'new', row_to_json(NEW)
)::text;
PERFORM pg_notify('sse_events', payload);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS planilla_notify_trigger ON "Planilla";
CREATE TRIGGER planilla_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "Planilla"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_planilla_change();

View File

@@ -1,6 +1,10 @@
-- Triggers for other tables to notify SSE channel -- prisma/migrations/20250609211800_notify_others_sse/migration.sql
/* 0⃣ Limpieza */
DROP FUNCTION IF EXISTS notify_planilla_change() CASCADE;
DROP FUNCTION IF EXISTS notify_generic_change() CASCADE;
CREATE OR REPLACE FUNCTION notify_generic_change() /* 1⃣ Función genérica */
CREATE OR REPLACE FUNCTION notify_sse_change()
RETURNS trigger AS $$ RETURNS trigger AS $$
DECLARE DECLARE
payload TEXT; payload TEXT;
@@ -11,7 +15,9 @@ BEGIN
'old', row_to_json(OLD), 'old', row_to_json(OLD),
'new', row_to_json(NEW) 'new', row_to_json(NEW)
)::text; )::text;
PERFORM pg_notify('sse_events', payload); PERFORM pg_notify('sse_events', payload);
IF TG_OP = 'DELETE' THEN IF TG_OP = 'DELETE' THEN
RETURN OLD; RETURN OLD;
ELSE ELSE
@@ -20,23 +26,33 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
/* 2⃣ Drop de triggers viejos (normal o constraint) */
DROP TRIGGER IF EXISTS planilla_notify_trigger ON "Planilla";
DROP TRIGGER IF EXISTS cliente_notify_trigger ON "Cliente"; DROP TRIGGER IF EXISTS cliente_notify_trigger ON "Cliente";
CREATE TRIGGER cliente_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "Cliente"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_generic_change();
DROP TRIGGER IF EXISTS tarea_notify_trigger ON "TareaRealizada"; DROP TRIGGER IF EXISTS tarea_notify_trigger ON "TareaRealizada";
CREATE TRIGGER tarea_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "TareaRealizada"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_generic_change();
DROP TRIGGER IF EXISTS asistencia_notify_trigger ON "Asistencia"; DROP TRIGGER IF EXISTS asistencia_notify_trigger ON "Asistencia";
CREATE TRIGGER asistencia_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "Asistencia" /* 3⃣ Constraint triggers DEFERRABLE */
DEFERRABLE INITIALLY DEFERRED CREATE CONSTRAINT TRIGGER planilla_notify_trigger
FOR EACH ROW AFTER INSERT OR UPDATE OR DELETE ON "Planilla"
EXECUTE PROCEDURE notify_generic_change(); DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_sse_change();
CREATE CONSTRAINT TRIGGER cliente_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "Cliente"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_sse_change();
CREATE CONSTRAINT TRIGGER tarea_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "TareaRealizada"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_sse_change();
CREATE CONSTRAINT TRIGGER asistencia_notify_trigger
AFTER INSERT OR UPDATE OR DELETE ON "Asistencia"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE notify_sse_change();

View File

@@ -31,7 +31,8 @@ services:
container_name: planilla-ui container_name: planilla-ui
image: gitea.interno.com/nucleo000/planilla-ui:latest image: gitea.interno.com/nucleo000/planilla-ui:latest
build: ./ui build: ./ui
restart: unless-stopped environment:
VITE_API_EVENTS_URL: http://planilla-api:4000/events
ports: ports:
- "3008:80" - "3008:80"
networks: [planilla, principal] networks: [planilla, principal]

View File

@@ -1,10 +1,21 @@
# planilla/ui/Dockerfile # Etapa de build de Vite
FROM node:18-alpine AS build FROM node:18-alpine AS build
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY . . COPY . .
RUN npm run build RUN npm run build
# Etapa final con Nginx
FROM nginx:alpine FROM nginx:alpine
# Copia la app compilada
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
# Copia script que genera config.js en base a variables de entorno
COPY runtime-env.sh /docker-entrypoint.d/
# Copia configuración de Nginx para asegurar que sirva correctamente la app
COPY nginx.conf /etc/nginx/conf.d/default.conf

4
ui/runtime-env.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
echo "window.RUNTIME_CONFIG = {" > /usr/share/nginx/html/config.js
echo " VITE_API_EVENTS_URL: '${VITE_API_EVENTS_URL}'" >> /usr/share/nginx/html/config.js
echo "};" >> /usr/share/nginx/html/config.js

View File

@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
// forzar subida // forzar subida
const baseURL = 'https://planilla.interno.com' const baseURL = 'http://localhost:4000';
// const baseURL = import.meta.env.API_BASE_URL || 'https://planilla.interno.com' // const baseURL = import.meta.env.API_BASE_URL || 'https://planilla.interno.com'
console.log(baseURL); console.log(baseURL);

View File

@@ -76,11 +76,30 @@ export const useEmpleadosStore = defineStore('empleados', {
await this.fetchEmpleados(); await this.fetchEmpleados();
} catch (error) { } catch (error) {
console.error(`Error eliminando empleado ${id}:`, error); console.error(`Error eliminando empleado ${id}:`, error);
const prismaCode = error?.response?.data?.code;
const constraint = error?.response?.data?.meta?.constraint;
if (prismaCode === 'P2003') {
if (constraint === 'Planilla_empleado_id_fkey') {
throw 'No se puede eliminar: el empleado tiene planillas asociadas.';
}
if (constraint === 'Asistencia_empleado_id_fkey') {
throw 'No se puede eliminar: el empleado tiene asistencias registradas.';
}
if (constraint === 'TareaRealizada_empleado_id_fkey') {
throw 'No se puede eliminar: el empleado tiene tareas registradas.';
}
// Si no sabemos qué constraint es
throw 'No se puede eliminar: el empleado está referenciado en otros datos.';
}
const errorMsg = error.response?.data?.message || error.message || 'Ocurrió un error.'; const errorMsg = error.response?.data?.message || error.message || 'Ocurrió un error.';
throw errorMsg; throw errorMsg;
} }
}, },
clearCurrentEmpleado() { clearCurrentEmpleado() {
this.currentEmpleado = getDefaultEmpleado(); this.currentEmpleado = getDefaultEmpleado();
}, },

View File

@@ -10,32 +10,40 @@ export const useRealtimeStore = defineStore('realtime', {
}), }),
actions: { actions: {
init() { init() {
if (this._sse) return if (this._sse) return;
this._sse = new EventSource('/events')
const eventosURL = import.meta.env.VITE_API_EVENTS_URL || '/events';
this._sse = new EventSource(eventosURL);
this._sse.onopen = () => {
console.log('🟢 Conexión SSE establecida correctamente');
};
this._sse.onmessage = (e) => { this._sse.onmessage = (e) => {
try { try {
const payload = JSON.parse(e.data) const payload = JSON.parse(e.data);
switch (payload.table) { switch (payload.table) {
case 'Planilla': case 'Planilla':
usePlanillasStore().fetchPlanillas() usePlanillasStore().fetchPlanillas();
break break;
case 'Cliente': case 'Cliente':
useEmpleadosStore().fetchEmpleados() useEmpleadosStore().fetchEmpleados();
break break;
case 'TareaRealizada': case 'TareaRealizada':
useTareasStore().fetchTareas() useTareasStore().fetchTareas();
break break;
case 'Asistencia': case 'Asistencia':
useAsistenciasStore().fetchAsistencias() useAsistenciasStore().fetchAsistencias();
break break;
} }
} catch (err) { } catch (err) {
console.error('Error procesando SSE', err) console.error('Error procesando SSE', err);
}
} }
};
this._sse.onerror = () => { this._sse.onerror = () => {
console.warn('SSE connection lost, reloading...') console.warn('SSE connection lost, reloading...');
};
} }
}, },
},
}) })