From 74b9c0ad7ec02f87073021cae2b703d0a00a036d Mon Sep 17 00:00:00 2001 From: josedario87 <71241187+josedario87@users.noreply.github.com> Date: Wed, 11 Jun 2025 04:12:02 -0600 Subject: [PATCH 1/4] feat(ui): add snackbar for realtime notifications --- ui/src/App.vue | 2 + ui/src/components/ui/SnackbarContainer.vue | 67 ++++++++++++++++++++++ ui/src/stores/useRealtime.js | 6 ++ ui/src/stores/useSnackbar.js | 22 +++++++ 4 files changed, 97 insertions(+) create mode 100644 ui/src/components/ui/SnackbarContainer.vue create mode 100644 ui/src/stores/useSnackbar.js diff --git a/ui/src/App.vue b/ui/src/App.vue index dd4e2d8..56c4abf 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -2,6 +2,7 @@ import { watchEffect, computed } from 'vue' // Added computed import TopBar from '@/components/ui/TopBar.vue' import NavBar from '@/components/ui/NavBar.vue' +import SnackbarContainer from '@/components/ui/SnackbarContainer.vue' import { useUi } from '@/stores/useUi' const ui = useUi() @@ -80,6 +81,7 @@ const transitionDurationStyle = computed(() => { + diff --git a/ui/src/components/ui/SnackbarContainer.vue b/ui/src/components/ui/SnackbarContainer.vue new file mode 100644 index 0000000..8d6e676 --- /dev/null +++ b/ui/src/components/ui/SnackbarContainer.vue @@ -0,0 +1,67 @@ + + + + + {{ snack.text }} + + + + + + + + diff --git a/ui/src/stores/useRealtime.js b/ui/src/stores/useRealtime.js index cf34662..09c444e 100644 --- a/ui/src/stores/useRealtime.js +++ b/ui/src/stores/useRealtime.js @@ -3,6 +3,7 @@ import { usePlanillasStore } from './usePlanillas' import { useEmpleadosStore } from './useEmpleados' import { useTareasStore } from './useTareas' import { useAsistenciasStore } from './useAsistencias' +import { useSnackbarStore } from './useSnackbar' export const useRealtimeStore = defineStore('realtime', { state: () => ({ @@ -55,6 +56,11 @@ export const useRealtimeStore = defineStore('realtime', { addEvent(); } + const snackbar = useSnackbarStore(); + const idPart = payload.new?.id || payload.old?.id; + const message = `${payload.operation} ${payload.table}${idPart ? ' #' + idPart : ''}`; + snackbar.push({ text: message }); + // mark badge for module and operation if (this.badges[payload.table]) { this.badges[payload.table][payload.operation] = true; diff --git a/ui/src/stores/useSnackbar.js b/ui/src/stores/useSnackbar.js new file mode 100644 index 0000000..7b01c32 --- /dev/null +++ b/ui/src/stores/useSnackbar.js @@ -0,0 +1,22 @@ +import { defineStore } from 'pinia' + +export const useSnackbarStore = defineStore('snackbar', { + state: () => ({ + snacks: [] + }), + actions: { + push({ text, type = 'info', duration = 4000 }) { + const id = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2) + const snack = { id, text, type } + this.snacks.push(snack) + if (duration > 0) { + setTimeout(() => this.remove(id), duration) + } + return id + }, + remove(id) { + const index = this.snacks.findIndex(s => s.id === id) + if (index !== -1) this.snacks.splice(index, 1) + } + } +}) From aa480e8faabb3fa88787015fcb8acce0f508a12d Mon Sep 17 00:00:00 2001 From: josedario87 <71241187+josedario87@users.noreply.github.com> Date: Wed, 11 Jun 2025 04:25:40 -0600 Subject: [PATCH 2/4] Improve snackbar style and duration --- ui/src/components/ui/SnackbarContainer.vue | 53 ++++++++-------------- ui/src/stores/useRealtime.js | 4 +- ui/src/stores/useSnackbar.js | 8 +++- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/ui/src/components/ui/SnackbarContainer.vue b/ui/src/components/ui/SnackbarContainer.vue index 8d6e676..0ba7fac 100644 --- a/ui/src/components/ui/SnackbarContainer.vue +++ b/ui/src/components/ui/SnackbarContainer.vue @@ -1,12 +1,13 @@ - + - {{ snack.text }} + {{ snack.text }} + × @@ -15,10 +16,22 @@ diff --git a/ui/src/stores/useRealtime.js b/ui/src/stores/useRealtime.js index 09c444e..6e8a312 100644 --- a/ui/src/stores/useRealtime.js +++ b/ui/src/stores/useRealtime.js @@ -59,7 +59,9 @@ export const useRealtimeStore = defineStore('realtime', { const snackbar = useSnackbarStore(); const idPart = payload.new?.id || payload.old?.id; const message = `${payload.operation} ${payload.table}${idPart ? ' #' + idPart : ''}`; - snackbar.push({ text: message }); + const type = payload.operation === 'DELETE' ? 'error' : + payload.operation === 'INSERT' ? 'success' : 'info'; + snackbar.push({ text: message, type }); // mark badge for module and operation if (this.badges[payload.table]) { diff --git a/ui/src/stores/useSnackbar.js b/ui/src/stores/useSnackbar.js index 7b01c32..0898b06 100644 --- a/ui/src/stores/useSnackbar.js +++ b/ui/src/stores/useSnackbar.js @@ -2,12 +2,16 @@ import { defineStore } from 'pinia' export const useSnackbarStore = defineStore('snackbar', { state: () => ({ - snacks: [] + snacks: [], + maxSnacks: 5 }), actions: { - push({ text, type = 'info', duration = 4000 }) { + push({ text, type = 'info', duration = 6000 }) { const id = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2) const snack = { id, text, type } + if (this.snacks.length >= this.maxSnacks) { + this.snacks.shift() + } this.snacks.push(snack) if (duration > 0) { setTimeout(() => this.remove(id), duration) From 52ccf50eaae7b02ba1f30388c46fd451bb9daf12 Mon Sep 17 00:00:00 2001 From: josedario87 <71241187+josedario87@users.noreply.github.com> Date: Wed, 11 Jun 2025 04:37:57 -0600 Subject: [PATCH 3/4] Add module-aware snackbar notifications --- ui/src/components/ui/NavBar.vue | 9 +++++---- ui/src/components/ui/SnackbarContainer.vue | 8 +++++++- ui/src/constants/moduleInfo.js | 6 ++++++ ui/src/stores/useRealtime.js | 9 ++++++++- ui/src/stores/useSnackbar.js | 4 ++-- 5 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 ui/src/constants/moduleInfo.js diff --git a/ui/src/components/ui/NavBar.vue b/ui/src/components/ui/NavBar.vue index 464b314..2ca7806 100644 --- a/ui/src/components/ui/NavBar.vue +++ b/ui/src/components/ui/NavBar.vue @@ -3,6 +3,7 @@ import { ref, watch, computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { useUi } from '@/stores/useUi' import { useRealtimeStore } from '@/stores/useRealtime' +import { moduleInfo } from '@/constants/moduleInfo' const ui = useUi() const realtime = useRealtimeStore() @@ -10,10 +11,10 @@ const realtime = useRealtimeStore() // enlaces de la app const links = [ { to: '/', label: 'Chat', icon: '💬' }, - { to: '/empleados', label: 'Empleados', icon: '👥' }, - { to: '/tareas', label: 'Tareas', icon: '📋' }, - { to: '/planillas', label: 'Planillas', icon: '📂' }, - { to: '/asistencias', label: 'Asistencias', icon: '⏰' }, + { to: '/empleados', label: 'Empleados', icon: moduleInfo.Cliente.icon }, + { to: '/tareas', label: 'Tareas', icon: moduleInfo.TareaRealizada.icon }, + { to: '/planillas', label: 'Planillas', icon: moduleInfo.Planilla.icon }, + { to: '/asistencias', label: 'Asistencias', icon: moduleInfo.Asistencia.icon }, { to: '/feed', label: 'Feed', icon: '📰' }, { to: '/config', label: 'Config', icon: '⚙️' }, ] diff --git a/ui/src/components/ui/SnackbarContainer.vue b/ui/src/components/ui/SnackbarContainer.vue index 0ba7fac..717abb5 100644 --- a/ui/src/components/ui/SnackbarContainer.vue +++ b/ui/src/components/ui/SnackbarContainer.vue @@ -6,7 +6,13 @@ :key="snack.id" :class="['snackbar-item', typeClass(snack.type)]" > - {{ snack.text }} + + + {{ snack.icon }} + {{ snack.header }} + + {{ snack.text }} + × diff --git a/ui/src/constants/moduleInfo.js b/ui/src/constants/moduleInfo.js new file mode 100644 index 0000000..5d8aa30 --- /dev/null +++ b/ui/src/constants/moduleInfo.js @@ -0,0 +1,6 @@ +export const moduleInfo = { + Cliente: { label: 'Empleados', icon: '👥' }, + TareaRealizada: { label: 'Tareas', icon: '📋' }, + Planilla: { label: 'Planillas', icon: '📂' }, + Asistencia: { label: 'Asistencias', icon: '⏰' } +} diff --git a/ui/src/stores/useRealtime.js b/ui/src/stores/useRealtime.js index 6e8a312..9992412 100644 --- a/ui/src/stores/useRealtime.js +++ b/ui/src/stores/useRealtime.js @@ -4,6 +4,7 @@ import { useEmpleadosStore } from './useEmpleados' import { useTareasStore } from './useTareas' import { useAsistenciasStore } from './useAsistencias' import { useSnackbarStore } from './useSnackbar' +import { moduleInfo } from '@/constants/moduleInfo' export const useRealtimeStore = defineStore('realtime', { state: () => ({ @@ -61,7 +62,13 @@ export const useRealtimeStore = defineStore('realtime', { const message = `${payload.operation} ${payload.table}${idPart ? ' #' + idPart : ''}`; const type = payload.operation === 'DELETE' ? 'error' : payload.operation === 'INSERT' ? 'success' : 'info'; - snackbar.push({ text: message, type }); + const info = moduleInfo[payload.table] || {}; + snackbar.push({ + text: message, + type, + icon: info.icon, + header: info.label + }); // mark badge for module and operation if (this.badges[payload.table]) { diff --git a/ui/src/stores/useSnackbar.js b/ui/src/stores/useSnackbar.js index 0898b06..1689eb6 100644 --- a/ui/src/stores/useSnackbar.js +++ b/ui/src/stores/useSnackbar.js @@ -6,9 +6,9 @@ export const useSnackbarStore = defineStore('snackbar', { maxSnacks: 5 }), actions: { - push({ text, type = 'info', duration = 6000 }) { + push({ text, type = 'info', icon = '', header = '', duration = 6000 }) { const id = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2) - const snack = { id, text, type } + const snack = { id, text, type, icon, header } if (this.snacks.length >= this.maxSnacks) { this.snacks.shift() } From 036bd2ef3d1cad46f9f6c83082deb5dc97f19708 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Wed, 11 Jun 2025 04:38:29 -0600 Subject: [PATCH 4/4] arreglado estilo del container hecho con tailwind cambiado a css moderno --- ui/src/components/ui/SnackbarContainer.vue | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/ui/src/components/ui/SnackbarContainer.vue b/ui/src/components/ui/SnackbarContainer.vue index 0ba7fac..bbda834 100644 --- a/ui/src/components/ui/SnackbarContainer.vue +++ b/ui/src/components/ui/SnackbarContainer.vue @@ -39,9 +39,29 @@ const snackbar = useSnackbarStore() z-index: 1000; } -.snackbar-item { - @apply pointer-events-auto text-white px-4 py-3 rounded-md shadow-lg flex items-start; +.snackbar-item{ + /* permite hacer clics dentro del snackbar */ + pointer-events:auto; + + /* texto blanco */ + color:#fff; + + /* px-4 py-3 → 0.75 rem arriba/abajo, 1 rem lados */ + padding:0.75rem 1rem; + + /* rounded-md ≈ 6 px */ + border-radius:0.375rem; + + /* shadow-lg de Tailwind llevado a CSS puro */ + box-shadow: + 0 10px 15px -3px rgba(0,0,0,.10), + 0 4px 6px -2px rgba(0,0,0,.05); + + /* flex items-start */ + display:flex; + align-items:flex-start; } + .snackbar-enter-active, .snackbar-leave-active { transition: opacity 0.35s, transform 0.35s;