Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ariellukezz/admision-web/llms.txt

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

The Sistema de Admisión Web delivers real-time notifications through a two-layer architecture: Firebase Cloud Messaging (FCM) for browser push notifications and Laravel’s database notification channel for persistent in-app badges and history. When a Revisor approves, rejects, or observes an applicant’s documents, both channels fire simultaneously — the applicant receives a push notification on their device and the event is stored in the notifications table for later retrieval. Revisors, on the other hand, receive their unread count as a server-side Inertia shared prop so the badge is always accurate on page load, with FCM filling in live updates while the tab is open.

Architecture Overview

Firebase Cloud Messaging (Push)

Browser push notifications delivered via the Web Push Protocol. Requires an FCM token registered per device and per user. Tokens are stored in the fcm_tokens table and managed by the FirebaseService class using the kreait/firebase-php SDK.

Laravel Database Notifications

Persistent notifications stored in the notifications table using Laravel’s built-in Notifiable trait. Both postulantes and revisors use this channel. Records include structured JSON data with message type, applicant details, document lists, and appointment information.

How Applicants Receive Notifications

Applicants (id_rol = 8) receive push notifications whenever a Revisor takes an action on their submission. The trigger flows through RevisorDocumentoService, which dispatches a Laravel notification and then calls FirebaseService::sendToTokens() with the applicant’s registered FCM tokens.

Notification types sent to applicants

Notification classFCM tipo fieldTrigger
RevisionIniciadaNotificationrevision_iniciadaRevisor calls POST /revisor/iniciar-revision/{dni}
RevisionCompletadaNotificationrevision_completadaAll documents validated — citación scheduled
RevisionCompletadaNotificationrevision_pendienteReview finalized with pending documents
(FCM direct)revision_renotificarRevisor sends a manual reminder via POST /revisor/renotificar-postulante/{dni}

Notification data payload structure

The JSON data field stored in the notifications table and forwarded over FCM carries the following fields:
{
  "tipo": "revision_completada",
  "mensaje": "Tu revisión ha sido completada.",
  "postulante_nombre": "QUISPE MAMANI Juan Carlos",
  "postulante_dni": "12345678",
  "veces": 1,
  "documentos_verificados": ["DNI", "Certificado de Estudios"],
  "documentos_pendientes": [],
  "fecha_cita": "2025-09-15",
  "hora_inicio": "09:00",
  "hora_fin": "09:30",
  "lugar": "Pabellón A - Sala 3",
  "instrucciones": "Traer DNI original"
}
The PostulanteNotificationController maps these raw fields when returning the notification list to the frontend:
// PostulanteNotificationController.php
$notificaciones = $user->notifications()
    ->orderBy('created_at', 'desc')
    ->limit($limit)
    ->get()
    ->map(function ($n) {
        $data = $n->data;
        return [
            'id'                     => $n->id,
            'tipo'                   => $data['tipo'] ?? 'otro',
            'mensaje'                => $data['mensaje'] ?? '',
            'postulante_nombre'      => $data['postulante_nombre'] ?? '',
            'postulante_dni'         => $data['postulante_dni'] ?? '',
            'veces'                  => $data['veces'] ?? 1,
            'documentos_verificados' => $data['documentos_verificados'] ?? [],
            'documentos_pendientes'  => $data['documentos_pendientes'] ?? [],
            'fecha_cita'             => $data['fecha_cita'] ?? null,
            'hora_inicio'            => $data['hora_inicio'] ?? null,
            'hora_fin'               => $data['hora_fin'] ?? null,
            'lugar'                  => $data['lugar'] ?? null,
            'instrucciones'          => $data['instrucciones'] ?? null,
            'leida'                  => $n->read_at !== null,
            'created_at'             => $n->created_at->format('d/m/Y H:i'),
            'created_at_diff'        => $n->created_at->diffForHumans(),
        ];
    });

Applicant notification API endpoints

MethodPathDescription
GET/notificacionesList all notifications (default limit: 20)
GET/notificaciones/no-leidasCount of unread notifications
POST/notificaciones/{id}/leerMark a single notification as read
POST/notificaciones/leer-todasMark all notifications as read

How Revisors Receive Notifications

Revisors receive notifications through two complementary mechanisms: a server-side Inertia prop for the badge count, and FCM push for real-time delivery of new submission alerts while the tab is open.

Inertia shared prop — notificacionesNoLeidas

Every Inertia page rendered for a Revisor automatically includes the unread notification count. This is computed in HandleInertiaRequests::share():
// app/Http/Middleware/HandleInertiaRequests.php
$notificacionesNoLeidas = 0;

