Fiche détail d'une demande de prêt (backoffice)
src/views/backoffice/LoanRequestDetailView.vue (frontend) — page authentifiée, permet à un gestionnaire de consulter et corriger une demande de prêt, gérer ses pièces justificatives, changer son statut, et consulter son historique de modifications.
Vérifié ligne à ligne le 2026-08-12.
1. Le système de brouillon éditable (draft)
Tous les champs "garantis" du formulaire (colonnes de loan_requests + clés d'additional_data) sont affichés comme des champs éditables, même si le client ne les a pas renseignés — un gestionnaire doit pouvoir compléter un dossier incomplet.
draft / buildDraft() / saveDraft()
draft(ref) : copie de travail construite depuisrequest(donnée chargée depuis l'API) +fieldRequirements(pour préremplir les champs dynamiques même absents).buildDraft()reconstruit systématiquementdraftdepuis zéro à chaque chargement — pas de fusion incrémentale.draftSnapshot: version JSON figée dedraftau moment du chargement, sert de référence pour détecter un changement (isDraftDirty = JSON.stringify(draft) !== draftSnapshot).- Un seul bouton "Enregistrer les modifications" apparaît dans l'en-tête, seulement si
isDraftDirty. saveDraft()envoie tout le contenu dedraften un seulPATCH /loan-requests/:id(colonnes garanties en racine +additional_data), puis recharge intégralement la fiche (loadRequest()) pour repartir d'un état serveur garanti cohérent plutôt que de fusionner la réponse localement.
Garde anti-perte de modifications
Deux mécanismes combinés, actifs tant que hasUnsavedChanges (= brouillon modifié ou décision statut/note non enregistrée, voir section 2) :
window.addEventListener('beforeunload', ...)— fermeture d'onglet / rafraîchissement.onBeforeRouteLeave(Vue Router) — navigation interne, confirmation viawindow.confirm.
Pourquoi debt_ratio est éditable mais sans code couleur
Le champ Taux d'Endettement est un simple input numérique (draft.debt_ratio_pct), sans coloration verte/orange/rouge — décision explicite : la couleur suggérait un jugement automatique qui n'a plus de sens une fois que le gestionnaire peut lui-même corriger la valeur. Une carte séparée ("Normes Taux d'Endettement BCEAO") affiche les seuils de référence (≤33% acceptable, 33-40% à risque, >40% critique) à titre indicatif, sans les appliquer automatiquement à l'affichage du champ.
Le ratio est recalculé automatiquement côté serveur (loan.controller.js, updateLoanStatus) si les champs qui le composent (salaire, mensualité, total des engagements existants, voir section 3) changent et qu'aucune valeur explicite de debt_ratio n'est envoyée — sinon la valeur envoyée est prioritaire. En pratique, saveDraft() envoie toujours debt_ratio explicitement (calculé depuis draft.debt_ratio_pct), donc le recalcul serveur automatique ne s'applique que si cette page n'est pas le seul point d'entrée sur updateLoanStatus.
2. Décision (statut + note) — avec aperçu de l'email avant envoi
Fonctionnalité qui n'existait pas dans une version antérieure de cette page : avant qu'un changement de statut ne soit réellement enregistré, le gestionnaire voit l'email exact qui sera envoyé au client.
confirmDecision(): si le brouillon a des modifications non enregistrées, bloque avec un message ("enregistrez d'abord vos modifications"). Si le statut sélectionné est identique au statut actuel, enregistre directement (saveStatus()) sans passer par l'aperçu — pas de changement de statut, donc pas d'email en jeu. Sinon, appelleGET /loan-requests/:id/status-email-preview(backend :previewStatusEmail) et ouvre un modal affichant l'objet et le corps HTML de l'email.- Le endpoint de prévisualisation réutilise la même fonction (
buildStatusUpdateEmailContent) que l'envoi réel — l'aperçu affiché correspond garanti à l'email qui partira, pas une approximation. datavautnulldans la réponse si ce statut ne déclenche aucun envoi (ex : passage àEN_ATTENTE) — le modal affiche alors "Aucun email ne sera envoyé au client".- Le gestionnaire valide via "OK, envoyer" (
confirmSendAndSave→saveStatus()) ou annule (cancelDecision, remetcurrentStatusà la valeur d'origine).
isDecisionDirty (statut ou note modifiés mais pas encore enregistrés) est distinct de isDraftDirty — les deux entrent dans hasUnsavedChanges pour la garde anti-perte de la section 1.
3. Projet & Analyse Financière
Indicateurs clés
Montant demandé, durée sollicitée, taux d'endettement (debt_ratio_pct) — tous éditables directement en ligne.
Engagements en cours déclarés (prêts existants)
Liste éditable d'engagements financiers déjà en cours pour le client (montant initial, mensualité, date d'échéance), saisis à l'origine par le client mais à vérifier via son n° de compte et à corriger si nécessaire par le gestionnaire. addExistingLoan() / removeExistingLoan(i) permettent d'ajouter/retirer une ligne.
Envoyés dans additional_data.existing_loans (le tableau brut) et additional_data.total_existing_monthly (somme des mensualités, calculée côté frontend juste avant l'enregistrement dans saveDraft()).
total_existing_monthly entre directement dans le calcul du taux d'endettement BCEAO côté backend (loan.controller.js, updateLoanStatus : finalDebtRatio = (chargeForRatio + existingMonthlyForRatio) / salaireForRatio, et déjà dans createLoanRequest à la soumission initiale). Une erreur de saisie ici fausse directement le ratio utilisé pour la décision.
4. Génération de la fiche PDF
Bouton "Générer le PDF" (visible seulement si request.loan_type === 'scolaire') → openPdf() → GET /loan-requests/:id/fiche (backend : getLoanRequestPdf, réponse application/pdf), ouvert dans un nouvel onglet via un Blob URL.
- Désactivé si
isDraftDirty— même logique que la décision : le gestionnaire doit d'abord enregistrer ses modifications, pour que le PDF généré reflète l'état réellement sauvegardé et pas un brouillon local non persisté. - Réservé aux prêts scolaires pour l'instant côté backend (
loan.loan_type !== 'scolaire'→ 400).
5. Système d'attribution de documents "à classer"
Contexte complet côté formulaire public dans Demande de prêt scolaire. Côté backoffice, trois pièces s'articulent :
requiredDocItems / findDocForKey
requiredDocItems (computed) liste tous les documents actuellement attendus (bulletins de paie, attestation si draft.has_salary_engagement === false, pièces configurables). findDocForKey(key) cherche, pour une clé donnée, un document en base dont le stored_name se termine par _${key} — s'il existe, le document est considéré comme fourni pour cet emplacement.
unmatchedDocuments — les documents "à classer"
Documents présents en base (table documents) mais dont le stored_name ne correspond à aucune clé actuellement requise — typiquement les fichiers envoyés via la sélection groupée desktop du formulaire public, stockés sous une clé anonyme a_classer_N. Affichés avec leur vrai nom d'upload (original_name), pas une étiquette technique devinée.
Attribution manuelle (openAttributeModal / attributeDocument)
Chaque document non attribué a un menu (▾) avec "Télécharger" et "Enregistrer en tant que". Ce dernier ouvre un modal listant les emplacements requis encore vides, et au clic déclenche PATCH /api/documents/:id avec le nouveau stored_name. Une fois attribué, le document sort naturellement de unmatchedDocuments et apparaît comme fourni dans requiredDocItems — aucune logique de synchronisation manuelle nécessaire.
resolveDocLabel — affichage lisible
Pour afficher un nom de document lisible (ex : dans l'historique, "Attribué à : Carte nationale d'identité..."), on re-matche la clé technique contre requiredDocItems pour retrouver le vrai libellé configuré, plutôt que de tenter de "réparer" la chaîne technique (docKey() supprime les accents à la source). Repli sur un nettoyage basique underscore→espace uniquement pour les clés orphelines/anonymes sans correspondance connue.
6. Historique des modifications
Carte compacte sur la page (dernière modification + bouton "Voir l'historique complet"), détail complet dans un modal paginé (9 entrées par page) avec chaque entrée en accordéon (repliée par défaut). Voir Journal d'activité pour le fonctionnement du système sous-jacent. activityEntryDetails(entry) traduit le meta brut (diff JSON) en phrases lisibles selon le type d'action (loan_request_updated, loan_document_added/removed/attributed).