feat(ui): add realtime feed view
This commit is contained in:
60
ui/src/components/realtime/RealtimeEventCard.vue
Normal file
60
ui/src/components/realtime/RealtimeEventCard.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="realtime-event">
|
||||
<p class="text-xs text-gray-500 mb-1">
|
||||
{{ event.operation }} · {{ event.table }} • {{ formatTimestamp(event.receivedAt) }}
|
||||
</p>
|
||||
<component v-if="component" :is="component" v-bind="componentProps" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import CardPlanilla from '@/components/planillas/cardPlanilla.vue'
|
||||
import CardEmpleado from '@/components/empleados/cardEmpleado.vue'
|
||||
import CardTarea from '@/components/tareas/cardTarea.vue'
|
||||
import CardAsistencia from '@/components/asistencias/cardAsistencia.vue'
|
||||
|
||||
const props = defineProps({
|
||||
event: { type: Object, required: true }
|
||||
})
|
||||
|
||||
const componentMap = {
|
||||
Planilla: CardPlanilla,
|
||||
Cliente: CardEmpleado,
|
||||
TareaRealizada: CardTarea,
|
||||
Asistencia: CardAsistencia,
|
||||
}
|
||||
|
||||
const component = computed(() => componentMap[props.event.table] || null)
|
||||
|
||||
const itemData = computed(() =>
|
||||
props.event.operation === 'DELETE' ? props.event.old : props.event.new
|
||||
)
|
||||
|
||||
const componentProps = computed(() => {
|
||||
const data = itemData.value || {}
|
||||
switch (props.event.table) {
|
||||
case 'Planilla':
|
||||
return { planilla: data }
|
||||
case 'Cliente':
|
||||
return { employee: data }
|
||||
case 'TareaRealizada':
|
||||
return { tarea: data }
|
||||
case 'Asistencia':
|
||||
return { asistencia: data }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
const formatTimestamp = (ts) => {
|
||||
if (!ts) return ''
|
||||
return new Date(ts).toLocaleString('es-HN', { hour12: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.realtime-event {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ const links = [
|
||||
{ to: '/tareas', label: 'Tareas', icon: '📋' },
|
||||
{ to: '/planillas', label: 'Planillas', icon: '📂' },
|
||||
{ to: '/asistencias', label: 'Asistencias', icon: '⏰' },
|
||||
{ to: '/feed', label: 'Feed', icon: '📰' },
|
||||
{ to: '/config', label: 'Config', icon: '⚙️' },
|
||||
]
|
||||
|
||||
@@ -24,6 +25,7 @@ const accentColorForPath = (path) => {
|
||||
if (path.startsWith('/tareas')) return ui.accentColorTareas
|
||||
if (path.startsWith('/planillas')) return ui.accentColorPlanillas
|
||||
if (path.startsWith('/asistencias')) return ui.accentColorAsistencias
|
||||
if (path.startsWith('/feed')) return ui.primaryColor
|
||||
if (path.startsWith('/config')) return ui.accentColorConfiguracion
|
||||
return ui.accentColorChat
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ const routes = [
|
||||
{ path: '/asistencias/nuevo', name: 'asistencias-new', component: () => import('@/views/asistencias/AsistenciaForm.vue') },
|
||||
{ path: '/asistencias/:id', name: 'asistencias-edit', component: () => import('@/views/asistencias/AsistenciaForm.vue'), props: true },
|
||||
|
||||
// ────── Feed en tiempo real ──────
|
||||
{ path: '/feed', name: 'feed', component: () => import('@/views/RealtimeFeedView.vue') },
|
||||
|
||||
// 404
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAsistenciasStore } from './useAsistencias'
|
||||
export const useRealtimeStore = defineStore('realtime', {
|
||||
state: () => ({
|
||||
_sse: null,
|
||||
events: [],
|
||||
}),
|
||||
actions: {
|
||||
init() {
|
||||
@@ -19,13 +20,16 @@ export const useRealtimeStore = defineStore('realtime', {
|
||||
console.log('🟢 Conexión SSE establecida correctamente');
|
||||
};
|
||||
|
||||
this._sse.onmessage = (e) => {
|
||||
console.log('📩 SSE message raw:', e.data);
|
||||
this._sse.onmessage = (e) => {
|
||||
console.log('📩 SSE message raw:', e.data);
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
console.log('📦 Payload parseado:', payload);
|
||||
console.log('🧪 Tabla recibida:', payload.table);
|
||||
try {
|
||||
const payload = JSON.parse(e.data);
|
||||
console.log('📦 Payload parseado:', payload);
|
||||
console.log('🧪 Tabla recibida:', payload.table);
|
||||
|
||||
// store event for feed
|
||||
this.events.unshift({ ...payload, receivedAt: new Date().toISOString() });
|
||||
|
||||
switch (payload.table) {
|
||||
case 'Planilla':
|
||||
|
||||
53
ui/src/views/RealtimeFeedView.vue
Normal file
53
ui/src/views/RealtimeFeedView.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useRealtimeStore } from '@/stores/useRealtime'
|
||||
import RealtimeEventCard from '@/components/realtime/RealtimeEventCard.vue'
|
||||
|
||||
const realtime = useRealtimeStore()
|
||||
|
||||
onMounted(() => {
|
||||
realtime.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="realtime-feed-container">
|
||||
<header class="page-header">
|
||||
<h1>Feed en Tiempo Real</h1>
|
||||
</header>
|
||||
|
||||
<div class="feed-list overflow-auto pr-2" style="max-height: calc(100vh - 160px);">
|
||||
<RealtimeEventCard
|
||||
v-for="(ev, idx) in realtime.events"
|
||||
:key="idx"
|
||||
:event="ev"
|
||||
/>
|
||||
<div v-if="realtime.events.length === 0" class="text-center text-gray-500 mt-4">
|
||||
No hay eventos aún.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.realtime-feed-container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--primary-color);
|
||||
font-size: 2.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user