if ($user && $user->id_rol == 2) {
    $notificacionesNoLeidas = $user->unreadNotifications()->count();
}

return array_merge(parent::share($request), [
    'auth' => [
        'user'        => $userData,
        'permissions' => $permisos,
    ],
    'proceso_actual'          => $procesoActual,
    'flash'                   => fn () => ['success' => $request->session()->get('success')],
    'showingMobileMenu'       => false,
    'notificacionesNoLeidas'  => $notificacionesNoLeidas,
]);
The Vue layout (LayoutDocente.vue) reads $page.props.notificacionesNoLeidas to render the badge in the navigation bar without any additional API call on mount.

Revisor notification API endpoints

MethodPathController methodDescription
GET/revisor/notificacionesindexList notifications (default limit: 20)
GET/revisor/notificaciones/no-leidasnoLeidasUnread count as { no_leidas: N }
POST/revisor/notificaciones/{id}/leermarcarLeidaMark one notification as read
POST/revisor/notificaciones/leer-todasmarcarTodasLeidasMark all notifications as read

Live FCM updates in SolicitudesRevision.vue

The Revisor/SolicitudesRevision.vue page subscribes to the fcm-message event emitted by the useNotificaciones composable. When a new push message arrives, the page reloads only the solicitudes Inertia prop — preserving scroll position and local UI state:
// resources/js/Pages/Revisor/SolicitudesRevision.vue
import { useNotificaciones } from '@/composables/useFcm';

let fcmListener = null;

onMounted(() => {
    const fcm = useNotificaciones();
    fcmListener = () => router.reload({
        only: ['solicitudes'],
        preserveScroll: true,
        preserveState: true,
    });
    fcm.fcmEventTarget.addEventListener('fcm-message', fcmListener);
});

onUnmounted(() => {
    if (fcmListener) {
        const fcm = useNotificaciones();
        fcm.fcmEventTarget.removeEventListener('fcm-message', fcmListener);
    }
});
Revisors are also automatically subscribed to the revisores FCM topic when their device token is registered. This allows the server to broadcast to all active reviewers simultaneously without enumerating individual tokens:
// PostulanteDocumentoController.php — registrarFcmToken()
if ($user->isRevisor()) {
    $firebaseService = new FirebaseService();
    $firebaseService->subscribeToTopic('revisores', [$request->token]);
}

FCM Token Registration Flow

Device tokens are registered on every authenticated page load. The useNotificaciones composable (resources/js/composables/useFcm.js) handles the full lifecycle: permission request, service worker registration, token retrieval, and backend sync.
// resources/js/composables/useFcm.js
import { messaging } from '@/firebase';
import { getToken, deleteToken, onMessage } from 'firebase/messaging';

const VAPID_KEY = "BKYJGo1CC3swVWZ49PnULvtwbgG1s45UKoRKtfy43fe9LcmYO2ni4hltrI7YkLRIIn-N17NvAIAw8oX-3FA7fDk";

const activar = async () => {
    const permiso = await Notification.requestPermission();
    if (permiso !== 'granted') return;

    const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');

    const currentToken = await getToken(messaging, {
        vapidKey: VAPID_KEY,
        serviceWorkerRegistration: registration,
    });

    if (!currentToken) return;

    // Persist token in the backend
    await axios.post('/fcm-token', {
        token: currentToken,
        device_type: 'web',
    });

    localStorage.setItem('fcm_token', currentToken);
};
The init() function is called on the initial Inertia app setup in app.js. It silently re-registers the token if permission was already granted, ensuring the backend always has a current association between the token and the authenticated user:
// resources/js/app.js — createInertiaApp setup()
setup({ el, App, props, plugin }) {
    if (props.initialPage?.props?.auth?.user) {
        const currentUserId = props.initialPage.props.auth.user.id;
        const storedUserId = sessionStorage.getItem('fcm_user_id');

        if (storedUserId && storedUserId != currentUserId) {
            localStorage.removeItem('fcm_token');
        }
        sessionStorage.setItem('fcm_user_id', currentUserId);

        const fcm = useNotificaciones();
        fcm.init().catch(e => console.warn('FCM init skipped:', e));
    } else {
        sessionStorage.removeItem('fcm_user_id');
    }

    return createApp({ render: () => h(App, props) })
        .use(plugin)
        .use(Antd)
        .use(ZiggyVue, Ziggy)
        .mount(el);
},
On logout, fcm.logout() deletes the token from both the backend (DELETE /fcm-token) and the Firebase SDK via deleteToken(messaging), and removes it from localStorage.
The fcm_tokens table stores one row per unique token value and is updated with the current user_id on each registration via updateOrCreate. The FcmToken model columns are user_id, token, and device_type (web, android, or ios). A user may have multiple rows if they use several devices or browsers.

