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)
|
### Notificaciones en tiempo real (SSE)
|
||||||
|
|
||||||
El backend expone un endpoint `/events` que utiliza **Server-Sent Events**. La
|
El backend expone un endpoint `/events` que utiliza **Server-Sent Events**. La
|
||||||
base de datos PostgreSQL emite mensajes mediante triggers y el servidor los
|
base de datos PostgreSQL emite mensajes mediante triggers en las tablas
|
||||||
reenvía a los navegadores conectados.
|
`Planilla`, `Cliente`, `TareaRealizada` y `Asistencia`. Cada cambio se
|
||||||
|
reenvía automáticamente a los navegadores conectados.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const source = new EventSource('http://localhost:4000/events');
|
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 { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { usePlanillasStore } from './stores/usePlanillas'
|
import { useRealtimeStore } from './stores/useRealtime'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './style.css' // Tailwind o tus estilos globales
|
import './style.css' // Tailwind o tus estilos globales
|
||||||
@@ -16,7 +16,7 @@ app.use(pinia)
|
|||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
// iniciar suscripción SSE cuando la app monte
|
// iniciar suscripción SSE cuando la app monte
|
||||||
const store = usePlanillasStore(pinia)
|
const realtime = useRealtimeStore(pinia)
|
||||||
store.subscribeToEvents()
|
realtime.init()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const usePlanillasStore = defineStore('planillas', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
planillas: [],
|
planillas: [],
|
||||||
currentPlanilla: getDefaultCurrentPlanilla(),
|
currentPlanilla: getDefaultCurrentPlanilla(),
|
||||||
_sse: null,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -83,23 +82,5 @@ export const usePlanillasStore = defineStore('planillas', {
|
|||||||
clearCurrentPlanilla() {
|
clearCurrentPlanilla() {
|
||||||
this.currentPlanilla = getDefaultCurrentPlanilla()
|
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