From 513c30597184585af2eacc1582d120b57b0f9c53 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Mon, 9 Jun 2025 16:35:36 -0600 Subject: [PATCH] agregada funcionalidad realtime postgress, api y ui. --- Makefile | 9 ++- .../20250609203423_notify_sse/migration.sql | 26 --------- .../migration.sql | 58 ++++++++++++------- docker-compose.yml | 3 +- ui/Dockerfile | 13 ++++- ui/runtime-env.sh | 4 ++ ui/src/apiClient.js | 2 +- ui/src/stores/useEmpleados.js | 39 +++++++++---- ui/src/stores/useRealtime.js | 40 ++++++++----- 9 files changed, 117 insertions(+), 77 deletions(-) delete mode 100644 api/prisma/migrations/20250609203423_notify_sse/migration.sql create mode 100644 ui/runtime-env.sh diff --git a/Makefile b/Makefile index f747f2c..efaa16f 100644 --- a/Makefile +++ b/Makefile @@ -49,4 +49,11 @@ mcp: cd mcp && npm install && npm run dev api: - cd api && npm install && npm run dev \ No newline at end of file + 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 diff --git a/api/prisma/migrations/20250609203423_notify_sse/migration.sql b/api/prisma/migrations/20250609203423_notify_sse/migration.sql deleted file mode 100644 index cb8f6ee..0000000 --- a/api/prisma/migrations/20250609203423_notify_sse/migration.sql +++ /dev/null @@ -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(); diff --git a/api/prisma/migrations/20250609211800_notify_others_sse/migration.sql b/api/prisma/migrations/20250609211800_notify_others_sse/migration.sql index af103a9..9e81f36 100644 --- a/api/prisma/migrations/20250609211800_notify_others_sse/migration.sql +++ b/api/prisma/migrations/20250609211800_notify_others_sse/migration.sql @@ -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 $$ DECLARE payload TEXT; @@ -11,7 +15,9 @@ BEGIN '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 @@ -20,23 +26,33 @@ BEGIN END; $$ LANGUAGE plpgsql; -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"; -CREATE TRIGGER tarea_notify_trigger - AFTER INSERT OR UPDATE OR DELETE ON "TareaRealizada" - DEFERRABLE INITIALLY DEFERRED - FOR EACH ROW - EXECUTE PROCEDURE notify_generic_change(); - +/* 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 tarea_notify_trigger ON "TareaRealizada"; DROP TRIGGER IF EXISTS asistencia_notify_trigger ON "Asistencia"; -CREATE TRIGGER asistencia_notify_trigger - AFTER INSERT OR UPDATE OR DELETE ON "Asistencia" - DEFERRABLE INITIALLY DEFERRED - FOR EACH ROW - EXECUTE PROCEDURE notify_generic_change(); + +/* 3️⃣ Constraint triggers DEFERRABLE */ +CREATE CONSTRAINT TRIGGER planilla_notify_trigger +AFTER INSERT OR UPDATE OR DELETE ON "Planilla" +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(); diff --git a/docker-compose.yml b/docker-compose.yml index e55beec..e1444bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,8 @@ services: container_name: planilla-ui image: gitea.interno.com/nucleo000/planilla-ui:latest build: ./ui - restart: unless-stopped + environment: + VITE_API_EVENTS_URL: http://planilla-api:4000/events ports: - "3008:80" networks: [planilla, principal] diff --git a/ui/Dockerfile b/ui/Dockerfile index deace79..ca07e77 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,10 +1,21 @@ -# planilla/ui/Dockerfile +# Etapa de build de Vite FROM node:18-alpine AS build WORKDIR /app + COPY package*.json ./ RUN npm install + COPY . . RUN npm run build +# Etapa final con Nginx FROM nginx:alpine + +# Copia la app compilada 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 diff --git a/ui/runtime-env.sh b/ui/runtime-env.sh new file mode 100644 index 0000000..11f3440 --- /dev/null +++ b/ui/runtime-env.sh @@ -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 diff --git a/ui/src/apiClient.js b/ui/src/apiClient.js index e40c907..739a105 100644 --- a/ui/src/apiClient.js +++ b/ui/src/apiClient.js @@ -1,7 +1,7 @@ import axios from 'axios'; // 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' console.log(baseURL); diff --git a/ui/src/stores/useEmpleados.js b/ui/src/stores/useEmpleados.js index ab43803..21c9cf6 100644 --- a/ui/src/stores/useEmpleados.js +++ b/ui/src/stores/useEmpleados.js @@ -70,16 +70,35 @@ export const useEmpleadosStore = defineStore('empleados', { } }, - async deleteEmpleado(id) { - try { - await apiClient.delete(`/api/empleados/${id}`); - await this.fetchEmpleados(); - } catch (error) { - console.error(`Error eliminando empleado ${id}:`, error); - const errorMsg = error.response?.data?.message || error.message || 'Ocurrió un error.'; - throw errorMsg; - } - }, + async deleteEmpleado(id) { + try { + await apiClient.delete(`/api/empleados/${id}`); + await this.fetchEmpleados(); + } catch (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.'; + throw errorMsg; + } + }, + clearCurrentEmpleado() { this.currentEmpleado = getDefaultEmpleado(); diff --git a/ui/src/stores/useRealtime.js b/ui/src/stores/useRealtime.js index 5602b80..3922737 100644 --- a/ui/src/stores/useRealtime.js +++ b/ui/src/stores/useRealtime.js @@ -10,32 +10,40 @@ export const useRealtimeStore = defineStore('realtime', { }), actions: { init() { - if (this._sse) return - this._sse = new EventSource('/events') + if (this._sse) return; + + 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) => { try { - const payload = JSON.parse(e.data) + const payload = JSON.parse(e.data); switch (payload.table) { case 'Planilla': - usePlanillasStore().fetchPlanillas() - break + usePlanillasStore().fetchPlanillas(); + break; case 'Cliente': - useEmpleadosStore().fetchEmpleados() - break + useEmpleadosStore().fetchEmpleados(); + break; case 'TareaRealizada': - useTareasStore().fetchTareas() - break + useTareasStore().fetchTareas(); + break; case 'Asistencia': - useAsistenciasStore().fetchAsistencias() - break + useAsistenciasStore().fetchAsistencias(); + break; } } catch (err) { - console.error('Error procesando SSE', err) + console.error('Error procesando SSE', err); } - } + }; + this._sse.onerror = () => { - console.warn('SSE connection lost, reloading...') - } - }, + console.warn('SSE connection lost, reloading...'); + }; + } }, })