Add SSE triggers for all modules and central listener
This commit is contained in:
@@ -135,8 +135,9 @@ docker compose down --remove-orphans
|
||||
### Notificaciones en tiempo real (SSE)
|
||||
|
||||
El backend expone un endpoint `/events` que utiliza **Server-Sent Events**. La
|
||||
base de datos PostgreSQL emite mensajes mediante triggers y el servidor los
|
||||
reenvía a los navegadores conectados.
|
||||
base de datos PostgreSQL emite mensajes mediante triggers en las tablas
|
||||
`Planilla`, `Cliente`, `TareaRealizada` y `Asistencia`. Cada cambio se
|
||||
reenvía automáticamente a los navegadores conectados.
|
||||
|
||||
```javascript
|
||||
const source = new EventSource('http://localhost:4000/events');
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Triggers for other tables to notify SSE channel
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_generic_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 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();
|
||||
|
||||
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();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import { usePlanillasStore } from './stores/usePlanillas'
|
||||
import { useRealtimeStore } from './stores/useRealtime'
|
||||
|
||||
import App from './App.vue'
|
||||
import './style.css' // Tailwind o tus estilos globales
|
||||
@@ -16,7 +16,7 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
// iniciar suscripción SSE cuando la app monte
|
||||
const store = usePlanillasStore(pinia)
|
||||
store.subscribeToEvents()
|
||||
const realtime = useRealtimeStore(pinia)
|
||||
realtime.init()
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -17,7 +17,6 @@ export const usePlanillasStore = defineStore('planillas', {
|
||||
state: () => ({
|
||||
planillas: [],
|
||||
currentPlanilla: getDefaultCurrentPlanilla(),
|
||||
_sse: null,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -83,23 +82,5 @@ export const usePlanillasStore = defineStore('planillas', {
|
||||
clearCurrentPlanilla() {
|
||||
this.currentPlanilla = getDefaultCurrentPlanilla()
|
||||
},
|
||||
|
||||
subscribeToEvents() {
|
||||
if (this._sse) return
|
||||
this._sse = new EventSource('/events')
|
||||
this._sse.onmessage = (e) => {
|
||||
try {
|
||||
const payload = JSON.parse(e.data)
|
||||
if (payload.table === 'Planilla') {
|
||||
this.fetchPlanillas()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error procesando SSE', err)
|
||||
}
|
||||
}
|
||||
this._sse.onerror = () => {
|
||||
console.warn('SSE connection lost, reloading...')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
41
ui/src/stores/useRealtime.js
Normal file
41
ui/src/stores/useRealtime.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { usePlanillasStore } from './usePlanillas'
|
||||
import { useEmpleadosStore } from './useEmpleados'
|
||||
import { useTareasStore } from './useTareas'
|
||||
import { useAsistenciasStore } from './useAsistencias'
|
||||
|
||||
export const useRealtimeStore = defineStore('realtime', {
|
||||
state: () => ({
|
||||
_sse: null,
|
||||
}),
|
||||
actions: {
|
||||
init() {
|
||||
if (this._sse) return
|
||||
this._sse = new EventSource('/events')
|
||||
this._sse.onmessage = (e) => {
|
||||
try {
|
||||
const payload = JSON.parse(e.data)
|
||||
switch (payload.table) {
|
||||
case 'Planilla':
|
||||
usePlanillasStore().fetchPlanillas()
|
||||
break
|
||||
case 'Cliente':
|
||||
useEmpleadosStore().fetchEmpleados()
|
||||
break
|
||||
case 'TareaRealizada':
|
||||
useTareasStore().fetchTareas()
|
||||
break
|
||||
case 'Asistencia':
|
||||
useAsistenciasStore().fetchAsistencias()
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error procesando SSE', err)
|
||||
}
|
||||
}
|
||||
this._sse.onerror = () => {
|
||||
console.warn('SSE connection lost, reloading...')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user