Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elegroag/nuxt-credito-caja/llms.txt

Use this file to discover all available pages before exploring further.

Comfaca Créditos en Línea incluye un sistema de notificaciones in-app que mantiene informado al afiliado sobre los cambios de estado de sus solicitudes, la finalización de firmas, documentos requeridos y otras acciones relevantes del proceso crediticio. Las notificaciones se almacenan en base de datos y se consultan desde el cliente mediante polling periódico configurable.

Composable useNotifications

El composable useNotifications gestiona el ciclo de vida completo de las notificaciones en el cliente: carga, actualización del contador de no leídas, marcado como leídas y eliminación.

Estado reactivo

const notifications = ref<Notification[]>([]);
const unreadCount = ref(0);
const loading = ref(false);
const error = ref<string | null>(null);
const pollingEnabled = ref(true);
const pollingInterval = ref(100000); // ~100 segundos

Propiedades computadas

const hasUnread = computed(() => unreadCount.value > 0);
const unreadNotifications = computed(() =>
  notifications.value.filter((n) => !n.read_at)
);
const readNotifications = computed(() =>
  notifications.value.filter((n) => n.read_at)
);

Métodos disponibles

// Carga notificaciones — opcionalmente solo las no leídas
await loadNotifications(onlyUnread?: boolean, limit?: number);

// Actualiza únicamente el contador (más liviano que cargar todas)
await updateUnreadCount();

// Marca una notificación como leída y actualiza el contador localmente
await markAsRead(notificationId: string): Promise<boolean>;

// Marca todas como leídas y pone unreadCount en 0
await markAllAsRead(): Promise<boolean>;

// Elimina una notificación (ajusta unreadCount si estaba sin leer)
await deleteNotification(notificationId: string): Promise<boolean>;

// Control del polling
startPolling();   // Inicia el intervalo automático
stopPolling();    // Detiene el intervalo y limpia el timer
togglePolling();  // Alterna entre activo e inactivo

Polling automático

El composable actualiza el contador de no leídas periódicamente en segundo plano. El polling se detiene automáticamente en onUnmounted:
const startPolling = (): void => {
  if (pollingTimer) {
    clearInterval(pollingTimer);
  }

  pollingEnabled.value = true;
  pollingTimer = setInterval(() => {
    if (pollingEnabled.value) {
      updateUnreadCount();
    }
  }, pollingInterval.value);
};

onUnmounted(() => {
  stopPolling();
});

Formato de Notificaciones

Los helpers de formato están en app/composables/notifications/notificationFormat.ts y se mantienen separados del composable principal para que los componentes presentacionales (NotificationItem) puedan usarlos sin instanciar useNotifications ni disparar efectos secundarios como el polling.

Interfaz Notification

interface Notification {
  id: string;
  type: string;
  data: Record<string, unknown> & {
    titulo: string;
    mensaje: string;
    solicitud_id?: string;  // ID de solicitud relacionada (si aplica)
    url?: string;           // URL de navegación al hacer clic
  };
  read_at: string | null;   // null = no leída
  created_at: string;       // ISO 8601
}

Tipos de notificación y sus iconos

const icons: Record<string, string> = {
  firma_completada:              "lucide:check-circle",
  firma_rechazada:               "lucide:x-circle",
  firma_expirada:                "lucide:alert-circle",
  solicitud_aprobada:            "lucide:thumbs-up",
  solicitud_rechazada:           "lucide:thumbs-down",
  documento_requerido:           "lucide:file-text",
  estado_actualizado:            "lucide:refresh-cw",
  solicitud_estado_actualizado:  "lucide:refresh-cw",
  comentario_nuevo:              "lucide:message-circle",
  recordatorio:                  "lucide:bell"
};

Colores por tipo

const colors: Record<string, string> = {
  firma_completada:              "text-green-600",
  firma_rechazada:               "text-red-600",
  firma_expirada:                "text-orange-600",
  solicitud_aprobada:            "text-green-600",
  solicitud_rechazada:           "text-red-600",
  documento_requerido:           "text-blue-600",
  estado_actualizado:            "text-blue-600",
  solicitud_estado_actualizado:  "text-blue-600",
  comentario_nuevo:              "text-purple-600",
  recordatorio:                  "text-yellow-600"
};

Función de tiempo relativo