Backend FCM Delivery — FirebaseService

The server-side App\Services\FirebaseService class wraps the kreait/firebase-php SDK. It is initialized with a service account credentials file and exposes three delivery methods used by the notification flow:
// app/Services/FirebaseService.php

// Send to one or more device tokens
$firebaseService->sendToTokens(
    $tokens,            // array of FCM token strings
    'Documentos por corregir',
    '1 documento(s) observado(s). Debes corregirlos y solicitar nuevamente revisión.',
    [
        'tipo'           => 'revision_pendiente',
        'postulante_dni' => '12345678',
    ],
    route('postulante.documentos')  // optional click URL (WebPushConfig link)
);

// Broadcast to all subscribers of a topic
$firebaseService->sendToTopic('revisores', $title, $body, $data, $clickUrl);

// Manage topic subscriptions
$firebaseService->subscribeToTopic('revisores', [$token]);
$firebaseService->unsubscribeFromTopic('revisores', [$token]);
All data values are cast to strings (array_map('strval', $data)) before being attached to the CloudMessage, as the FCM data payload only supports string-typed fields.

Foreground Message Handling

When the browser tab is in the foreground, Firebase does not display a native notification automatically. The onMessage handler in useFcm.js intercepts the payload and forwards it to the active service worker to render:
// resources/js/composables/useFcm.js
onMessage(messaging, async (payload) => {
    // Notify the layout to reload the notification list
    fcmEventTarget.dispatchEvent(new Event('fcm-message'));

    const title = payload.data?.title || payload.notification?.title || 'Nueva notificación';
    const body  = payload.data?.body  || payload.notification?.body  || '';
    const url   = payload.data?.url   || '';

    const registration = await navigator.serviceWorker.ready;
    registration.showNotification(title, {
        body,
        icon: '/favicon.ico',
        requireInteraction: true,
        tag: payload.data?.tipo || 'default',
        data: { ...payload.data, url },
    });
});
The fcm-message custom event is listened to by both LayoutDocente.vue (for revisors) and PostulanteAuthenticatedLayout.vue (for applicants), triggering a targeted notification list reload without a full page refresh.

Firebase Configuration

The backend Firebase connection is configured via environment variables. The .env.example defines the following keys:
# .env
FIREBASE_PROJECT_ID=app-admision-2026
FIREBASE_CREDENTIALS_PATH="${APP_STORAGE}/app/firebase-credentials.json"
FIREBASE_AUTH_DOMAIN=app-admision-2026.firebaseapp.com
FIREBASE_MESSAGING_SENDER_ID=116539912844415710654
FIREBASE_API_KEY=
FIREBASE_APP_ID=
FIREBASE_STORAGE_BUCKET=app-admision-2026.firebasestorage.app
FIREBASE_VAPID_KEY=
The frontend Firebase SDK is initialized in resources/js/firebase.js with the project’s public config (safe to expose to the browser). The FirebaseConfigController also exposes a dynamic GET /firebase-config endpoint and generates the service worker at GET /firebase-messaging-sw.js with the config values injected server-side:
// resources/js/firebase.js
import { initializeApp } from 'firebase/app';
import { getMessaging } from 'firebase/messaging';

const firebaseConfig = {
    apiKey:            "AIzaSyCQh0hM9glpqQcYMxmygzpz6bv_D0ZBLbg",
    authDomain:        "admision-unap.firebaseapp.com",
    projectId:         "admision-unap",
    storageBucket:     "admision-unap.firebasestorage.app",
    messagingSenderId: "75683577446",
    appId:             "1:75683577446:web:9bc3ccc3f50740e300f7a7",
};

const app       = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

export { messaging };
The service worker is served dynamically at /firebase-messaging-sw.js by FirebaseConfigController::serviceWorker(), which injects the same environment-driven values so the background message handler always matches the active project.
FCM device tokens expire or become invalid when a user clears their browser data, reinstalls the app, or when Firebase rotates the token. The init() function in useFcm.js re-registers the token on the initial page load to handle this. If the stored localStorage token differs from the one returned by getToken(), a new POST /fcm-token request is made automatically. Always ensure FIREBASE_CREDENTIALS_PATH points to a valid service account JSON with the Firebase Cloud Messaging API (V1) permission enabled in the Google Cloud Console.

Build docs developers (and LLMs) love