Agences, zones & GAB (données géographiques)
backend/src/controllers/agence.controller.js + zone.controller.js + gab.controller.js + frontend/src/views/backoffice/AgenciesView.vue + GabsView.vue + frontend/src/views/public/AgencesView.vue.
Vérifié le 2026-08-13. PostGIS est une extension PostgreSQL obligatoire (voir README backend) — sans elle, la création du schéma échoue.
1. Relations
zone 1—N agence (agence.id_zone, nullable) ; agence 1—N gab (gab.id_agence, nullable — un GAB peut exister sans agence de rattachement).
CHEF_ZONE filtre désormais réellement par zone — corrigé en deux temps le 2026-08-13Le rôle backoffice CHEF_ZONE avait la permission view_all_agencies alors que users.id_zone n'était jamais lu comme filtre — il voyait donc toutes les agences de toutes les zones, comme un SUPER_ADMIN. Un module partagé a été introduit (backend/src/utils/agency-scope.js) :
agencyScopeWhere(user)— fragmentwherePrisma pour les listes : aucune restriction si vue globale,{ agence: { id_zone: user.id_zone } }siCHEF_ZONE, sinon{ id_agence: user.id_agence }.canAccessAgency(user, agenceId)— même logique pour un accès à un enregistrement précis.
Ce premier correctif ne suffisait pas — testé et confirmé cassé en conditions réelles avant d'être vraiment corrigé :
view_all_agenciescourt-circuitait tout le reste.agencyScopeWhere/canAccessAgencyvérifient d'abord si l'utilisateur a cette permission (accès total, sans restriction) — orCHEF_ZONEl'avait dansseed/seed_roles.js, donc la branche "restreindre à la zone" n'était jamais atteinte. Corrigé en retirantview_all_agenciesdes permissions deCHEF_ZONE(permission déjà retirée en base sur l'environnement de dev, vianode seed/seed_roles.js— pas le seed complet, qui aurait vidé la tableagence).- Une fois ce premier point corrigé, plus aucun dossier n'était visible pour
CHEF_ZONE(régression inverse) :id_zonen'était jamais inclus dans le JWT émis parverify2fa(auth.controller.js) — seulid_agencel'était.agencyScopeWhereretombait donc sur{ id_agence: user.id_agence }, et unCHEF_ZONEn'a justement pas deid_agence(null) — filtre qui ne matchait donc jamais rien. Corrigé en ajoutantid_zoneau payload du JWT et à la réponse deverify2fa, ainsi qu'à la sélection Prisma degetMe.
Vérifié par un test de bout en bout (vrai login + 2FA + appels API) : un CHEF_ZONE de la zone Maritime voit les dossiers de son agence, et reçoit un 403 sur une agence d'une autre zone.
Appliqué à loan.controller.js (getLoanRequests, updateLoanStatus, previewStatusEmail, getLoanRequestById, getLoanRequestPdf) et account_requests.controller.js (getAll, getById). Appliqué aussi, un temps, à account.controller.js, qui s'est révélé être un doublon inutilisé et a été supprimé le même jour — voir Fiche détail d'une demande de compte.
2. Coordonnées géographiques : SQL brut, pas Prisma classique
La colonne PostGIS est Unsupported("geography") côté Prisma — impossible de créer/modifier via le client Prisma standard. agence.service.js/gab.service.js utilisent des requêtes SQL brutes (pool.query, driver pg) avec ST_SetSRID(ST_MakePoint($lng,$lat),4326)::geography pour créer/modifier, et relisent via ST_Y/ST_X pour reconvertir en lat/lng. Seules delete et les comptages passent par Prisma classique.
Toute évolution du CRUD agence/GAB touchant aux coordonnées doit passer par le SQL brut de agence.service.js/gab.service.js — ajouter un champ sans regarder ces fichiers risque de casser silencieusement la mise à jour des coordonnées (un prisma.agence.update() direct ignorerait la colonne geography).
Édition backoffice : deux champs numériques lat/lng simples (AgenciesView.vue, GabsView.vue) — pas de carte interactive pour la saisie.
3. Carte publique (AgencesView.vue)
Carte Leaflet (tuiles CartoDB Voyager), marqueurs distincts agence/siège/GAB, popup au clic, ajustement automatique de la vue sur les points valides, recherche texte (ville/désignation/zone) avec flyTo. Aucun calcul de distance ni "agence la plus proche" — ni géolocalisation navigateur ni ST_Distance ne sont utilisés nulle part dans le code actuel, malgré PostGIS qui le permettrait nativement.
4. Suppression d'agence : soft ou hard selon les dépendances
agence.remove fait un soft-delete (deleted_at) si l'agence est référencée par un dossier de prêt, une demande de compte, un GAB ou un utilisateur ; sinon un hard-delete avec suppression en cascade des numéros de téléphone associés (voir note ci-dessous). unsetOtherSieges garantit qu'une seule agence est marquée "siège" à la fois.
5. Note : sellphone_number
La table sellphone_number (numéros de téléphone de contact, rattachés à une agence ou à la fiche entreprise) n'a rien à voir avec de la vente malgré son nom — voir Contenu vitrine pour la clarification complète.
6. Pièges connus pour un futur développeur
— corrigé le 2026-08-13 (section 1). Si un nouveau contrôleur ajoute un filtre par agence, utiliserCHEF_ZONEne filtrait pas réellement par zoneagencyScopeWhere/canAccessAgency(backend/src/utils/agency-scope.js) plutôt que de réécrire le patternhasViewAllà la main.- Toujours passer par
agence.service.js/gab.service.jspour toute modification touchant lat/lng, jamais par Prisma direct. - PostGIS reste une dépendance dure à l'installation — image Docker
postgis/postgis:15-3.3déjà utilisée dansdocker-compose.yml.