feat(ui): show realtime event dots

This commit is contained in:
josedario87
2025-06-09 21:40:05 -06:00
parent a6cc91af3d
commit 5e83f10784
7 changed files with 109 additions and 8 deletions

View File

@@ -11,7 +11,11 @@
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<h2 class="text-xl font-semibold text-gray-800" :style="{ color: ui.accentColorAsistencias }">Asistencia ID: {{ asistencia.id }}</h2>
<span v-if="lastOp === 'INSERT'" class="realtime-dot dot-insert"></span>
<span v-if="lastOp === 'UPDATE'" class="realtime-dot dot-update"></span>
</div>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(asistencia.estado)]">
{{ asistencia.estado || 'N/A' }}
</span>
@@ -24,9 +28,11 @@
import { defineProps, defineEmits, computed } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { useUi } from '../../stores/useUi.js';
import { useRealtimeStore } from '../../stores/useRealtime';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
const realtime = useRealtimeStore();
const props = defineProps({
asistencia: {
@@ -74,6 +80,10 @@ const deleteAsistenciaInternal = async () => {
}
};
const lastOp = computed(() =>
realtime.getLatestOperation('Asistencia', props.asistencia.id)
);
const getStatusClass = (status) => {
if (!status) return 'bg-gray-400 text-white'; // Default for N/A, ensuring text is visible
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');

View File

@@ -16,7 +16,11 @@
:style="{ borderColor: ui.accentColorEmpleados }"
/>
<div class="flex-grow">
<div class="flex items-center gap-2">
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: ui.accentColorEmpleados }">{{ employee.name }}</h2>
<span v-if="lastOp === 'INSERT'" class="realtime-dot dot-insert"></span>
<span v-if="lastOp === 'UPDATE'" class="realtime-dot dot-update"></span>
</div>
<p class="text-xs text-gray-500">ID: {{ employee.id }}</p>
</div>
</div>
@@ -46,14 +50,16 @@
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'; // Removed PropType, useRouter as they are not used
import { defineProps, defineEmits, computed } from 'vue';
import { useUi } from '../../stores/useUi.js';
import { useEmpleadosStore } from '../../stores/useEmpleados'; // Added import
import { useRealtimeStore } from '../../stores/useRealtime';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
const emit = defineEmits(['edit']);
const empleadosStore = useEmpleadosStore();
const realtime = useRealtimeStore();
const props = defineProps({
employee: {
@@ -81,6 +87,10 @@ const deleteEmpleado = async () => {
alert('Ocurrió un error al eliminar el empleado.');
}
};
const lastOp = computed(() =>
realtime.getLatestOperation('Cliente', props.employee.id)
);
// buttonHover function removed as it's no longer used
</script>

View File

@@ -10,7 +10,11 @@
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorPlanillas }">Planilla ID: {{ planilla.id }}</h2>
<span v-if="lastOp === 'INSERT'" class="realtime-dot dot-insert"></span>
<span v-if="lastOp === 'UPDATE'" class="realtime-dot dot-update"></span>
</div>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(planilla.estado)]">
{{ planilla.estado || 'N/A' }}
</span>
@@ -23,9 +27,11 @@
import { defineProps, defineEmits, computed } from 'vue';
import { usePlanillasStore } from '../../stores/usePlanillas';
import { useUi } from '../../stores/useUi.js';
import { useRealtimeStore } from '../../stores/useRealtime';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
const realtime = useRealtimeStore();
const props = defineProps({
planilla: {
@@ -62,6 +68,10 @@ const cardFields = computed(() => [
{ label: 'Total', value: formatCurrency(props.planilla.total) },
]);
const lastOp = computed(() =>
realtime.getLatestOperation('Planilla', props.planilla.id)
);
const editPlanilla = () => {
emit('edit', props.planilla.id);
};

View File

@@ -11,7 +11,11 @@
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorTareas }">Tarea ID: {{ tarea.id }}</h2>
<span v-if="lastOp === 'INSERT'" class="realtime-dot dot-insert"></span>
<span v-if="lastOp === 'UPDATE'" class="realtime-dot dot-update"></span>
</div>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(tarea.estado)]">
{{ tarea.estado || 'N/A' }}
</span>
@@ -24,9 +28,11 @@
import { defineProps, defineEmits, computed } from 'vue';
import { useTareasStore } from '../../stores/useTareas';
import { useUi } from '../../stores/useUi.js';
import { useRealtimeStore } from '../../stores/useRealtime';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
const realtime = useRealtimeStore();
const props = defineProps({
tarea: {
@@ -87,6 +93,10 @@ const deleteTareaInternal = async () => {
}
};
const lastOp = computed(() =>
realtime.getLatestOperation('TareaRealizada', props.tarea.id)
);
const getStatusClass = (status) => {
if (!status) return 'bg-gray-400 text-white';
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');

View File

@@ -1,9 +1,11 @@
<script setup>
import { ref, watch, computed } from 'vue'
import { ref, watch, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useUi } from '@/stores/useUi'
import { useRealtimeStore } from '@/stores/useRealtime'
const ui = useUi()
const realtime = useRealtimeStore()
// enlaces de la app
const links = [
@@ -18,7 +20,16 @@ const links = [
const route = useRoute()
const activePath = ref(route.path)
watch(route, v => (activePath.value = v.path))
watch(route, v => {
activePath.value = v.path
const table = tableForPath(v.path)
if (table) realtime.clearBadgesForTable(table)
})
onMounted(() => {
const table = tableForPath(activePath.value)
if (table) realtime.clearBadgesForTable(table)
})
const accentColorForPath = (path) => {
if (path.startsWith('/empleados')) return ui.accentColorEmpleados
@@ -30,6 +41,19 @@ const accentColorForPath = (path) => {
return ui.accentColorChat
}
const tableForPath = (path) => {
if (path.startsWith('/empleados')) return 'Cliente'
if (path.startsWith('/tareas')) return 'TareaRealizada'
if (path.startsWith('/planillas')) return 'Planilla'
if (path.startsWith('/asistencias')) return 'Asistencia'
return null
}
const hasBadge = (path, op) => {
const table = tableForPath(path)
return table && realtime.badges[table]?.[op]
}
// clases dinámicas p/ mostrar / ocultar barra
const sidebarClasses = computed(() => ui.sidebarOpen ? 'translate-x-0' : '-translate-x-full')
@@ -74,7 +98,12 @@ const handleLinkClick = () => {
@click="handleLinkClick"
>
<span class="text-lg" aria-hidden="true">{{ l.icon }}</span>
<span class="truncate">{{ l.label }}</span>
<span class="truncate flex-1">{{ l.label }}</span>
<span class="flex gap-1">
<span v-if="hasBadge(l.to, 'INSERT')" class="realtime-dot dot-insert"></span>
<span v-if="hasBadge(l.to, 'UPDATE')" class="realtime-dot dot-update"></span>
<span v-if="hasBadge(l.to, 'DELETE')" class="realtime-dot dot-delete"></span>
</span>
</RouterLink>
</li>
</ul>

View File

@@ -8,6 +8,12 @@ export const useRealtimeStore = defineStore('realtime', {
state: () => ({
_sse: null,
events: [],
badges: {
Planilla: { INSERT: false, UPDATE: false, DELETE: false },
Cliente: { INSERT: false, UPDATE: false, DELETE: false },
TareaRealizada: { INSERT: false, UPDATE: false, DELETE: false },
Asistencia: { INSERT: false, UPDATE: false, DELETE: false },
},
}),
actions: {
init() {
@@ -31,6 +37,11 @@ export const useRealtimeStore = defineStore('realtime', {
// store event for feed
this.events.unshift({ ...payload, receivedAt: new Date().toISOString() });
// mark badge for module and operation
if (this.badges[payload.table]) {
this.badges[payload.table][payload.operation] = true;
}
switch (payload.table) {
case 'Planilla':
usePlanillasStore().fetchPlanillas();
@@ -54,5 +65,21 @@ export const useRealtimeStore = defineStore('realtime', {
console.warn('SSE connection lost, reloading...');
};
}
clearBadgesForTable(table) {
if (this.badges[table]) {
this.badges[table].INSERT = false;
this.badges[table].UPDATE = false;
this.badges[table].DELETE = false;
}
},
getLatestOperation(table, id) {
const ev = this.events.find(
(e) =>
e.table === table &&
((e.new && e.new.id === id) || (e.old && e.old.id === id))
);
return ev ? ev.operation : null;
},
},
})

View File

@@ -86,3 +86,8 @@ body {
background-color: var(--primary-color);
font-family: var(--font-family);
}
.realtime-dot{width:0.6rem;height:0.6rem;border-radius:50%;display:inline-block;}
.dot-insert{background-color:#22c55e;}
.dot-update{background-color:#a855f7;}
.dot-delete{background-color:#ef4444;}