Merge pull request #43 from josedario87/codex/add-event-status-dots-to-navbar-and-cards
Add realtime event indicators
This commit is contained in:
@@ -11,7 +11,11 @@
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h2 class="text-xl font-semibold text-gray-800" :style="{ color: ui.accentColorAsistencias }">Asistencia ID: {{ asistencia.id }}</h2>
|
||||
<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, '-');
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
:style="{ borderColor: ui.accentColorEmpleados }"
|
||||
/>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: ui.accentColorEmpleados }">{{ employee.name }}</h2>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorPlanillas }">Planilla ID: {{ planilla.id }}</h2>
|
||||
<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);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorTareas }">Tarea ID: {{ tarea.id }}</h2>
|
||||
<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, '-');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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;}
|
||||
|
||||
Reference in New Issue
Block a user