1/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 MAT â MEL Assistante IA v3.7.2 (Phase 3) 3 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4 Contenu : 5 - _PLU_DATA_FALLBACK : base PLU embarquée (résilience offline 1er lancement) 6 - _MEL_TREE_FALLBACK : arbre de décision embarqué 7 - PLU_DATA, MEL_TREE : variables mutables, initialisées au fallback, 8 puis écrasées par les JSON externes dès qu'ils 9 arrivent via loadMelData() (arrière-plan). 10 - loadMelData() : fetch non-bloquant de data/plu-data.json et 11 data/mel-tree.json â appelé depuis mat-init.js. 12 - PLU helpers : pluNormalizeZone, pluGetZone, pluRenderAuth, pluRenderZoneCard, pluBuildAnswer 13 - MEL UI : melShowTree, melSelectCat, melSelectQuestion, _showPluAnswer, pluCloseAnswer, pluSwitchTab 14 - Direct : _renderDirectAnswer, _showDirectAnswer, melOpenChatFromDirect 15 - Chat : melOpenChat, melResetTree, addMsg, showTyping, sendMel, MEL_THEME_SUGGESTIONS 16 - Zone PLU : melFindZoneByAddr, melFindZoneByGPS, _fetchZonePLU, _showManualZoneSelector, _setZoneResult 17 18 ââ PHASE 3 : externalisation des données ââ 19 Les données PLU et MEL_TREE vivent désormais dans : 20 - data/plu-data.json 21 - data/mel-tree.json 22 Pour modifier une zone, une question ou une réponse, éditez le JSON 23 sur GitHub directement â AUCUN changement de code nécessaire. 24 Le fallback embarqué ci-dessous garantit que MEL fonctionne même si 25 les fichiers JSON ne sont pas accessibles (offline, 404, etc.). 26 Copyright (c) 2024-2026 Commune de Mézières-lez-Cléry â Licence MIT 27 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 28 29// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 30// PLU â Fallback embarqué (extrait du règlement 4.1) 31// â ï¸ Ne pas modifier ce bloc : éditez data/plu-data.json à la place 32// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 33const _PLU_DATA_FALLBACK = { 34 zones: { 35 Ua: { 36 label: "Ua â Centre-bourg / hameaux anciens", 37 description: "Secteurs bâtis les plus anciens du bourg. Vocation principale : habitat.", 38 interdictions: ["Industrie","Agriculture","Carrières","Caravanes/camping","ICPE soumises à autorisation"], 39 emprise: "50% max", espaces_verts: "non réglementé", 40 hauteur_principale: "6 m max à l'égout du toit", hauteur_annexe: "non limitée spécifiquement", 41 recul_voie_principale: "à l'alignement ou ⥠2 m (si clôture assure la continuité)", 42 recul_voie_annexe: "à l'alignement ou ⥠3 m", recul_voie_abri: "⥠3 m", 43 recul_sep: "en contiguïté ou ⥠3 m", 44 toiture_principale: "⥠2 pentes ⥠35°, ardoises ou tuiles plates", 45 toiture_annexe_sup30: "⥠1 pente ⥠25°, ardoises ou tuiles plates", 46 toiture_annexe_inf30: "pente ⥠25°, teinte ardoise ou brun rouge", 47 cloture_voie: "mur plein ou claire-voie, max 1,50 m", cloture_sep: "mur plein ou grillage/treillage + haie, max 1,80 m", 48 cloture_carrefour: "max 1,20 m sur 20 m", cloture_dp: true, 49 abri_materiaux: "pierre, brique, parpaing enduit ou bois", 50 piscine: "â Autorisée (emprise totale ⤠50%)", stationnement: "2 places min/logement", 51 rdc_max: "⤠0,50 m / sol naturel" 52 }, 53 Ub: { 54 label: "Ub â Habitat résidentiel récent", 55 description: "Secteur bâti à dominante d'habitat construit principalement dans la 2e moitié du XXe siècle.", 56 interdictions: ["Industrie","Agriculture/sylviculture","Dépôts à l'air libre","Carrières","Caravane
56s/camping"], 57 emprise: "30% max", espaces_verts: "30% min (libre de construction et imperméabilisation)", 58 hauteur_principale: "4 m max à l'égout du toit", hauteur_annexe: "2,5 m max", 59 recul_voie_principale: "⥠5 m", recul_voie_annexe: "à l'alignement ou ⥠5 m", recul_voie_abri: "⥠3 m", 60 recul_sep: "en contiguïté ou ⥠3 m", 61 toiture_principale: "⥠2 pentes ⥠35°, ardoises ou tuiles plates", 62 toiture_annexe_sup30: "⥠1 pente ⥠25°, ardoises ou tuiles plates", 63 toiture_annexe_inf30: "pente ⥠25°, teinte ardoise ou brun rouge", 64 cloture_voie: "mur à claire-voie UNIQUEMENT, max 1,50 m", cloture_sep: "mur plein pierre/maçonnerie ou grillage/treillage + haie, max 1,80 m", 65 cloture_carrefour: "max 1,20 m sur 20 m", cloture_dp: true, 66 abri_materiaux: "pierre, brique, parpaing enduit ou bois â tôle ondulée INTERDITE", 67 piscine: "â Autorisée (emprise ⤠30%, espaces verts ⥠30%)", stationnement: "2 places min/logement", 68 rdc_max: "⤠0,30 m / sol naturel", 69 facades: "palettes traditionnelles â blanc pur et couleurs vives INTERDITS" 70 }, 71 Ub1: { 72 label: "Ub1 â Clos de Manthelon (prescriptions spécifiques)", 73 description: "Extension du lotissement Clos de Manthelon. Mêmes règles que Ub + prescriptions spécifiques ci-dessous.", 74 interdictions: ["Terrasses en toiture","Toits à un seul versant (sauf annexes)","Toits à 4 pans","Tuiles couleur ardoise","Tôle ondulée","Amiante-ciment","Plaques ciment en clôture"], 75 emprise: "30% max + implantation dans zones constructibles du plan parcellaire", 76 espaces_verts: "30% min + 1 arbre haute tige / 250 m²", 77 hauteur_principale: "4 m max (égout) ET 8 m max absolu", hauteur_annexe: "2,5 m max", 78 recul_voie_principale: "⥠5 m", recul_voie_abri: "⥠5 m", recul_sep: "⥠3 m", 79 toiture_principale: "2 pentes entre 40° et 45°, tuiles plates TERRE CUITE uniquement, min 22 tuiles/m², faîtage selon plan parcellaire", 80 cloture_voie: "haie végétale ⥠3 variétés + grillage vert max 1,50 m (retrait 0,80 m de la limite)", 81 cloture_sep: "grillage vert max 1,50 m + haie max 1,80 m â plaques ciment INTERDITES", 82 cloture_portail: "retrait 5 m espaces communs, hauteur max 1,50 m, piliers max 1,80 m", cloture_dp: true, 83 niveaux: "RDC seul ou RDC + combles UNIQUEMENT", 84 piscine: "â Autorisée (selon zones constructibles du plan parcellaire)" 85 }, 86 Ue: { 87 label: "Ue â Ãquipements publics", 88 description: "Zone réservée aux équipements publics.", 89 interdictions: ["Industrie","Artisanat","Agriculture","Dépôts","Caravanes","Carrières"], 90 emprise: "non réglementée", hauteur_principale: "7 m max", 91 recul_voie_principale: "à l'alignement ou ⥠2 m", recul_sep: "en contiguïté ou ⥠3 m", 92 piscine: "â Zone réservée aux équipements publics â consulter mairie" 93 }, 94 Ui: { 95 label: "Ui â Zone industrielle", 96 interdictions: ["Hébergement hôtelier","Agriculture","Caravanes","Carrières"], 97 emprise: "60% max", hauteur_principale: "8 m max", 98 recul_voie_principale: "à l'alignement ou ⥠5 m", recul_sep: "⥠moitié de la hauteur, min 3 m", 99 cloture_voie: "grillage couleur sombre, max 1,50 m", cloture_sep: "grillage couleur sombre, max 1,80 m", cloture_dp: true, 100 piscine: "â Zone industrielle â non destinée à cet usage" 101 }, 102 "1AU": { 103 label: "1AU â Zone à urbaniser (court terme)", 104 description: "Construction uniquement dans le cadre d'une opération d'aménagement d'ensemble.", 105 interdictions: ["Constructions isolées hors opération d'ensemble","Industrie","Agriculture","Dépôts","Caravanes"], 106 emprise: "40% max", espaces_verts: "30% min", 107 hauteur_principale: "5 m max", hauteur_annexe: "2,5 m max", 108 recul_voie_principale: "⥠5 m", recul_voie_annexe: "⥠5 m", recul_sep: "en contiguïté ou ⥠3 m", 109 cloture_voie: "non obligatoire â si clôture : grillage sombre max 1,20 m + haie", 110 cloture_sep: "non obligatoire â si clôture : grillage sombre max 1,50 m + haie", cloture_dp: true, 111 abri_materiaux: "matériaux traditionnels (pierre, brique, parpaing enduit) et bois", 112 piscine: "â Autorisée (emprise ⤠40%)", stationnement: "2 places min/logement",
113 alerte: "â ï¸ Toute construction doit s'inscrire dans une opération d'aménagement d'ensemble (lotissement, ZACâ¦)." 114 }, 115 "2AU": { 116 label: "2AU â Réserve foncière (long terme)", 117 description: "Zone inconstructible. Urbanisation uniquement après révision du PLU.", 118 interdictions: ["TOUTE construction privée"], 119 piscine: "â INTERDITE â zone inconstructible", 120 alerte: "ð« Cette zone est inconstructible. Seuls les équipements publics peuvent être autorisés après révision du PLU." 121 }, 122 A: { 123 label: "A â Zone agricole", 124 description: "Seules les constructions liées à l'exploitation agricole sont autorisées.", 125 interdictions: ["Constructions non agricoles","Habitations (sauf logement de fonction)","Grillages/piquets fer/poteaux béton"], 126 hauteur_principale: "10 m max hors tout (exploitation) / 5 m max (habitation) / 2,5 m (abris animaux)", 127 recul_voie_principale: "⥠8 m (100 m de l'axe A71)", recul_sep: "en contiguïté ou ⥠3 m", 128 cloture_voie: "pieux de bois + 3 fils métalliques, max 1,20 m", 129 cloture_sep: "pieux de bois + 3 fils, max 1,20 m (1,80 m si élevage déclaré)", 130 piscine: "â INTERDITE en zone A", 131 alerte: "â ï¸ Zone strictement agricole. Constructions non agricoles interdites. Voir secteur Ah pour les hameaux." 132 }, 133 Ah: { 134 label: "Ah â Hameaux agricoles", 135 description: "Hameaux et écarts non agricoles en zone A. Extensions limitées.", 136 emprise: "Extensions ⤠20% de la SP existante / Annexes ⤠50 m²", 137 espaces_verts: "30% min", hauteur_principale: "5 m max", hauteur_annexe: "2,5 m max", 138 recul_voie_principale: "à l'alignement ou ⥠5 m", recul_sep: "en contiguïté ou ⥠3 m", 139 toiture_principale: "⥠2 pentes ⥠35°, ardoises ou tuiles plates", 140 piscine: "â ï¸ Possible si dans le cadre de l'extension autorisée (⤠20% SP existante)", 141 changement_destination: "Autorisé vers habitat, bureaux, commerce, artisanat, tourisme, loisirs", 142 alerte: "Seules extensions (+20% max) et annexes (⤠50 m²) autorisées. Pas de nouvelle construction principale." 143 }, 144 N: { 145 label: "N â Zone naturelle et forestière", 146 interdictions: ["Industrie","Artisanat","Bureaux","Entrepôts","Dépôts","Camping","Tout changement dans les EBC"], 147 piscine: "â INTERDITE en zone N", 148 alerte: "ð² Zone naturelle protégée. Seules constructions agricoles/forestières et d'intérêt public autorisées." 149 }, 150 Nh: { 151 label: "Nh â Hameaux naturels", 152 emprise: "Extensions ⤠20% de la SP existante / Annexes ⤠50 m²", 153 espaces_verts: "30% min", hauteur_principale: "5 m max", hauteur_annexe: "2,5 m max", 154 recul_voie_principale: "⥠5 m", recul_sep: "en contiguïté ou ⥠3 m", 155 cloture_voie: "pieux de bois + 3 fils, max 1,20 m", 156 toiture_principale: "⥠2 pentes ⥠35°, ardoises ou tuiles d'aspect plat", 157 piscine: "â INTERDITE â pas de nouvelle construction principale", 158 changement_destination: "Autorisé vers habitat, tourisme, loisirs uniquement", 159 alerte: "Seules extensions (+20% max) et annexes (⤠50 m²) autorisées. Pas de nouvelle construction principale." 160 }, 161 Nj: { 162 label: "Nj â Jardins naturels", 163 emprise: "⤠20 m²", hauteur_principale: "2,5 m max", 164 recul_voie_principale: "⥠10 m", recul_sep: "en contiguïté ou ⥠3 m", 165 piscine: "â Autorisée explicitement avec local technique dédié", 166 alerte: "Seuls abris de jardin (⤠20 m²), piscines et locaux techniques autorisés." 167 }, 168 Nl: { 169 label: "Nl â Loisirs naturels", 170 piscine: "â Zone publique collective â projets privés non admis", 171 alerte: "Seuls aménagements de loisirs à vocation publique et collective autorisés." 172 } 173 }, 174 175 autorisations: { 176 permis: { 177 label: "Permis de construire / Déclaration préalable", 178 tableau: [ 179 {cas:"Extension accolée < 40 m² (SP totale ⤠150 m²)", auth:"Déclaration Préalable (DP)"}, 180 {cas:"Extension accolée ⥠40 m² ou SP totale >
180 150 m²", auth:"Permis de Construire (PC)"}, 181 {cas:"Garage/annexe non accolé < 20 m²", auth:"Déclaration Préalable (DP)"}, 182 {cas:"Garage/annexe non accolé ⥠20 m²", auth:"Permis de Construire (PC)"}, 183 {cas:"Construction neuve", auth:"Permis de Construire (PC) â architecte si > 150 m² SP"}, 184 ], 185 notes: ["Délai DP : 1 mois | PC : 2 mois","Validité : 3 ans","Dépôt : mairie ou GNAU CCTVL","Architecte obligatoire si SP > 150 m²"] 186 }, 187 piscine: { 188 tableau: [ 189 {cas:"< 10 m², non couverte, restant < 3 mois", auth:"Aucune formalité"}, 190 {cas:"< 100 m², non couverte", auth:"Déclaration Préalable (DP)"}, 191 {cas:"< 100 m², couverte < 1,80 m de hauteur", auth:"Déclaration Préalable (DP)"}, 192 {cas:"> 100 m²", auth:"Permis de Construire (PC)"}, 193 ] 194 }, 195 cloture: { 196 tableau: [ 197 {cas:"Tout type de clôture, mur, portail", auth:"Déclaration Préalable obligatoire (délibération CM 01/03/2012)"} 198 ] 199 }, 200 abri: { 201 tableau: [ 202 {cas:"< 5 m²", auth:"Aucune formalité"}, 203 {cas:"5 à 20 m²", auth:"Déclaration Préalable (DP)"}, 204 {cas:"> 20 m²", auth:"Permis de Construire (PC)"}, 205 ] 206 }, 207 toiture: { 208 tableau: [ 209 {cas:"Remplacement à l'identique (même matériau, même couleur)", auth:"Aucune formalité"}, 210 {cas:"Changement de matériau ou de couleur", auth:"Déclaration Préalable (DP)"}, 211 {cas:"Velux / fenêtre de toit", auth:"Déclaration Préalable (DP)"}, 212 {cas:"Lucarne (ajout ou modification)", auth:"Déclaration Préalable (DP)"}, 213 ] 214 }, 215 extension: { 216 tableau: [ 217 {cas:"Extension accolée < 20 m² (si aspect modifié)", auth:"Déclaration Préalable (DP)"}, 218 {cas:"Extension 20 à 40 m² (zone urbaine, SP ⤠150 m²)", auth:"Déclaration Préalable (DP)"}, 219 {cas:"Extension > 40 m² ou SP totale > 150 m²", auth:"Permis de Construire (PC)"}, 220 ] 221 }, 222 panneaux_solaires: { 223 tableau: [{cas:"Panneaux photovoltaïques en toiture", auth:"Déclaration Préalable (DP)"}], 224 note: "Les ouvrages ENR sont exclus du calcul de hauteur maximale dans toutes les zones." 225 } 226 } 227}; 228 229// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 230// PLU â Helpers : normalisation, accès, rendu 231// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 232 233function pluNormalizeZone(raw) { 234 // L'IGN peut retourner "1AU1", "1AUe", "Ub1" etc. 235 // On cherche d'abord exact, puis avec troncature progressive 236 if (!raw) return null; 237 const z = String(raw).trim(); 238 if (PLU_DATA.zones[z]) return z; 239 // Essayer sans suffixe numérique final 240 const base = z.replace(/\d+$/, ''); 241 if (PLU_DATA.zones[base]) return base; 242 // Essayer majuscule 243 const up = z.toUpperCase(); 244 for (const k of Object.keys(PLU_DATA.zones)) { 245 if (k.toUpperCase() === up) return k; 246 } 247 // Chercher une clé qui commence pareil 248 for (const k of Object.keys(PLU_DATA.zones)) { 249 if (z.startsWith(k) || k.startsWith(z)) return k; 250 } 251 return null; 252} 253 254function pluGetZone(zoneKey) { 255 const normalized = pluNormalizeZone(zoneKey); 256 return normalized ? PLU_DATA.zones[normalized] : null; 257} 258 259function pluAuthLink(auth) { 260 // Retourne un lien cliquable si DP ou PC reconnu. 261 // 262 // â ï¸ Les cerfa d'urbanisme changent de NUMÃRO, pas seulement de millésime. 263 // Au 1er janvier 2025, les cerfa 13703 (DP maison individuelle), 13702 264 // (lotissement) et 13404 (DP constructions et travaux) ont été ABROGÃS et 265 // remplacés par le 16702 (constructions et travaux) et le 16703 266 // (aménagements). La fiche R11646, qui portait le 13703, n'a pas déménagé : 267 // elle a été supprimée, et répondait 404 (issue #400). La référence courante 268 // de la DP côté particuliers est R2028. Ne jamais figer un millésime 269 // (« *02 ») dans une URL ni dans un texte : seul le numéro à 5 chiffres est 270 // stable. Voir docs/adr/0029. 271 if (/Déclaration Préalable|\bDP\b/.test(auth)) { 272 return auth + ' <a href="https://www.service-public.gouv.fr/particuliers/vosdroits/R2028" target="_blank" class="plu-form-link">ð Cerfa DP</a>'; 273 } 274 if (/Permis de Construire|\bPC\b/.test(auth)) { 275 return auth + ' <a href="https://www.service-public.gouv.fr/particuliers/vosdroits/R11637" target="_blank" class="plu-form-link">ð Cerfa PC</a>'; 276 } 277 return auth; 278} 279 280function pluRenderAuth(authKey) { 281 const a = PLU_DATA.autorisations[authKey]; 282 if (!a) return ''; 283 let h = '<div class="plu-auth"><div class="plu-auth-title">ð Autorisation requise</div><table class="plu-table">'; 284 for (const row of (a.tableau||[])) { 285 h += `<tr><td>${row.cas}</td><td class="plu-auth-val">${pluAuthLink(row.auth)}</td></tr>`; 286 } 287 h += '</table>'; 288 if (a.notes) h += '<ul class="plu-notes">'+a.notes.map(n=>`<li>${n}</li>`).join('')+'</ul>'; 289 if (a.note) h += '<p class="plu-note">â¹ï¸ '+a.note+'</p>'; 290 h += '</div>'; 291 return h; 292} 293 294function pluRenderZoneCard(zone) { 295 if (!zone) return ''; 296 let h = ''; 297 if (zone.alerte) h += `<div class="plu-alerte">${zone.alerte}</div>`; 298 const rows = [ 299 ['Emprise au sol max', zone.emprise], 300 ['Espaces verts min', zone.espaces_verts], 301 ['Hauteur construction principale', zone.hauteur_principale], 302 ['Hauteur annexes', zone.hauteur_annexe], 303 ['Recul par rapport à la voie', zone.recul_voie_principale], 304 ['Recul voie (annexes)', zone.recul_voie_annexe],
305 ['Recul voie (abri jardin)', zone.recul_voie_abri], 306 ['Recul limites séparatives', zone.recul_sep], 307 ['Toiture principale', zone.toiture_principale], 308 ['Toiture annexe > 30 m²', zone.toiture_annexe_sup30], 309 ['Toiture annexe < 30 m²', zone.toiture_annexe_inf30], 310 ['Clôture sur voie', zone.cloture_voie], 311 ['Clôture limite séparative', zone.cloture_sep], 312 ['Carrefour', zone.cloture_carrefour], 313 ['DP clôture obligatoire', zone.cloture_dp ? 'OUI â Déclaration Préalable obligatoire' : null], 314 ['Portail (Ub1)', zone.cloture_portail], 315 ['Abri jardin â matériaux', zone.abri_materiaux], 316 ['Piscine', zone.piscine], 317 ['Façades', zone.facades], 318 ['Niveaux autorisés', zone.niveaux], 319 ['Stationnement', zone.stationnement], 320 ['Changement de destination', zone.changement_destination], 321 ['Plancher RDC / sol', zone.rdc_max], 322 ].filter(([,v]) => v); 323 if (rows.length) { 324 h += '<table class="plu-table">'; 325 for (const [label, val] of rows) { 326 const cls = (String(val).includes('INTERDIT') || String(val).includes('â') || String(val).includes('ð«')) ? ' plu-nok' : (String(val).includes('â ')) ? ' plu-ok' : ''; 327 h += `<tr><td class="plu-label">${label}</td><td class="plu-val${cls}">${val}</td></tr>`; 328 } 329 h += '</table>'; 330 } 331 if (zone.interdictions && zone.interdictions.length) { 332 h += '<div class="plu-interdits-title">ð« Interdictions</div><ul class="plu-interdits">'; 333 zone.interdictions.forEach(i => h += `<li>${i}</li>`); 334 h += '</ul>'; 335 } 336 return h; 337} 338 339function pluBuildAnswer(qid, zone, zoneKey) { 340 // Retourne un objet {html, hasPluAnswer} 341 // hasPluAnswer=true si on a une réponse précise du JSON, false si fallback MEL 342 const z = zone; 343 const zKey = pluNormalizeZone(zoneKey) || zoneKey; 344 let html = ''; 345 let hasPluAnswer = false; 346 347 if (qid === 'piscine') { 348 hasPluAnswer = true; 349 // Autorisation selon surface 350 html += pluRenderAuth('piscine'); 351 // Règle spécifique à la zone 352 if (z && z.piscine) { 353 const ok = z.piscine.includes('â '); 354 const nok = z.piscine.includes('â'); 355 html += `<div class="plu-zone-rule ${ok?'plu-ok-block':nok?'plu-nok-block':'plu-warn-block'}"> 356 <strong>En zone ${zoneKey} :</strong> ${z.piscine} 357 ${z.emprise ? '<br><small>Emprise totale (maison + piscine) : '+z.emprise+'</small>' : ''} 358 ${z.espaces_verts ? '<br><small>Espaces verts obligatoires : '+z.espaces_verts+'</small>' : ''} 359 </div>`; 360 } 361 if (z && z.alerte) html += `<div class="plu-alerte">${z.alerte}</div>`; 362 } 363 else if (qid === 'cloture') { 364 hasPluAnswer = true; 365 html += pluRenderAuth('cloture'); 366 if (z) { 367 html += `<div class="plu-zone-rule plu-warn-block"><strong>En zone ${zoneKey} :</strong><br>`; 368 if (z.cloture_voie) html += `⢠Sur voie : ${z.cloture_voie}<br>`; 369 if (z.cloture_sep) html += `⢠Limite séparative : ${z.cloture_sep}<br>`; 370 if (z.cloture_carrefour) html += `⢠Carrefour : ${z.cloture_carrefour}<br>`; 371 if (z.cloture_portail) html += `⢠Portail : ${z.cloture_portail}`; 372 html += '</div>'; 373 } 374 if (z && z.alerte) html += `<div class="plu-alerte">${z.alerte}</div>`; 375 } 376 else if (qid === 'abri') { 377 hasPluAnswer = true; 378 html += pluRenderAuth('abri'); 379 if (z) { 380 html += `<div class="plu-zone-rule plu-warn-block"><strong>En zone ${zoneKey} â matériaux :</strong><br>`; 381 if (z.abri_materiaux) html += z.abri_materiaux; 382 if (z.recul_voie_abri) html += `<br>Recul par rapport à la voie : ${z.recul_voie_abri}`; 383 html += '</div>'; 384 } 385 if (z && z.alerte) html += `<div class="plu-alerte">${z.alerte}</div>`; 386 } 387 else if (qid === 'toiture') { 388 hasPluAnswer = true; 389 html += pluRenderAuth('toiture'); 390 if (z) { 391 html += `<div class="plu-zone-rule plu-warn-block"><strong>En zone ${zoneKey} :</strong><br>`; 392 if (z.toiture_principale) html += `⢠Construction principale : ${z.toiture_principale}<br>`; 393 if (z.toiture_annexe_sup30) html += `⢠Annexe > 30 m² : ${z.toiture_annexe_sup30}<br>`; 394 if (z.toiture_annexe_inf30) html += `⢠Annexe < 30 m² : ${z.toiture_annexe_inf30}`; 395 html += '<br><small>Exception : appentis, vérandas, verrières non soumis à ces règles.</small></div>'; 396 } 397 if (z && z.alerte) html += `<div class="plu-alerte">${z.alerte}</div>`; 398 } 399 else if (qid === 'extension') { 400 hasPluAnswer = true; 401 html += pluRenderAuth('extension'); 402 if (z) { 403 html += `<div class="plu-zone-rule plu-warn-block"><strong>En zone ${zoneKey} :</strong><br>`; 404 if (z.emprise) html += `⢠Emprise au sol max (maison + extension) : ${z.emprise}<br>`; 405 if (z.hauteur_principale) html += `⢠Hauteur max : ${z.hauteur_principale}<br>`; 406 if (z.recul_voie_principale) html += `⢠Recul voie : ${z.recul_voie_principale}<br>`; 407 if (z.recul_sep) html += `⢠Limites séparatives : ${z.recul_sep}`; 408 if (['Ah','Nh'].includes(zoneKey)) html += '<br>â ï¸ Extension limitée à +20% de la surface de plancher existante.'; 409 html += '</div>'; 410 } 411 if (z && z.alerte) html += `<div class="plu-alerte">${z.alerte}</div>`; 412 } 413 else if (qid === 'permis') { 414 hasPluAnswer = true; 415 html += pluRenderAuth('permis'); 416 if (z) { 417 html += `<div class="plu-zone-rule plu-warn-block"><strong>Règles générales en zone ${zoneKey} :</strong><br>`; 418 if (z.emprise) html += `⢠Emprise au sol max : ${z.emprise}<br>`; 419 if (z.hauteur_principale) html += `⢠Hauteur max : ${z.hauteur_principale}<br>`; 420 if (z.recul_voie_principale) html += `⢠Recul voie : ${z.recul_voie_principale}<br>`; 421 if (z.recul_sep) html += `⢠Limites séparatives : ${z.recul_sep}`; 422 if (z.alerte) html += '<br>'+z.alerte; 423 html += '</div>'; 424 } 425 } 426 else if (qid === 'changement_destination') { 427 hasPluAnswer = true; 428 429 html += `<div class="plu-auth"> 430 <div class="plu-auth-title">ð Changement de destination</div> 431 <div class="plu-zone-rule plu-warn-block"> 432 Le changement de destination consiste à faire passer un bâtiment dâune destination à une autre 433 (par exemple : agricole vers habitation, habitation vers commerce, local vers bureaux). 434 <br><br> 435 <strong>Autorisation :</strong><br> 436 ⢠Sans modification de façade ni de structure porteuse : <strong>Déclaration préalable (DP)</strong><br> 437 ⢠Avec modification de façade et/ou de structure porteuse : <strong>
437Permis de construire (PC)</strong> 438 </div> 439 </div>`; 440 441 if (zoneKey === 'A') { 442 html += `<div class="plu-zone-rule plu-nok-block"> 443 <strong>En zone A :</strong><br> 444 Le changement de destination nâest autorisé que pour les <strong>bâtiments agricoles identifiés</strong> 445 dans les documents graphiques du PLU.<br> 446 Si le bâtiment nâest pas identifié, le changement de destination nâest pas autorisé en lâétat. 447 </div>`; 448 } 449 else if (zoneKey === 'Ah') { 450 html += `<div class="plu-zone-rule plu-ok-block"> 451 <strong>En secteur Ah :</strong><br> 452 Le changement de destination des constructions existantes est autorisé vers :<br> 453 ⢠lâhabitat<br> 454 ⢠les bureaux<br> 455 ⢠le commerce<br> 456 ⢠lâartisanat<br> 457 ⢠le tourisme<br> 458 ⢠les loisirs<br><br> 459 Le projet doit rester compatible avec le voisinage en termes de nuisances et dâaspect extérieur. 460 </div>`; 461 } 462 else if (zoneKey === 'Nh') { 463 html += `<div class="plu-zone-rule plu-ok-block"> 464 <strong>En secteur Nh :</strong><br> 465 Le changement de destination des constructions existantes est autorisé vers :<br> 466 ⢠lâhabitat<br> 467 ⢠le tourisme<br> 468 ⢠les loisirs<br><br> 469 Le projet doit rester compatible avec le caractère de la zone. 470 </div>`; 471 } 472 else { 473 html += `<div class="plu-zone-rule plu-warn-block"> 474 <strong>En zone ${zoneKey || '?' } :</strong><br> 475 Le PLU ne prévoit pas ici, dans les extraits intégrés à MEL, de règle spéciale aussi explicite que pour 476 les secteurs A, Ah ou Nh.<br> 477 Le projet doit donc être vérifié au cas par cas par le service urbanisme, selon la destination actuelle 478 du bâtiment, la destination envisagée et les éventuelles modifications extérieures ou structurelles. 479 </div>`; 480 } 481 482 html += `<div class="plu-note"> 483 â¹ï¸ Avant dépôt, vérifiez aussi si le bâtiment est situé dans un lotissement, sâil est repéré au PLU, 484 ou si dâautres contraintes sâappliquent. 485 </div>`; 486 } 487 // Footer commun : contact mairie 488 if (hasPluAnswer) { 489 html += `<div class="plu-footer"> 490 â ï¸ Informations issues du PLU approuvé le 30/01/2013 â à titre indicatif.<br> 491 Toute décision définitive relève du service urbanisme. 492 <a href="mailto:[email protected]" onclick="event.stopPropagation();window.location.href='mailto:[email protected]';return false;" style="display:block;margin-top:6px;padding:8px 12px;background:var(--forest);color:white;border-radius:8px;text-decoration:none;font-weight:700;text-align:center;">âï¸ Contacter le service urbanisme</a> 493 <a href="tel:0238456176" onclick="event.stopPropagation();window.location.href='tel:0238456176';return false;" style="display:block;margin-top:5px;padding:8px 12px;background:var(--leaf);color:white;border-radius:8px;text-decoration:none;font-weight:700;text-align:center;">ð Mairie : 02 38 45 61 76</a> 494 </div>`; 495 } 496 497 return { html, hasPluAnswer }; 498} 499 500// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 501// MEL â Arbre de décision (fallback embarqué) 502// â ï¸ Ne pas modifier ce bloc : éditez data/mel-tree.json à la place 503// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 504const _MEL_TREE_FALLBACK = { 505 506 urbanisme:{ 507 label:"Urbanisme & Construction", ico:"ðï¸", needZone:true, 508 questions:[ 509 510 {id:"piscine",ico:"ð",label:"Piscine",prompt:"Je souhaite construire une piscine. Quelles sont les règles PLU applicables à mon terrain ?",topic:"urbanisme"}, 511 {id:"cloture",ico:"ð²",label:"Clôture / Mur / Portail",prompt:"Je souhaite installer une clôture, un mur ou un portail. Quelles sont les règles et formalités ?",topic:"urbanisme"}, 512 {id:"extension",ico:"ð",label:"Extension / Agrandissement",prompt:"Je souhaite agrandir ma maison. Quelles sont les règles d'extension applicables ?",topic:"urbanisme"}, 513 {id:"abri",ico:"ð¿",label:"Abri de jardin / Garage / Annexe",prompt:"Je souhaite construire un abri de jardin, un garage ou une dépendance. Quelles sont les règles ?",topic:"urbanisme"}, 514 {id:"toiture",ico:"ð ",label:"Toiture / Façade / Matériaux",prompt:"Je souhaite modifier la toiture ou la façade de ma maison. Quelles sont les règles applicables ?",topic:"urbanisme"}, 515 ] 516 }, 517 518 enfance:{ 519 label:"Enfance & Jeunesse", ico:"ð", needZone:false, 520 questions:[ 521 // â ï¸ Source : règlement intérieur scolaire et périscolaire 2026-2027 de 522 // l'école de la Forêt. Deux garde-fous, verrouillés par 523 // tests/e2e/ecole-periscolaire.spec.js : 524 // ⢠AUCUN nom de personnel â pas même en commentaire. Ce bloc nommait 525 // la directrice de l'école et la directrice du périscolaire : des
526 // personnes qui changent d'une rentrée à l'autre, et rien ne le 527 // signalait. On désigne une fonction, jamais quelqu'un. 528 // ⢠AUCUN montant, pas même en commentaire. La ligne cantine affichait 529 // encore, en septembre 2026, une grille tarifaire de quatre ans son 530 // aînée â datée dans le texte, et personne ne l'avait vue. Les tarifs 531 // sont votés chaque année par le conseil municipal et calculés sur le 532 // quotient familial CAF : ils vivent sur le portail parents, pas ici. 533 // Même règle que la salle communale et la crèche. 534 {id:"ecole",ico:"ð",label:"Ãcole de la Forêt", 535 directAnswer:{text:"L'école de la Forêt est l'école primaire publique de la commune, 36 rue du Bourg. La semaine est de 4 jours â lundi, mardi, jeudi et vendredi â de 8h30 à 11h45 et de 13h45 à 16h30 ; il n'y a pas de classe le mercredi. L'accueil des élèves est assuré dix minutes avant la classe, à 8h20 et à 13h35, et le portail ferme à 8h30 et à 13h45. Des activités pédagogiques complémentaires (APC) peuvent s'ajouter pour les élèves de la GS au CM2, les lundis et jeudis sur le temps méridien. Toute absence est à signaler le jour même en précisant le motif, par téléphone, par mail ou dans le cahier de liaison. Aucun médicament n'est administré à l'école, sauf dans le cadre d'un PAI ; seul le traitement contre l'asthme est admis, avec copie de l'ordonnance. Votre premier interlocuteur est l'enseignante de votre enfant ; pour une question administrative, adressez-vous à la direction de l'école.", 536 links:[{label:"ð Ãcole : 02 38 45 65 00",tel:"0238456500"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 537 }, 538 {id:"periscolaire",ico:"ð ",label:"Accueil périscolaire", 539 directAnswer:{text:"Les services périscolaires â « L'Accueil enchanté », chemin des écoliers â sont gérés par la commune. L'accueil du matin se fait de 7h30 à 8h20 en arrivée échelonnée, celui du soir de 16h30 à 18h30, avec le goûter fourni, les lundi, mardi, jeudi et vendredi. Tout passe par le portail parents : dossier d'inscription unique pour tous les services périscolaires (copie des vaccins, attestation d'assurance mentionnant la responsabilité civile et l'individuelle accident), réservations, annulations, mise à jour des personnes autorisées à venir chercher l'enfant, paiement par carte. Les réservations et annulations du matin et du soir se font au plus tard 3 jours avant ; passé ce délai, l'inscription se fait par mail et un surcoût est facturé. Au-delà de 18h30, un retard est facturé par enfant. Les tarifs sont fixés chaque année par délibération du conseil municipal et calculés sur le quotient familial CAF au 1er août : la grille en vigueur est sur le portail parents.", 540 links:[{label:"ð Portail parents",url:"https://parents.logiciel-enfance.fr/mezieres-lez-clery"},{label:"ð Périscolaire : 09 67 28 01 20",tel:"0967280120"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 541 }, 542 {id:"mercredi",ico:"ðï¸",label:"Accueil du mercredi", 543 directAnswer:{text:"L'accueil périscolaire du mercredi est ouvert tous les mercredis en période scolaire, de 7h30 à 18h00, en journée entière ou en demi-journée, avec ou sans repas. Les arrivées du matin se font entre 7h30 et 9h00, celles de l'après-midi entre 11h50 et 12h00 avec repas ou entre 13h15 et 13h30 sans repas ; les départs de fin de journée à partir de 16h00. Les réservations et annulations se font sur le portail parents, au plus tard le mardi de la semaine précédente ; passé ce délai, l'inscription se fait par mail et un surcoût est facturé. Le goûter est fourni. Au-delà de 18h00 le mercredi, un retard est facturé par enfant. Les tarifs suivent le quotient familial CAF et sont fixés chaque année par délibération du conseil municipal.", 544 links:[{label:"ð Portail parents",url:"https://parents.logiciel-enfance.fr/mezieres-lez-clery"},{label:"ð Périscolaire : 09 67 28 01 20",tel:"0967280120"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 545 }, 546 {id:"cantine",ico:"ð½ï¸",label:"Restaurant scolaire", 547 directAnswer:{text:"Le restaurant scolaire est ouvert les lundi, mardi, jeudi et vendredi de 11h45 à 13h30. Les repas â une centaine par jour â sont confectionnés sur place par un cuisinier communal, qui élabore les menus en veillant à l'équilibre nutritionnel et en privilégiant les circuits courts ; prévoyez une serviette de table dans une trousse marquée au nom de l'enfant. Seuls les enfants venus en classe le matin sont admis, et aucun départ ni retour n'est accepté entre 11h45 et 13h30. L'inscription et les réservations se font sur le portail parents, au plus tard le mardi de la semaine précédente ; passé ce délai, l'inscription se fait par mail et un surcoût est facturé. En cas de régime alimentaire médical, un PAI doit être établi et réactualisé chaque rentrée : la commune ne réalise pas de menu spécifique et s'en tient à la neutralité religieuse en matière de repas. Les tarifs suivent le quotient familial CAF au 1er août et sont fixés chaque année par délibération du conseil municipal : la grille en vigueur est sur le portail parents.", 548 links:[{label:"ð Portail parents",url:"https://parents.logiciel-enfance.fr/mezieres-lez-clery"},{label:"ð Périscolaire : 09 67 28 01 20",tel:"0967280120"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 549 }, 550 {id:"creche",ico:"ð¶",label:"Crèche familiale Les Marmousets", 551 // â ï¸ Chiffres tirés du règlement de fonctionnement « parents » 2026-2027 552 // du SIVU (modifications au 1er septembre 2026). Deux erreurs y ont été 553 // corrigées : « 17 assistantes maternelles » (le règlement en compte 16) 554 // et « enfants de moins de 6 ans » â la crèche prend de 10 semaines à 555 // l'entrée en maternelle ; les « moins de six ans » du règlement ne 556 // visent que les places garanties de l'article D.214-7. Ne pas rétablir. 557 directAnswer:{text:"C'est une crèche « familiale » : 16 assistantes maternelles salariées accueillent les enfants à leur domicile, avec des temps collectifs chaque semaine dans les locaux de la crèche ou en salle communale. Elle accueille les enfants de 10 semaines jusqu'à leur entrée
557à l'école maternelle ; les enfants déjà scolarisés peuvent l'être le mercredi et pendant les vacances, selon les places disponibles. Ouverte du lundi au vendredi de 7h30 à 18h30, au 92 rue du Maréchal Foch à Cléry-Saint-André. Elle est gérée par un syndicat intercommunal (SIVU) qui regroupe Cléry-Saint-André, Mareau-aux-Prés et Mézières-lez-Cléry. L'inscription se fait en liste d'attente, puis une commission d'attribution statue sur les places ; aucune condition d'activité professionnelle des parents n'est exigée. Le tarif est calculé selon le barème national de la CNAF, d'après vos revenus et le nombre d'enfants à charge.", 558 links:[{label:"ð Crèche Les Marmousets : 02 38 45 76 56",tel:"0238457656"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 559 }, 560 // â ï¸ Doublon volontaire avec data/mel-tree.json (l'arbre éditable depuis 561 // l'admin) : les deux copies doivent rester en phase. Le LAEP n'est PAS 562 // un mode de garde et ne passe PAS par Mézières â ne pas « simplifier ». 563 {id:"laep",ico:"ð¤±",label:"LAEP â Lieu d'Accueil Enfants-Parents", 564 directAnswer:{text:"Un lieu convivial d'écoute, d'échanges, de jeux et de rencontres, ouvert par la Communauté de Communes à compter du 7 septembre 2026. Gratuit, confidentiel, sans inscription. Pour les enfants de moins de 6 ans accompagnés d'un parent, d'un grand-parent ou d'un adulte référent, et pour les futurs parents. Ce n'est pas un mode de garde : l'adulte reste avec l'enfant pendant toute la durée de l'accueil. Au moins deux professionnels, les « accueillants », sont présents et tenus au secret professionnel. Le LAEP est itinérant : il se déplace sur Beauce la Romaine, Beaugency, Cléry-Saint-André et Meung-sur-Loire â pas à Mézières, Cléry-Saint-André étant la commune d'accueil la plus proche. Le planning des créneaux n'est pas encore publié : renseignements auprès de la Communauté de Communes.", 565 links:[{label:"ð LAEP : 06 62 65 59 04",tel:"0662655904"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"},{label:"ð Le LAEP sur le site de la CCTVL",url:"https://www.ccterresduvaldeloire.fr/laep-lieu-accueil-enfants-parents/"}]} 566 }, 567 {id:"loisirs",ico:"ð¨",label:"Centre de loisirs", 568 directAnswer:{text:"L'accueil de loisirs sans hébergement (ALSH) du Val d'Ardoux, organisé par la Communauté de Communes des Terres du Val de Loire, accueille les 3-13 ans pendant les vacances scolaires â à l'exception des vacances de Noël. Il est basé à Cléry-Saint-André, et ponctuellement à Mézières-lez-Cléry. 1ʳᵠinscription : dossier à retirer auprès de la CCTVL. Réinscription : dossier pré-rempli par email. Vacances 2026-2027 : automne du 19 octobre au 1er novembre 2026, hiver du 22 février au 7 mars 2027, printemps du 19 avril au 2 mai 2027, été à partir du 5 juillet 2027 â sous réserve du maintien du calendrier scolaire.", 569 links:[{label:"âï¸ [email protected]",url:"mailto:[email protected]"},{label:"ð Site CCTVL",url:"https://www.ccterresduvaldeloire.fr/listes/enfance-jeunesse/"}]} 570 }, 571 ] 572 }, 573 574 administratif:{ 575 label:"Démarches Administratives", ico:"ð", needZone:false, 576 questions:[ 577 {id:"etatcivil",ico:"ð",label:"Ãtat civil (naissance, mariage, décès, actes)", 578 directAnswer:{text:"Les actes d'état civil (naissance, mariage, décès, acte de naissanceâ¦) sont délivrés par la mairie. Accueil physique : lundi 14h-17h30, vendredi 8h30-11h30. Mercredi sur rendez-vous uniquement.", 579 links:[{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 580 }, 581 {id:"cni",ico:"ðªª",label:"Carte Nationale d'Identité", 582 directAnswer:{text:"La CNI se fait dans une mairie équipée d'un dispositif biométrique (la mairie de Mézières vous orientera vers la plus proche, souvent Cléry-Saint-André). Démarche en ligne sur le site ANTS : créer un compte â commencer la démarche â renseigner vos informations â acheter votre timbre fiscal â valider â prendre rendez-vous en mairie â préparer vos pièces justificatives â déposer le dossier en mairie.", 583 links:[{label:"ð Démarche CNI sur ANTS",url:"https://ants.gouv.fr"},{label:"ð Prendre rendez-vous",url:"https://www.service-public.gouv.fr/particuliers/vosdroits/R63715"},{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"}]} 584 }, 585 {id:"passeport",ico:"ð",label:"Passeport", 586 directAnswer:{text:"Le passeport se fait dans une mairie équipée d'un dispositif biométrique. Vous pouvez faire une pré-demande sur ants.gouv.fr. Documents nécessaires selon la situation : première demande ou renouvellement, majeur ou mineur. Un t
586imbre fiscal est requis. Le délai varie selon la période et le lieu.", 587 links:[{label:"ð Toutes les infos passeport",url:"https://www.service-public.gouv.fr/particuliers/vosdroits/N360"},{label:"ð Pré-demande sur ANTS",url:"https://ants.gouv.fr"},{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"}]} 588 }, 589 {id:"pacs",ico:"ð",label:"PACS â Pacte Civil de Solidarité", 590 directAnswer:{text:"Le PACS se déclare à la mairie. Il faut remplir la déclaration conjointe (cerfa 15725*03) avec attestations sur l'honneur de non-parenté, non-alliance et résidence commune, joindre une convention-type (cerfa 15726*02) ou une convention personnalisée, et les justificatifs d'identité.", 591 links:[{label:"ð Déclaration PACS (cerfa 15725*03)",url:"https://www.formulaires.service-public.gouv.fr/gf/cerfa_15725.do"},{label:"ð Convention-type PACS (cerfa 15726*02)",url:"https://www.formulaires.service-public.gouv.fr/gf/cerfa_15726.do"},{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"}]} 592 }, 593 {id:"vote",ico:"ð³ï¸",label:"Inscription sur les listes électorales", 594 directAnswer:{text:"Pour voter, vous devez être inscrit sur les listes électorales. Inscription en mairie ou en ligne sur service-public.gouv.fr. Documents à prévoir : pièce d'identité et justificatif de domicile.", 595 links:[{label:"ð Formulaire d'inscription (cerfa 12669)",url:"https://www.service-public.gouv.fr/particuliers/vosdroits/R16024"},{label:"ð Inscription en ligne",url:"https://www.service-public.gouv.fr"},{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"}]} 596 }, 597 {id:"assainissement",ico:"ð°",label:"Assainissement (fosse, raccordement, SPANC)", 598 directAnswer:{text:"L'assainissement collectif et le SPANC (Service Public d'Assainissement Non Collectif, ANC) sont gérés par la CCTVL depuis 2018. Pour un raccordement, un contrôle de fosse ou toute question, contactez directement la CCTVL. La facturation est accessible sur le portail usagers.", 599 links:[{label:"ð CCTVL : 02 38 44 59 35",tel:"0238445935"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"},{label:"ð Portail usagers CCTVL",url:"https://portail-usagers.ccterresduvaldeloire.fr"}]} 600 }, 601 {id:"location",ico:"ðª",label:"Location de matériel communal", 602 directAnswer:{text:"â ï¸ La salle communale (salle des fêtes) n'est plus proposée à la location : ni pour une fête de famille, ni pour un événement privé. En revanche, la commune met du matériel à disposition des Macériens (tables, chaises, barnumsâ¦), avec un chèque de caution restitué en fin de location si aucune dégradation n'est constatée. Pour connaître le matériel disponible, les tarifs et les modalités de réservation, contactez la mairie.", 603 links:[{label:"ð Mairie : 02 38 45 61 76",tel:"0238456176"},{label:"âï¸ [email protected]",url:"mailto:[email protected]"}]} 604 }, 605 ] 606 }, 607 608 dechets:{ 609 label:"Déchets & Propreté", ico:"â»ï¸", needZone:false, 610 questions:[ 611 {id:"collecte",ico:"ðï¸",label:"Jours de collecte ordures / tri sélectif", 612 directAnswer:{text:"Sortez vos bacs la veille au soir ou avant 7h le matin du jour de collecte. La collecte n'a pas lieu les jours fériés â elle est reportée au lendemain. Des points d'apport volontaire (verre, papiers, plastique-cartons, vêtements) sont présents sur la commune. Consultez votre calendrier personnalisé en ligne.", 613 links:[{label:"ðï¸ Mon calendrier de collecte",action:"openDechets()"}]} 614 }, 615 {id:"dechetterie",ico:"â»ï¸",label:"Déchetterie â horaires & inscription", 616 directAnswer:{text:"La déchetterie de Cléry-Saint-André est la plus proche. Inscription OBLIGATOIRE (lecture automatique de plaque). Enregistrez vos plaques SANS tiret (ex : AA123BB). Horaires : hiver (oct-mars) 10h-12h / 14h-17h, été (avr-sep) 9h-12h / 14h-18h, lun-sam sauf jours fériés. Une inscription vaut pour tous les sites CCTVL.", 617 links:[{label:"ð S'inscrire à la déchetterie",url:"https://portail-usagers.ccterresduvaldeloire.fr"},{label:"ð CCTVL : 02 38 44 59 35",tel:"0238445935"}
617]} 618 }, 619 {id:"encombrants",ico:"ðï¸",label:"Encombrants / objets volumineux", 620 directAnswer:{text:"Les encombrants (meubles, électroménager, gros objets) se déposent à la déchetterie de Cléry-Saint-André. Inscription préalable obligatoire sur le portail usagers CCTVL. Certains équipements électriques peuvent être repris en magasin (obligation légale de reprise 1 pour 1).", 621 links:[{label:"ð Portail usagers CCTVL",url:"https://portail-usagers.ccterresduvaldeloire.fr"},{label:"ð CCTVL : 02 38 44 59 35",tel:"0238445935"}]} 622 }, 623 {id:"tri",ico:"ð¦",label:"Tri sélectif â que mettre où ?", 624 directAnswer:{text:"Sur la commune : bac jaune (plastiques, cartons, papiers, métaux), bac vert (ordures résiduelles). Points d'apport volontaire pour le verre, les papiers-magazines, plastique-cartons et vêtements. En cas de doute sur un déchet, consultez le guide du tri en ligne.", 625 links:[{label:"ð Guide du tri CCTVL",url:"https://www.ccterresduvaldeloire.fr/reseau-des-dechetteries/"}]} 626 }, 627 ] 628 }, 629 630 numerique:{ 631 label:"Numérique & Internet", ico:"ð¡", needZone:false, 632 questions:[ 633 // â ï¸ Lysséo n'est pas un fournisseur d'accès : c'est le réseau public fibre 634 // du Loiret, construit et exploité par Loiret THD / Loiret Fibre (groupe 635 // XpFibre) en délégation de service public du Département. Il ne vend aucun 636 // abonnement. Ne pas réécrire ces réponses comme si l'habitant pouvait 637 // « souscrire chez Lysséo », ni renvoyer vers la page d'accueil en décrivant 638 // un bouton « en haut à droite » : ces instructions de navigation cassent 639 // au premier changement de menu du site. Pointer les URL directes. 640 {id:"fibre",ico:"ð",label:"Raccordement fibre / Nouvelle construction", 641 directAnswer:{text:"Lysséo est le réseau public fibre du Loiret : il construit et gère l'infrastructure, mais ne vend aucun abonnement â les forfaits affichés sur son site sont ceux des opérateurs commerciaux présents sur le réseau.\n\n1ï¸â£ Déclarez d'abord votre construction neuve. Une maison neuve n'est pas automatiquement présente dans la base d'adresses de l'opérateur d'infrastructure : tant que la vôtre n'y figure pas comme « raccordable », aucun opérateur ne pourra enregistrer votre commande. La démarche se fait auprès de XpFibre / Loiret THD, ou via le formulaire de contact Lysséo. Prévoyez de joindre votre permis de construire, votre certificat de numérotation et un plan de masse localisant le regard et les fourreaux en limite de propriété.\n\n2ï¸â£ Une fois l'adresse déclarée éligible, souscrivez auprès de l'opérateur de votre choix (Orange, SFR, Bouygues, Freeâ¦). C'est son prestataire qui réalise le tirage de la fibre depuis le point de branchement de la voirie jusqu'au logement, en empruntant vos fourreaux existants. Aucun génie civil supplémentaire n'est à prévoir de votre côté, sous réserve que les fourreaux soient bien aiguillés et non obstrués â leur bon état relève du propriétaire sur la partie privative.\n\n3ï¸â£ De son côté, la mairie vérifie que la numérotation de votre parcelle a bien été transmise et intégrée à la Base Adresse Nationale : c'est la condition nécessaire à la prise en compte de votre adresse par l'opérateur d'infrastructure.", 642 links:[{label:"ðï¸ Déclarer une nouvelle construction â XpFibre / Loiret THD",url:"https://www.xpfibre.com/loiret-thd"}, 643 {label:"ð Lysséo â site officiel",url:"https://lysseo.fr"}, 644 {label:"ðï¸ Mairie : 02 38 45 61 76",tel:"0238456176"}]} 645 }, 646 {id:"thd",ico:"ð¶",label:"Questions fibre (raccordement, éligibilitéâ¦)", 647 directAnswer:{text:"Pour une question portant sur le réseau lui-même â adresse raccordable, avancement du déploiement, point de branchement â écrivez à Lysséo via son formulaire de contact.\n\nâ ï¸ Lysséo ne commercialise aucune offre : pour un abonnement, une facture, un rendez-vous d'installation ou une panne de votre ligne, votre interlocuteur est votre opérateur (Orange, SFR, Bouygues, Freeâ¦).", 648 links:[{label:"ð Lysséo â site officiel",url:"https://lysseo.fr"}]} 649 }, 650 {id:"probleme",ico:"ð§",label:"Problème sur les installations (armoires, câblesâ¦)", 651 directAnswer:{text:"En cas de problème constaté sur les équipements du réseau public (armoires de rue, boîtiers, câbles arrachés), remplissez le formulaire de déclaration de dommage réseau de Lysséo.", 652 links:[{label:"ð ï¸ Déclarer un dommage réseau",url:"https://lysseo.fr/declaration-dommages-reseaux/45"}]} 653 }, 654 ] 655 }, 656 657 autre:{label:"Autre question",ico:"ð¬",needZone:false,openChatDirectly:true,questions:[]} 658}; 659 660// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 661// PHASE 3 â Données MEL/PLU externalisées 662// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 663// PLU_DATA et MEL_TREE sont initialisées au fallback embarqué, 664// puis écrasées par les fichiers JSON externes dès qu'ils arrivent. 665// Avantage : MEL fonctionne IMMÃDIATEMENT au chargement du script, 666// sans attendre le fetch réseau. Le SW cache les JSON dès la 1ère
667// visite, donc à partir du 2e lancement tout est instantané. 668// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 669let PLU_DATA = _PLU_DATA_FALLBACK; 670let MEL_TREE = _MEL_TREE_FALLBACK; 671let _melDataLoaded = false; // true dès que les JSON externes sont chargés (info debug) 672 673async function loadMelData() { 674 const V = '3.7.6'; 675 676 try { 677 // allSettled : un fetch qui timeout/échoue n'invalide pas les deux autres, 678 // contrairement à Promise.all() où le 1er reject coupait toute la chaîne 679 // (le .catch sur le 3e fetch n'était qu'un cataplasme partiel). 680 const settled = await Promise.allSettled([ 681 matFetch('./data/plu-data.json?v=' + V, { cache: 'no-cache' }, 6000), 682 matFetch('./data/mel-tree.json?v=' + V, { cache: 'no-cache' }, 6000), 683 matFetch(MEL_BACKEND + '/mel/tree', { cache: 'no-cache' }, 6000) 684 ]); 685 const pluRes = settled[0].status === 'fulfilled' ? settled[0].value : null; 686 const treeRes = settled[1].status === 'fulfilled' ? settled[1].value : null; 687 const serverTreeRes = settled[2].status === 'fulfilled' ? settled[2].value : null; 688 689 if (pluRes && pluRes.ok) { 690 const plu = await pluRes.json(); 691 if (plu && plu.zones && Object.keys(plu.zones).length >= 5) { 692 PLU_DATA = plu; 693 } 694 } 695 696 let tree = null; 697 698 if (serverTreeRes && serverTreeRes.ok) { 699 const remote = await serverTreeRes.json(); 700 if (remote && remote.ok && remote.tree && typeof remote.tree === 'object' && Object.keys(remote.tree).length >= 1) { 701 tree = remote.tree; 702 } 703 } 704 705 if (!tree && treeRes && treeRes.ok) { 706 const localTree = await treeRes.json(); 707 if (localTree && typeof localTree === 'object' && Object.keys(localTree).length >= 3) { 708 tree = localTree; 709 } 710 } 711 712 if (tree) { 713 MEL_TREE = tree; 714 } 715 716 _melDataLoaded = true; 717 console.log('[MEL] Données chargées : ' + 718 Object.keys(PLU_DATA.zones || {}).length + ' zones PLU, ' + 719 Object.keys(MEL_TREE || {}).length + ' catégories MEL'); 720 } catch (e) { 721 console.warn('[MEL] Fallback embarqué utilisé :', e.message || e); 722 } 723} 724 725function _melEsc(v){ 726 if (typeof esc === 'function') return esc(v); 727 return String(v == null ? '' : v) 728 .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') 729 .replace(/"/g,'"').replace(/'/g,'''); 730} 731 732// Argument sûr pour un onclick="fn(â¦)" : JSON.stringify gère la couche JS 733// (quotes, backslash, retours ligne), puis " est encodé pour la couche 734// attribut HTML. à insérer SANS quotes additionnelles autour. 735function _melJsArg(v){ 736 return JSON.stringify(v == null ? '' : String(v)).replace(/"/g,'"'); 737} 738 739// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 740// MEL â Ãtat global du module 741// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 742let _melCat=null, _melZone=null, _melZoneLabel=null, _melAddr=null; 743let _melLat=null, _melLon=null; 744let _melChatContext='', _melChatCategory='autre', _melChatTopic='autre'; 745let _melZoneNorm=null; 746 747// Variables chat MEL 748let melInited=false, melBusy=false; 749const hist=[]; 750 751// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 752// MEL â Chat (addMsg, showTyping, sendMel) 753// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 754 755function addMsg(role,text){ 756 const c=document.getElementById('msgs'); 757 const d=document.createElement('div'); d.className='msg '+role; 758 // Double contexte : chaîne JS ('...') imbriquée dans un attribut HTML ("..."). 759 // 1) échappement JS (antislash, apostrophe, saut de ligne) ; 2) échappement 760 // HTML via _melEsc pour empêcher tout " ou < de casser l'attribut onclick. 761 const ttsArg = _melEsc(text.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,' ')); 762 const ttsBtn = (role==='bot' && _ttsEnabled) 763 ? '<button class="tts-btn" onclick="ttsRead(\''+ttsArg+'\',\'MEL\')">ð Ãcouter</button>' 764 : ''; 765 const formatted = formatMelText(text); 766 d.innerHTML='<div class="mav">
766'+(role==='bot'?'\ud83d\udc69':'\ud83d\udc64')+'</div><div class="bub">'+formatted+ttsBtn+'</div>'; 767 c.appendChild(d); c.scrollTop=c.scrollHeight; 768 // Lire automatiquement si TTS actif 769 if(role==='bot' && _ttsEnabled) ttsSpeak(text, 'MEL'); 770} 771 772function showTyping(){ 773 const c=document.getElementById('msgs'); 774 const d=document.createElement('div'); d.className='msg bot'; d.id='typ'; 775 d.innerHTML='<div class="mav">\ud83d\udc69</div><div class="bub"><div class="typ"><span></span><span></span><span></span></div></div>'; 776 c.appendChild(d); c.scrollTop=c.scrollHeight; 777} 778 779async function sendMel(){ 780 const inp=document.getElementById('minp'); 781 const txt=inp.value.trim(); if(!txt||melBusy)return; 782 inp.value=''; inp.placeholder='Votre questionâ¦'; const _sugs=document.getElementById('sugs'); if(_sugs)_sugs.style.display='none'; 783 addMsg('user',txt); hist.push({role:'user',content:txt}); melBusy=true; showTyping(); 784 try{ 785 const r=await fetch(MEL_PROXY,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:hist.slice(-8),category:_melChatCategory||'autre',extraCtx:_melChatContext||''}),signal:matAbortTimeout(20000)}); 786 if(!r.ok) throw new Error('HTTP '+r.status); 787 const d=await r.json(); 788 const rep=(d.reply||'Erreur technique. Contactez la mairie au 02 38 45 61 76.').replace(/\*\*(.*?)\*\*/g,'$1').replace(/\*(.*?)\*/g,'$1'); 789 var _typ=document.getElementById('typ'); if(_typ) _typ.remove(); hist.push({role:'assistant',content:rep}); addMsg('bot',rep); 790 if(d.showElus){ 791 const c=document.getElementById('msgs'); 792 const sug=document.createElement('div'); sug.className='msg bot'; 793 sug.innerHTML='<div class="mav">ð©</div><div class="bub" style="background:var(--mist);border:1px solid rgba(0,0,0,0.06);font-size:0.78rem;display:flex;align-items:center;gap:8px;padding:10px 12px;flex-wrap:wrap;">' 794 +'<span style="font-size:1.1rem;flex-shrink:0;">ðï¸</span>' 795 +'<span style="flex:1;color:var(--forest);min-width:100px;">Consulter la fiche ou contacter un élu ?</span>' 796 +'<button onclick="closeOv(\'mel\');openConseil();" style="background:var(--forest);color:white;border:none;border-radius:8px;padding:7px 12px;font-size:0.72rem;font-weight:900;cursor:pointer;flex-shrink:0;">ðï¸ Voir les élus</button>' 797 +'</div>'; 798 c.appendChild(sug); c.scrollTop=99999; 799 } 800 if(d.showUrbanisme){ 801 const c=document.getElementById('msgs'); 802 const sug=document.createElement('div'); sug.className='msg bot'; 803 sug.innerHTML='<div class="mav">ð©</div><div class="bub" style="background:var(--mist);border:1px solid rgba(0,0,0,0.06);padding:10px 12px;">' 804 +'<div style="font-size:0.72rem;font-weight:900;color:var(--forest);margin-bottom:6px">ðï¸ Schéma des autorisations d\'urbanisme</div>' 805 +'<img src="./img/autorisation-urbanisme.jpg?v=3.7.4" ' 806 +'alt="Autorisations urbanisme" ' 807 +'style="width:100%;border-radius:8px;display:block;margin-bottom:6px;cursor:zoom-in" ' 808 +'onclick="openMelImageModal(this.src, \'Schéma des autorisations d\\\'urbanisme\')" ' 809 +'onerror="this.style.display=\'none\'">' 810 +'<div style="font-size:0.67rem;color:var(--muted);line-height:1.5">DP = Déclaration Préalable · PC = Permis de Construire · SP = Surface Plancher</div>' 811 +'</div>'; 812 c.appendChild(sug); c.scrollTop=99999; 813 } 814 }catch(e){ var _typ=document.getElementById('typ'); if(_typ) _typ.remove(); addMsg('bot','Je ne peux pas répondre pour le moment.\nMairie\u00a0: 02\u00a038\u00a045\u00a061\u00a076 \ud83d\ude0a'); if(navigator.onLine && typeof window.matSignalServerError==='function') window.matSignalServerError(); } 815 melBusy=false; 816} 817 818// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 819// MEL â Suggestions thématiques (affichées en haut du chat) 820// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 821const MEL_THEME_SUGGESTIONS = { 822 urbanisme: [ 823 "Faut-il une déclaration préalable pour une clôture ?", 824 "Ai-je besoin d'un permis pour une extension ?", 825 "Comment trouver ma zone PLU sur le Géoportail ?", 826 "Quelles règles pour un abri de jardin en zone Ub ?", 827 "Quelle hauteur maximale pour construire en zone Ua ?", 828 "Puis-je construire une piscine sans permis ?", 829 "Quelles règles pour une véranda ou une terrasse ?", 830 "Qu'est-ce que la zone A agricole ?", 831 "Quelles sont les zones constructibles à Mézières ?", 832 ], 833 enfance: ["Comment fonctionne la cantine ?","Quels sont les services périscolaires ?","Comment s'inscrire au centre de loisirs ?","Quelles infos sur la crèche familiale ?"], 834 demarches: ["Comment demander un acte de naissance ?","Où faire une carte d'identité ?","Comment obtenir un passeport ?","Quelles démarches pour un mariage ?"], 835 fibre: ["Suis-je éligible à la fibre ?","Qui contacter pour le raccordement fibre ?","Que faire si la fibre n'est pas disponible ?","Où trouver les infos numériques utiles ?"], 836 cctvl: ["Comment m'inscrire à la déchetterie de Cléry ?","Quelle maison de santé est proche de Mézières ?","Comment bénéficier de l'OPAH pour rénover mon logement ?","Ma fosse septique doit-elle être contrôlée ?"] 837}; 838 839var MEL_ACTION_WHITELIST={openDechets:function(){typeof openDechets==='function'&&openDechets();},openAgenda:function(){typeof openAgenda==='function'&&openAgenda();},openNotifs:function(){typeof openNotifs==='function'&&openNotifs();}}; 840function melRunAction(name){var fn=MEL_ACTION_WHITELIST[name];if(fn)fn();} 841 842function renderMelThemes(){ 843 const sugs = document.getElementById('sugs'); 844 if(!sugs) return; 845 sugs.style.display=''; 846 sugs.innerHTML = '<button class="sug" onclick="showMelThemeQuestions(\'urbanisme\')">ðï¸ Urbanisme</button>' 847 + '<button class="sug" onclick="showMelThemeQuestions(\'enfance\')">ð§ Enfance</button>' 848 + '<button class="sug" onclick="showMelThemeQuestions(\'demarches\')">ð Démarches</button>' 849 + '<button class="sug" onclick="showMelThemeQuestions(\'fibre\')">ð Fibre</button>' 850 + '<button class="sug" onclick="showMelThemeQuestions(\'cctvl\')">ðï¸ CCTVL</button>'; 851} 852 853function showMelThemeQuestions(theme){ 854 const sugs = document.getElementById('sugs'); 855 const qs = MEL_THEME_SUGGESTIONS[theme] || []; 856 if(!sugs) return; 857 let extra = ''; 858 if(theme === 'urbanisme'){ 859 extra = '<a class="sug" href="https://www.geoportail-urbanisme.gouv.fr/map/#tile=1&lon=1.8048256197128292&lat=47.81814028325263&zoom=15&mlon=1.805022&mlat=47.818434" target="_blank" style="text-decoration:none">ðºï¸ Voir le PLU sur Géoportail â</a>'; 860 } 861 sugs.innerHTML = '<button class="sug" onclick="renderMelThemes()">â Thèmes</button>' 862 + qs.map(q => '<button class="sug" onclick="askMelSuggestedQuestion(this)">'+q+'</button>').join('') 863 + extra; 864} 865
866function askMelSuggestedQuestion(btn){ 867 document.getElementById('minp').value = btn.textContent.trim(); 868 sendMel(); 869} 870 871function sendSug(btn){ document.getElementById('minp').value=btn.textContent.trim(); sendMel(); } 872 873// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 874// MEL â Navigation arbre de décision 875// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 876 877function melShowTree(){ 878 const tree=document.getElementById('mel-tree'); 879 const l1=document.getElementById('mel-tree-l1'); 880 const l2=document.getElementById('mel-tree-l2'); 881 const msgs=document.getElementById('msgs'); 882 const sugs=document.getElementById('sugs'); 883 const mbar=document.querySelector('.mbar'); 884 const backBtn=document.getElementById('mel-back-btn'); 885 if(tree){tree.style.display='flex';tree.style.flexDirection='column';} 886 if(l1){ 887 l1.style.display='flex'; 888 l1.style.flexDirection='column'; 889 l1.innerHTML = Object.entries(MEL_TREE || {}).map(function(entry){ 890 var cat = entry[0], def = entry[1] || {}; 891 return '<button class="mel-cat-btn" onclick="melSelectCat(' + _melJsArg(cat) + ')"><span class="ico">' 892 + _melEsc(def.ico || 'ð¬') + '</span>' 893 + _melEsc(def.label || cat) + '</button>'; 894 }).join(''); 895 } 896 if(l2){l2.classList.remove('active');l2.style.display='none';l2.innerHTML='';} 897 if(msgs)msgs.style.display='none'; 898 if(sugs)sugs.style.display='none'; 899 if(mbar)mbar.style.display='none'; 900 const privacyHint=document.getElementById('mel-privacy-hint'); 901 if(privacyHint)privacyHint.style.display='none'; 902 if(backBtn)backBtn.remove(); 903 _melCat=null;_melZone=null;_melZoneLabel=null;_melAddr=null; 904 _melChatContext='';_melChatCategory='autre'; 905} 906 907function melSelectCat(cat){ 908 _melCat=cat; 909 const def=MEL_TREE[cat]; 910 if(!def)return; 911 if(def.openChatDirectly){ 912 melOpenChat('',cat,''); 913 return; 914 } 915 const l1=document.getElementById('mel-tree-l1'); 916 const l2=document.getElementById('mel-tree-l2'); 917 if(!l2){console.error('mel-tree-l2 introuvable');return;} 918 if(l1)l1.style.display='none'; 919 // Injecter le contenu AVANT d'afficher pour éviter le flash vide 920 l2.innerHTML=_buildSubLevel(cat,def); 921 l2.classList.add('active'); 922 l2.style.display='flex'; 923 l2.style.flexDirection='column'; 924 l2.scrollTop=0; 925} 926 927function _buildSubLevel(cat,def){ 928 let h=`<div class="mel-sub"><div class="mel-sub-hdr"><button class="mel-sub-back" onclick="melResetTree()">â Retour</button><span>${_melEsc(def.ico)} ${_melEsc(def.label)}</span></div>`; 929 if(def.needZone){ 930 h+=`<div class="mel-zone-block"><label>ð Votre adresse ou position GPS (pour identifier votre zone PLU)</label><div class="mel-zone-row"><input type="text" id="mel-addr-input" class="mel-addr-input" placeholder="Ex : 12 rue du Bourg" oninput="this.style.borderColor='';document.getElementById('mel-addr-warn')?.remove()" onkeydown="if(event.key==='Enter')melFindZoneByAddr()"/><button class="mel-addr-btn" onclick="melFindZoneByAddr()">ð</button><button class="mel-gps-btn" onclick="melFindZoneByGPS()" title="Utiliser mon GPS">ð</button></div><div id="mel-zone-result" class="mel-zone-result"></div></div>`; 931 h+=`<div class="mel-schema-block"> 932 <div class="mel-schema-label">ð Schéma des autorisations</div> 933 <img 934 src="./img/autorisation-urbanisme.jpg?v=3.7.4" 935 alt="Autorisations urbanisme" 936 class="mel-schema-img" 937 onclick="openMelImageModal(this.src, 'Schéma des autorisations d\\'urbanisme')" 938 style="cursor:zoom-in" 939 onerror="this.closest('.mel-schema-block').style.display='none'"/> 940</div>`; 941 } 942 for(const q of def.questions){ 943 h+=`<button class="mel-q-btn" onclick="melSelectQuestion(${_melJsArg(cat)},${_melJsArg(q.id)})"><span class="q-ico">${_melEsc(q.ico)}</span><span>${_melEsc(q.label)}</span></button>`; 944 } 945 h+=`<button class="mel-q-btn mel-autre" onclick="melSelectQuestion(${_melJsArg(cat)},'__autre__')"><span class="q-ico">ð¬</span><span>Autre question ou cas particulierâ¦</span></button>`; 946 h+=`</div>`; 947 return h; 948} 949 950function openMelImageModal(src, title){ 951 closeMelImageModal(); 952 953 const ov = document.createElement('div'); 954 ov.id = 'mel-image-modal'; 955 ov.style.cssText = `
956 position:fixed; inset:0; z-index:99999; 957 background:rgba(0,0,0,.82); 958 display:flex; align-items:center; justify-content:center; 959 padding:16px; 960 `; 961 ov.onclick = function(e){ 962 if(e.target === ov) closeMelImageModal(); 963 }; 964 965 ov.innerHTML = ` 966 <div style="position:relative;max-width:95vw;max-height:95vh;width:100%;display:flex;flex-direction:column;align-items:center;"> 967 <button onclick="closeMelImageModal()" style=" 968 position:absolute;top:-6px;right:-6px; 969 width:40px;height:40px;border:none;border-radius:999px; 970 background:white;color:#111;font-size:22px;font-weight:900;cursor:pointer; 971 box-shadow:0 4px 16px rgba(0,0,0,.35); 972 ">Ã</button> 973 <div style="color:white;font-weight:800;font-size:.95rem;margin-bottom:10px;text-align:center;">${title || ''}</div> 974 <img src="${src}" alt="${title || 'Image'}" style=" 975 max-width:100%; 976 max-height:calc(95vh - 50px); 977 border-radius:12px; 978 box-shadow:0 12px 40px rgba(0,0,0,.45); 979 background:white; 980 "> 981 </div> 982 `; 983 984 document.body.appendChild(ov); 985 document.body.style.overflow = 'hidden'; 986} 987 988function closeMelImageModal(){ 989 const el = document.getElementById('mel-image-modal'); 990 if(el) el.remove(); 991 document.body.style.overflow = ''; 992} 993 994function melSelectQuestion(cat,qid){ 995 const def=MEL_TREE[cat]; 996 if(!def)return; 997 998 // ââ Urbanisme : adresse obligatoire avant de continuer ââââââââââ 999 if(def.needZone && !_melAddr){ 1000 const inp=document.getElementById('mel-addr-input'); 1001 if(inp){ 1002 inp.style.borderColor='#e53e3e'; 1003 inp.placeholder='â ï¸ Saisissez votre adresse d\'abordâ¦'; 1004 inp.focus(); 1005 let warn=document.getElementById('mel-addr-warn'); 1006 if(!warn){ 1007 warn=document.createElement('div'); 1008 warn.id='mel-addr-warn'; 1009 warn.style.cssText='color:#e53e3e;font-size:.78rem;font-weight:700;margin-top:5px;padding:6px 10px;background:#fff5f5;border-radius:7px;'; 1010 inp.closest('.mel-zone-row').after(warn); 1011 } 1012 warn.textContent='â ï¸ Veuillez saisir votre adresse ou utiliser le GPS pour identifier votre zone PLU.'; 1013 } 1014 return; 1015 } 1016 1017 const zoneCtx=[ 1018 _melAddr ? 'Adresse : '+_melAddr+'.' : '', 1019 _melZone ? 'Zone PLU : '+_melZone+(_melZoneLabel?' ('+_melZoneLabel+')':'')+'.' : '' 1020 ].filter(Boolean).join(' '); 1021 1022 if(qid==='__autre__'){ 1023 melOpenChat('',cat,'Catégorie : '+def.label+'. '+zoneCtx); 1024 return; 1025 } 1026 const q=def.questions.find(x=>x.id===qid); 1027 if(!q)return; 1028 1029 // ââ Urbanisme : réponse directe depuis le JSON PLU âââââââââââââââ 1030 if(cat==='urbanisme' && _melZone){ 1031 const zoneData=pluGetZone(_melZone); 1032 const {html:pluHtml, hasPluAnswer}=pluBuildAnswer(qid, zoneData, _melZone); 1033 if(hasPluAnswer){ 1034 _showPluAnswer(pluHtml, q.label, cat, qid, zoneCtx); 1035 return; 1036 } 1037 } 1038 1039 // ââ Autres catégories ou cas non couverts : directAnswer ou MEL ââ 1040 if(q.directAnswer){ 1041 _showDirectAnswer(q.directAnswer,cat,q.topic||cat,zoneCtx+' '+(q.prompt||''),q.label); 1042 return; 1043 } 1044 const fullPrompt=[zoneCtx,q.prompt].filter(Boolean).join('\n'); 1045 melOpenChat(fullPrompt,cat,zoneCtx,q.topic||cat); 1046} 1047 1048// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1049// MEL â Affichage réponse PLU avec onglets 1050// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1051function _showPluAnswer(pluHtml, questionLabel, cat, qid, zoneCtx){ 1052 const l2=document.getElementById('mel-tree-l2'); 1053 if(!l2)return; 1054 1055 // Masquer les boutons de questions (on garde juste le header et le bloc zone) 1056 const subDiv=l2.querySelector('.mel-sub'); 1057 if(subDiv){ 1058 subDiv.querySelectorAll('.mel-q-btn').forEach(b=>b.style.display='none'); 1059 } 1060 1061 // Supprimer réponse précédente si elle existe 1062 l2.querySelector('.mel-plu-answer')?.remove(); 1063 1064 const zoneData = _melZone ? pluGetZone(_melZone) : null; 1065 const ctx=encodeURIComponent((zoneCtx||'').substring(0,300)); 1066 1067 // ââ Construire TOUT le HTML en une seule passe (évite innerHTML += qui casse les liens) ââ 1068 let fullHtml = ''; 1069 1070 // Titre + bouton fermer 1071 fullHtml += '<div class="plu-answer-hdr">' 1072 +'<div class="plu-answer-title">ð '+questionLabel+' â Zone <strong>'+(_melZone||'?')+'</strong></div>' 1073 +'<button class="plu-close-btn" onclick="pluCloseAnswer()" title="Retour aux questions">â Retour</button>' 1074 +'</div>'; 1075 1076 // Onglets 1077 fullHtml += '<div class="plu-tabs">' 1078 +'<button class="plu-tab active" onclick="pluSwitchTab(this,"auth")">ð Autorisation</button>' 1079 +'<button class="plu-tab" onclick="pluSwitchTab(this,"rules")">ð Règles de zone</button>' 1080 +'<button class="plu-tab" onclick="pluSwitchTab(this,"interdits")">ð« Interdictions</button>' 1081 +'</div>'; 1082 1083 // Onglet 1 : Autorisation (contenu principal) 1084 fullHtml += '<div class="plu-tab-content" id="plu-tab-auth">'+pluHtml+'</div>'; 1085 1086 // Onglet 2 : Toutes les règles de la zone 1087 let rulesHtml = ''; 1088 if(zoneData){ 1089 rulesHtml = pluRenderZoneCard(zoneData).replace(/<ul class="plu-interdits[\s\S]*?<\/ul>/,'').replace(/<div class="plu-interdits[\s\S]*?<\/div>/,'').replace(/<div class="plu-alerte[\s\S]*?<\/div>/,''); 1090 } else { 1091 rulesHtml = '<p style="color:var(--muted);font-size:.82rem;padding:8px 0;">Zone non identifiée â saisissez votre adresse pour obtenir les règles spécifiques.</p>'; 1092 } 1093 fullHtml += '<div class="plu-tab-content" id="plu-tab-rules" style="display:none">'+rulesHtml+'</div>'; 1094 1095 // Onglet 3 : Interdictions 1096 let interditsHtml = ''; 1097 if(zoneData && zoneData.interdictions && zoneData.interdictions.length){ 1098 interditsHtml = '<ul class="plu-interdits">' 1099 + zoneData.interdictions.map(i=>'<li>'+i+'</li>').join('') 1100 + '</ul>'; 1101 if(zoneData.alerte) interditsHtml = '<div class="plu-alerte">'+zoneData.alerte+'</div>'+interditsHtml; 1102 } else { 1103 interditsHtml = '<p style="color:var(--muted);font-size:.82rem;padding:8px 0;">Aucune interdiction spécifique trouvée pour cette zone.</p>'; 1104 } 1105 fullHtml += '<div class="plu-tab-content" id="plu-tab-interdits" style="display:none">
1105'+interditsHtml+'</div>'; 1106 1107 // Bouton MEL + bouton retour en bas 1108 fullHtml += '<div style="display:flex;flex-direction:column;gap:6px;margin-top:10px;">' 1109 +'<button class="mel-open-chat-btn" onclick="melOpenChatFromDirect('+_melJsArg(cat)+',\'urbanisme\','+_melJsArg(ctx)+')">ð¬ Question complémentaire ? Parler à MEL</button>' 1110 +'<button class="plu-back-bottom-btn" onclick="pluCloseAnswer()">â Retour aux questions</button>' 1111 +'</div>'; 1112 1113 // ââ Injection unique dans le DOM ââ 1114 const div=document.createElement('div'); 1115 div.className='mel-plu-answer'; 1116 div.innerHTML = fullHtml; 1117 1118 if(subDiv) subDiv.appendChild(div); 1119 else l2.appendChild(div); 1120 setTimeout(()=>div.scrollIntoView({behavior:'smooth',block:'start'}),50); 1121} 1122 1123function pluCloseAnswer(){ 1124 // Réafficher les boutons de questions, supprimer la réponse 1125 const l2=document.getElementById('mel-tree-l2'); 1126 if(!l2)return; 1127 l2.querySelectorAll('.mel-q-btn').forEach(b=>b.style.display=''); 1128 l2.querySelector('.mel-plu-answer')?.remove(); 1129} 1130 1131function pluSwitchTab(btn, tabId){ 1132 const answer=btn.closest('.mel-plu-answer'); 1133 if(!answer)return; 1134 answer.querySelectorAll('.plu-tab').forEach(t=>t.classList.remove('active')); 1135 btn.classList.add('active'); 1136 answer.querySelectorAll('.plu-tab-content').forEach(c=>c.style.display='none'); 1137 const target=answer.querySelector('#plu-tab-'+tabId); 1138 if(target)target.style.display=''; 1139} 1140 1141// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1142// MEL â Réponses directes (catégories non-urbanisme) 1143// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1144 1145// Rend cliquables les adresses contenues dans une réponse de MEL. 1146// 1147// Les réponses sont rédigées en texte libre côté backend (DIRECT_RULES), donc 1148// une adresse peut y apparaître sous plusieurs formes. Avant, seules 1149// les adresses en `https:` et `www.` devenaient des liens : un domaine nu 1150// (« valdeloire-fibre.fr ») s'affichait mais ne s'ouvrait pas â 14 règles sur 1151// 27 étaient dans ce cas. Les courriels non plus n'étaient pas cliquables. 1152// 1153// â ï¸ UN SEUL passage, avec une alternance unique : en chaînant plusieurs 1154// .replace(), le second re-matchait le HTML fabriqué par le premier (le 1155// le `href` en `https:` déjà inséré) et produisait des liens imbriqués. 1156// L'ordre de l'alternance compte : le courriel doit passer AVANT le domaine 1157// nu, sinon « [email protected] » deviendrait un lien vers le domaine. 1158const _MEL_LIEN = new RegExp( 1159 '(https?:\\/\\/[^\\s<>]+)' // 1. URL complète 1160 + '|([a-z0-9._%+-]+@(?:[a-z0-9-]+\\.)+[a-z]{2,})' // 2. courriel 1161 + '|(?<![\\w@.\\-\\/])(www\\.[^\\s<>]+)' // 3. www.⦠1162 + '|(?<![\\w@.\\-\\/])((?:[a-z0-9-]+\\.)+(?:gouv\\.fr|fr|com|org|eu)(?:\\/[^\\s<>]*)?)', // 4. domaine nu 1163 'gi' 1164); 1165 1166function _melLinkify(texte){ 1167 return String(texte==null?'':texte).replace(_MEL_LIEN, function(m, url, mail){ 1168 // La ponctuation qui suit l'adresse n'en fait pas partie. Le motif d'URL 1169 // est volontairement large (`[^\s<>]+`) pour accepter les chemins et les 1170 // paramètres ; sans ce rognage, « (voir https://example.com) » produisait 1171 // un href « https://example.com) » â lien cassé, constaté en production. 1172 let suffixe=''; 1173 const p=/[.,;:!?)\]»â¦]+$/.exec(m); 1174 if(p){ suffixe=p[0]; m=m.slice(0,-suffixe.length); } 1175 if(!m) return suffixe; 1176 const style='color:var(--leaf);font-weight:700;word-break:break-all;'; 1177 if(mail) return '<a href="mailto:'+m+'" style="'+style+'">'+m+'</a>'+suffixe; 1178 const href=/^https?:\/\//i.test(m)?m:'https://'+m; 1179 return '<a href="'+href+'" target="_blank" rel="noopener noreferrer" style="'+style+'">'+m+'</a>'+suffixe; 1180 }); 1181} 1182 1183function _renderDirectAnswer(answer){ 1184 // answer peut être une string ou un objet {text, links:[{label,url,tel}]} 1185 if(typeof answer==='string'){ 1186 // Transformer URLs, courriels et tél en liens cliquables 1187 let t=_melLinkify(answer.replace(/\n/g,'<br>')) 1188 .replace(/(?<!\d)(0[1-9](?:[\s\.\-]?\d{2}){4})(?!\d)/g,'<a href="tel:$1" style="color:var(--leaf);font-weight:700;">ð $1</a>'); 1189 return '<p style="margin:0 0 8px;line-height:1.6;">'+t+'</p>'; 1190 } 1191 // Format objet 1192 let h='<p style="margin:0 0 10px;line-height:1.6;">'+(answer.text||'').replace(/\r\n|\r|\n/g,'<br>')+'</p>'; 1193 if(answer.links&&answer.links.length){ 1194 h+='<div style="display:flex;flex-direction:column;gap:6px;">'; 1195 for(const lk of answer.links){ 1196 if(lk.action){ 1197 var safeAct=lk.action.replace(/[^a-zA-Z0-9_]/g,''); 1198 h+='<button onclick="event.stopPropagation();melRunAction(\''+safeAct+'\')" style="display:block;width:100%;padding:10px 13px;background:var(--forest);color:white;border-radius:9px;text-decoration:none;font-weight:700;font-size:.84rem;border:none;cursor:pointer;font-family:inherit;text-align:left;">'+_melEsc(lk.label)+'</button>'; 1199 continue; 1200 } 1201 const href=lk.tel?'tel:'+lk.tel:lk.url; 1202 const target=lk.tel?'':'target="_blank"'; 1203 const isLocal=href.startsWith('tel:')||href.startsWith('mailto:'); 1204 const clickHandler=isLocal?' onclick="event.stopPropagation();window.location.href='+_melJsArg(href)+';return false;"':''; 1205 h+='<a href="'+_melEsc(href)+'" '+target+clickHandler+' style="display:block;padding:10px 13px;background:var(--forest);color:white;border-radius:9px;text-decoration:none;font-weight:700;font-size:.84rem;">'+_melEsc(lk.label)+'</a>'; 1206 } 1207 h+='</div>'; 1208 } 1209 return h; 1210} 1211 1212function _showDirectAnswer(answer,cat,topic,ctxForMel,questionLabel){ 1213 const l2=document.getElementById('mel-tree-l2'); 1214 if(!l2)return; 1215 l2.querySelector('.mel-direct-answer')?.remove(); 1216 const container=l2.querySelector('.mel-sub'); 1217 if(!container){console.warn('mel-sub not found in l2');return;} 1218 const div=document.createElement('div'); 1219 div.className='mel-direct-answer'; 1220 // Enrichir le contexte avec le libellé de la question posée 1221 const fullCtx = (questionLabel ? 'Question initiale : '+questionLabel+'. ' : '') + (ctxForMel||''); 1222 div.innerHTML='<span class="da-label">Réponse</span>' 1223 +_renderDirectAnswer(answer) 1224 +'<button class="mel-open-chat-btn" style="margin-top:10px;" onclick="melOpenChatFromDirect('+_melJsArg(cat)+','+_melJsArg(topic)+','+_melJsArg(fullCtx.substring(0,400))+')">ð¬ Question complémentaire ? Parler à MEL</button>'; 1225 container.appendChild(div); 1226 setTimeout(()=>div.scrollIntoView({behavior:'smooth',block:'nearest'}),50); 1227} 1228 1229function melOpenChatFromDirect(cat,topic,ctx){ 1230 melOpenChat('',cat,ctx||'',topic||cat); 1231} 1232 1233// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1234// MEL â Bascule vers le mode chat libre 1235// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1236function melOpenChat(initialMsg,category,extraCtx,topic){ 1237 _melChatContext=extraCtx||''; 1238 _melChatCategory=category||'autre'; 1239 _melChatTopic=topic||category||'autre'; 1240 1241 // Construire le contexte complet à injecter dans _melChatContext 1242 // Zone PLU + adresse si disponibles 1243 const zonePrefix = _melZone 1244 ? '[Zone PLU : '+_melZone+(_melAddr?' â '+_melAddr:'')+'] ' 1245 : (_melAddr ? '[Adresse : '+_melAddr+'] ' : ''); 1246 const catLabel = { 1247 urbanisme:'Urbanisme & Construction', 1248 enfance:'Enfance & Jeunesse', 1249 administratif:'Démarches administratives',
1250 dechets:'Déchets & Propreté', 1251 numerique:'Numérique & Internet', 1252 autre:'' 1253 }[_melChatCategory]||''; 1254 const catPrefix = catLabel ? '[Catégorie : '+catLabel+'] ' : ''; 1255 if(!_melChatContext) _melChatContext = zonePrefix + catPrefix; 1256 else _melChatContext = zonePrefix + catPrefix + _melChatContext; 1257 1258 // Afficher le chat, cacher l'arbre 1259 const tree=document.getElementById('mel-tree'); 1260 const msgs=document.getElementById('msgs'); 1261 const mbar=document.querySelector('.mbar'); 1262 if(tree)tree.style.display='none'; 1263 if(msgs){msgs.style.display='flex';msgs.style.flexDirection='column';} 1264 if(mbar)mbar.style.display='flex'; 1265 const privacyHint=document.getElementById('mel-privacy-hint'); 1266 if(privacyHint)privacyHint.style.display='block'; 1267 1268 // Bouton retour vers l'arbre 1269 if(!document.getElementById('mel-back-btn')){ 1270 const backBtn=document.createElement('button'); 1271 backBtn.id='mel-back-btn'; 1272 backBtn.textContent='â Retour aux thèmes'; 1273 backBtn.style.cssText='width:100%;padding:8px;background:var(--mist);border:none;border-radius:8px;font-family:Nunito,sans-serif;font-size:.78rem;font-weight:700;color:var(--leaf);cursor:pointer;margin-bottom:6px;flex-shrink:0;'; 1274 backBtn.onclick=()=>{document.getElementById('mel-back-btn')?.remove();melShowTree();}; 1275 if(msgs)msgs.parentNode.insertBefore(backBtn,msgs); 1276 } 1277 1278 // Message d'accueil MEL (une seule fois) 1279 if(!melInited){ 1280 melInited=true; 1281 addMsg('bot','Bonjour\u00a0! Je suis MEL.\n\nComment puis-je vous aider\u00a0? \ud83d\ude0a'); 1282 } 1283 1284 // Reset melBusy (sécurité si état corrompu) 1285 melBusy=false; 1286 1287 const inp=document.getElementById('minp'); 1288 if(!inp) return; 1289 1290 // Labels catégories pour le pré-remplissage 1291 const catLabels = { 1292 urbanisme:"une question d'urbanisme", 1293 enfance:"une question sur l'enfance et la jeunesse", 1294 administratif:"une question sur les démarches administratives", 1295 dechets:"une question sur les déchets", 1296 numerique:"une question sur le numérique" 1297 }; 1298 const catPhrase = catLabels[_melChatCategory] || ''; 1299 const zonePhrase = _melZone ? ', je suis en zone '+_melZone : ''; 1300 1301 if(initialMsg && initialMsg.trim()){ 1302 inp.value=initialMsg; 1303 inp.placeholder='Votre questionâ¦'; 1304 setTimeout(()=>{ melBusy=false; sendMel(); }, 200); 1305 } else { 1306 // Format demandé : "J'ai une question [catégorie], je suis en zone [zone]. " 1307 inp.value = catPhrase ? 'J\'ai '+catPhrase+zonePhrase+'. ' : ''; 1308 inp.placeholder='Votre questionâ¦'; 1309 inp.focus(); 1310 const len=inp.value.length; 1311 inp.setSelectionRange(len,len); 1312 } 1313} 1314 1315function melResetTree(){ 1316 _melCat=null;_melZone=null;_melZoneLabel=null;_melAddr=null; 1317 const l1=document.getElementById('mel-tree-l1'); 1318 const l2=document.getElementById('mel-tree-l2'); 1319 if(l1)l1.style.display=''; 1320 if(l2){l2.classList.remove('active');l2.style.display='';l2.innerHTML='';} 1321} 1322 1323// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1324// MEL â Détection de zone PLU (adresse + GPS) 1325// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1326 1327async function melFindZoneByAddr(){ 1328 const inp=document.getElementById('mel-addr-input'); 1329 const addr=(inp?.value||'').trim(); 1330 if(!addr){_setZoneResult('warn','â ï¸ Saisissez une adresse');return;} 1331 document.getElementById('mel-addr-warn')?.remove(); 1332 _setZoneResult('','â³ Rechercheâ¦',true); 1333 try{ 1334 const q=encodeURIComponent(addr+' Mézières-lez-Cléry 45370'); 1335 const geo=await fetch(`https://api-adresse.data.gouv.fr/search/?q=${q}&limit=1&citycode=45204`); 1336 const gd=await geo.json(); 1337 const feat=gd?.features?.[0]; 1338 if(!feat){_setZoneResult('warn','â ï¸ Adresse introuvable. Essayez avec moins de mots ou utilisez le GPS.');return;} 1339 const [lon,lat]=feat.geometry.coordinates; 1340 _melAddr=feat.properties?.label||addr; 1341 await _fetchZonePLU(lat,lon); 1342 }catch(e){_setZoneResult('err','â Erreur réseau. Réessayez ou utilisez le GPS.');} 1343} 1344 1345function melFindZoneByGPS(){ 1346 if(!navigator.geolocation){_setZoneResult('warn','â ï¸ GPS non disponible.');return;} 1347 _setZoneResult('','ð Localisationâ¦',true); 1348 navigator.geolocation.getCurrentPosition( 1349 async pos=>{ 1350 document.getElementById('mel-addr-warn')?.remove(); 1351 _melAddr=`GPS (${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)})`; 1352 await _fetchZonePLU(pos.coords.latitude,pos.coords.longitude); 1353 }, 1354 ()=>_setZoneResult('warn','â ï¸ GPS refusé. Saisissez votre adresse.'), 1355 {enableHighAccuracy:true,timeout:8000} 1356 ); 1357} 1358 1359async function _fetchZonePLU(lat,lon){ 1360 _melLat=lat; _melLon=lon; 1361 try{ 1362 const r=await fetch(`${MEL_BACKEND}/api/zone-plu?lat=${lat}&lon=${lon}`); 1363 const d=await r.json(); 1364 if(!d.ok||!d.zone){ 1365 _setZoneResult('warn','â ï¸ Zone PLU non trouvée automatiquement. Sélectionnez-la manuellement :'); 1366 _showManualZoneSelector();return; 1367 } 1368 _melZone=d.zone;_melZoneLabel=d.liblong||'';_melZoneNorm=pluNormalizeZone(d.zone); 1369 _setZoneResult('ok',`â Zone PLU détectée : <strong>${d.zone}</strong>${d.liblong?' â '+d.liblong:''}`); _appendCadastreBtn(); 1370 }catch(e){ 1371 _setZoneResult('err','â Service IGN indisponible. Sélectionnez votre zone :'); 1372 _showManualZoneSelector(); 1373 } 1374} 1375 1376 1377function _appendCadastreBtn(){ 1378 const el=document.getElementById('mel-zone-result'); 1379 if(!el||el.querySelector('.mel-cadastre-btn'))return; 1380 const btn=document.createElement('button'); 1381 btn.className='mel-cadastre-btn'; 1382 btn.style.cssText='margin-top:8px;width:100%;background:#f0f4f8;border:1px solid var(--border);border-radius:10px;padding:8px 12px;font-family:inherit;font-size:0.76rem;font-weight:800;color:var(--forest);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px'; 1383 btn.innerHTML='ðºï¸ Ouvrir la carte cadastrale';
1384 btn.onclick=melShowCadastreData; 1385 el.appendChild(btn); 1386 _appendCarte3dBtn(el); 1387} 1388 1389// Passerelle vers la carte 3D. On NE refait PAS de recherche d'adresse : 1390// melFindZoneByAddr / melFindZoneByGPS ont déjà obtenu les coordonnées et la 1391// zone, on les transmet telles quelles. MEL répond pour un point ; la carte 1392// montre ce qu'il y a autour â les deux se complètent au lieu de se doubler. 1393function _appendCarte3dBtn(el){ 1394 if(!el||el.querySelector('.mel-carte3d-btn'))return; 1395 if(typeof window.matOuvrirCarte3D!=='function')return; // module non chargé 1396 const b=document.createElement('button'); 1397 b.className='mel-carte3d-btn'; 1398 b.style.cssText='margin-top:8px;width:100%;background:var(--mist);border:1px solid var(--sage);border-radius:10px;padding:8px 12px;font-family:inherit;font-size:0.76rem;font-weight:800;color:var(--forest);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px'; 1399 b.innerHTML='ðï¸ Voir ma zone en 3D'; 1400 b.onclick=function(){ 1401 window.matOuvrirCarte3D({ lat:_melLat, lon:_melLon, zone:_melZoneNorm||_melZone }); 1402 }; 1403 el.appendChild(b); 1404} 1405 1406async function melShowCadastreData(){ 1407 if(_melLat==null||_melLon==null)return; 1408 const el=document.getElementById('mel-zone-result'); 1409 const btn=el&&el.querySelector('.mel-cadastre-btn'); 1410 if(btn){btn.disabled=true;btn.textContent='â³ Chargement des données cadastralesâ¦';} 1411 const url=`https://cadastre.data.gouv.fr/map?style=plan#17/${_melLat.toFixed(6)}/${_melLon.toFixed(6)}`; 1412 window.open(url,'_blank','noopener'); 1413 if(btn){btn.innerHTML='ðºï¸ Carte ouverte â';} 1414} 1415 1416function _appendCadastreInfo(html){ 1417 const el=document.getElementById('mel-zone-result'); 1418 if(!el)return; 1419 let card=el.querySelector('.mel-cadastre-result'); 1420 if(!card){card=document.createElement('div');card.className='mel-cadastre-result';el.appendChild(card);} 1421 card.innerHTML=html; 1422} 1423function _showManualZoneSelector(){ 1424 const result=document.getElementById('mel-zone-result'); 1425 if(!result||result.querySelector('select'))return; 1426 const sel=document.createElement('select'); 1427 sel.className='mel-zone-manual'; 1428 sel.innerHTML='<option value="">â Sélectionner votre zone â</option>' 1429 +'<option value="Ua">Ua â Centre-bourg / hameaux anciens</option>' 1430 +'<option value="Ub">Ub â Habitat résidentiel récent</option>' 1431 +'<option value="Ub1">Ub1 â Clos de Manthelon</option>' 1432 +'<option value="Ue">Ue â Ãquipements publics</option>' 1433 +'<option value="Ui">Ui â Zone industrielle</option>' 1434 +'<option value="1AU">1AU â Zone à urbaniser</option>' 1435 +'<option value="2AU">2AU â Réserve foncière (inconstructible)</option>' 1436 +'<option value="A">A â Zone agricole</option>' 1437 +'<option value="Ah">Ah â Hameaux agricoles</option>' 1438 +'<option value="N">N â Zone naturelle</option>' 1439 +'<option value="Nh">Nh â Hameaux naturels</option>' 1440 +'<option value="Nj">Nj â Jardins</option>'; 1441 sel.onchange=()=>{ 1442 _melZone=sel.value||null; 1443 _melZoneLabel=sel.options[sel.selectedIndex]?.text?.split('â')[1]?.trim()||''; 1444 if(_melZone)_setZoneResult('ok',`â Zone sélectionnée : <strong>${_melZone}</strong> â ${_melZoneLabel}`); 1445 }; 1446 result.after(sel); 1447} 1448 1449function _setZoneResult(type,html,spin=false){ 1450 const el=document.getElementById('mel-zone-result'); 1451 if(!el)return; 1452 el.style.display='block'; 1453 el.className='mel-zone-result'+(type?' '+type:''); 1454 el.innerHTML=spin?`<span class="mel-zone-spin">â³</span> ${html}`:html; 1455}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.