export function formatRelativeTime(date: string): string {
  const diff = Date.now() - new Date(date).getTime();
  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours   = Math.floor(minutes / 60);
  const days    = Math.floor(hours / 24);

  if (seconds < 60)  return "Hace unos segundos";
  if (minutes < 60)  return `Hace ${minutes} minuto${minutes > 1 ? "s" : ""}`;
  if (hours < 24)    return `Hace ${hours} hora${hours > 1 ? "s" : ""}`;
  if (days < 7)      return `Hace ${days} día${days > 1 ? "s" : ""}`;

  return new Date(date).toLocaleDateString("es-CO", {
    year: "numeric", month: "short", day: "numeric"
  });
}

Servicio de Notificaciones

server/services/notification.service.ts es la capa de acceso a datos para las notificaciones. Utiliza Prisma para interactuar con la tabla notifications.

Crear una notificación

const service = notificationService();

await service.createNotification({
  owner_username: "juan.perez",
  type: "solicitud_estado_actualizado",
  data: {
    titulo: "Tu solicitud fue actualizada",
    mensaje: "La solicitud 000001-2026-01 pasó a estado FIRMADO.",
    solicitud_id: "000001-2026-01",
    url: "/dash/solicitudes/000001-2026-01"
  }
});

Métodos del servicio

MétodoDescripción
getNotifications(username)Retorna las últimas 50 notificaciones del usuario, ordenadas por created_at desc
getUnreadCount(username)Cuenta las notificaciones donde read_at IS NULL
markAsRead(id, username)Establece read_at = new Date() con validación de propietario
markAllAsRead(username)updateMany donde read_at IS NULL para el usuario dado
createNotification(data)Inserta una nueva notificación con UUID generado por crypto.randomUUID()

Esquema de la notificación en base de datos

// Campos escritos por createNotification()
{
  id:               string,   // UUID v4
  owner_username:   string,   // Usuario destinatario
  type:             string,   // Tipo de notificación
  data:             JSON,     // { titulo, mensaje, solicitud_id?, url? }
  notifiable_type:  "User",
  notifiable_id:    "1",
  read_at:          null,     // null al crear
  created_at:       Date,
  updated_at:       Date
}

API de Notificaciones

GET /api/notifications?limit=50&unread=false
Authorization: Bearer <token>
Parámetros de query:
  • limit (opcional, default 50) — número máximo de notificaciones a retornar
  • unread (opcional, "true") — retornar solo las no leídas
Respuesta:
{
  "success": true,
  "data": {
    "notifications": [...],
    "unread_count": 3,
    "total": 15
  }
}

Ejemplo de uso en un componente Vue

<script setup lang="ts">
import {
  getNotificationIcon,
  getNotificationColor,
  formatRelativeTime
} from "~/composables/notifications/notificationFormat";

const {
  notifications,
  unreadCount,
  hasUnread,
  loading,
  loadNotifications,
  markAsRead,
  markAllAsRead,
  deleteNotification,
  startPolling
} = useNotifications();

// Cargar al montar e iniciar polling
onMounted(async () => {
  await loadNotifications();
  startPolling();
});
</script>

<template>
  <div class="notification-panel">
    <!-- Badge de no leídas -->
    <span v-if="hasUnread" class="badge">{{ unreadCount }}</span>

    <!-- Botón "Marcar todas como leídas" -->
    <button v-if="hasUnread" @click="markAllAsRead">
      Marcar todas como leídas
    </button>

    <!-- Lista de notificaciones -->
    <ul>
      <li
        v-for="notif in notifications"
        :key="notif.id"
        :class="{ unread: !notif.read_at }"
        @click="markAsRead(notif.id)"
      >
        <!-- Icono según tipo -->
        <Icon
          :name="getNotificationIcon(notif.type)"
          :class="getNotificationColor(notif.type)"
        />

        <div>
          <p class="font-semibold">{{ notif.data.titulo }}</p>
          <p class="text-sm text-gray-600">{{ notif.data.mensaje }}</p>
          <span class="text-xs text-gray-400">
            {{ formatRelativeTime(notif.created_at) }}
          </span>
        </div>

        <!-- Eliminar -->
        <button @click.stop="deleteNotification(notif.id)">×</button>
      </li>
    </ul>

    <p v-if="loading">Cargando notificaciones...</p>
  </div>
</template>

Las notificaciones se generan automáticamente por el servidor en los siguientes eventos: cambio de estado de una solicitud (cualquier transición), finalización de firma digital (FIRMADO), rechazo de firma, solicitud de documentos adicionales y aprobación o rechazo final del crédito. El tipo de notificación determina el icono y el color mostrado en la interfaz, según los mapas definidos en notificationFormat.ts.

Build docs developers (and LLMs) love