spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1<!DOCTYPE html>2<html lang="fr">3<head>4<meta charset="UTF-8">5<meta http-equiv="Content-Security-Policy" content="6 default-src 'self';7 script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;8 style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;9 img-src 'self' data: blob: https:;10 font-src 'self' data:;11 connect-src 'self' https://jfgkriojazaohgjezijj.supabase.co wss://jfgkriojazaohgjezijj.supabase.co https://cdn.jsdelivr.net https://nominatim.openstreetmap.org https://*.tile.openstreetmap.org;12 frame-src 'self';13 worker-src 'self' blob:;14 object-src 'none';15 base-uri 'self';16">17<meta name="viewport" content="width=device-width, initial-scale=1.0">18<title>Logements disponibles — Gestion ISR</title>19<meta name="description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">20<meta property="og:type" content="website">21<meta property="og:title" content="Logements disponibles — Gestion ISR">22<meta property="og:description" content="Découvrez nos logements disponibles dans le Centre-du-Québec avec Gestion ISR.">23<meta property="og:url" content="https://gestion-isr-logements.vercel.app/">24<meta property="og:image" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">25<meta property="og:image:secure_url" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">26<meta property="og:image:type" content="image/png">27<meta property="og:image:width" content="1200">28<meta property="og:image:height" content="630">29<meta name="twitter:card" content="summary_large_image">30<meta name="twitter:image" content="https://gestion-isr-logements.vercel.app/og-image.png?v=20">31<meta name="theme-color" content="#FB8C00">32<link rel="icon" type="image/png" href="data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAwADADASIAAhEBAxEB/8QAGwAAAwADAQEAAAAAAAAAAAAAAAUGAwQHAgj/xAAwEAACAQQBAgUCAwkAAAAAAAABAgMABAURBhIhBzFBUWETIghSgRQWNnFydaGxs//EABkBAAIDAQAAAAAAAAAAAAAAAAACAQMFBv/EACIRAAICAgEEAwEAAAAAAAAAAAECAwQAESESEzFBIjJx8P/aAAwDAQACEQMRAD8A+eqKKKMMKKKpOCcLzXMbyWDGIiRQruW4mJEaH0XYB7n2H86rllSJC7nQGPHG0jBUGycm6K3M1jL7DZOfG5K3e3uoG6XRv9j3B8wfWtOnVgw2PGKQQdHCqDw7wUPJeZ47C3MzxQ3Dt9RkH3dKozkD5PTrfpukSRSupdI3ZV8yFJAqh8Psq3GeY2OauLG5nS2+oWjRdMQ0bL6/LVRZZuy/bPy0dfuuMtgC9xevxsb/ADO5+Ifhnx7KYfHx2UKYyW1lht0khTe4mkClWHqfu3s9979zSSTLngHilieJ4eAHDXsNvA8DN3EjyMv1t/m8t+4GvQa8ZfxpsbiyCR8dyKuJYpV62UBuiRW1v51r9a5Zecpyt9z215TlYmmngu4ZhCAVAVGDLGvsO3z7965qjStyIUs8qAdAn2da/vWbtu1WRw8H22OQPQyt/E3/AB7Zf2uP/rLXLKc8pzeX5VmpstkS8srjSqinpiQE6VR6AbP+T5mlLxSooZ43VT5EqQDXQ0YjXrpEx5AzFtyCaZpFHBONcVnpbDGtYi1gmUtKymRd6Lqq7/TpBGtVu/vbcG6eZrO3ZZNdcTIhVtEkE7XufuI359/Opqih6FdyWZNk5C25lAAbxjux5JeWhj6URhHbG3ALN2UzfV2O/Y77b9qz2vLLq1S2hjtopYLdkZVnJcsUZ2Uk9u4LnRGuw15b3O0UPQrvvqTzgtuZdabHmJ5HPj7VLX9lgniUDaSKCrfcx77B/MRsaNYstnbnI2EdnJDDGiMrEoNF+leler30N6/qPxSiimFKASdwL8vOQbMpTo3xn//Z">33<style>34*{box-sizing:border-box;margin:0;padding:0}35html,body{overflow-x:hidden;max-width:100%}36body{font-family:system-ui,sans-serif;background:#f5f5f3;color:#1a1a1a;min-height:100vh;width:100%}37.header{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}38.logo-row{display:flex;align-items:center;gap:10px}39.logo-circle{width:36px;height:36px;border-radius:50%;background:#FB8C00;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:12px}40.brand{font-size:18px;font-weight:600}41.hero{background:#fff;border-bottom:1px solid #e8e8e5;padding:1.5rem 2rem}42.count-badge{display:inline-block;background:#FFECCF;color:#E67700;font-size:13px;font-weight:500;padding:4px 10px;border-radius:20px;margin-bottom:0.75rem}43.hero h1{font-size:24px;font-weight:600;margin-bottom:4px}44.hero p{font-size:14px;color:#666}45.filters-wrap{background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end}46.filter-group{display:flex;flex-direction:column;gap:4px;min-width:130px}47.filter-group label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}48.filter-group select{font-size:13px;padding:7px 10px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#1a1a1a;cursor:pointer}49#browse-controls .filters-wrap,#browse-controls .tags-row{display:none!important}50.price-range{display:flex;flex-direction:column;gap:4px}51.price-range label{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.04em}52.price-inputs{display:flex;align-items:center;gap:6px}53.price-inputs input{width:80px;font-size:13px;padding:7px 8px;border:1px solid #ddd;border-radius:8px;color:#1a1a1a}54.price-inputs span{font-size:13px;color:#888}55.btn-reset{font-size:13px;padding:7px 14px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#666;cursor:pointer;align-self:flex-end}56.tags-row{padding:0.6rem 2rem;display:flex;gap:6px;flex-wrap:wrap;min-height:40px;align-items:center;background:#f5f5f3;border-bottom:1px solid #e8e8e5}57.tag{display:flex;align-items:center;gap:5px;background:#fff;border:1px solid #ddd;border-radius:20px;padding:3px 10px;font-size:12px;color:#444}58.tag button{background:none;border:none;color:#888;cursor:pointer;font-size:14px;line-height:1;padding:0}59.tags-empty{font-size:13px;color:#aaa}60.quick-filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center}61.quick-filter-btn{font-size:12px;font-weight:600;padding:5px 12px;border-radius:20px;border:1.5px solid #ddd;background:#fff;color:#555;cursor:pointer;transition:all 0.15s;white-space:nowrap}62.quick-filter-btn:hover{border-color:#FB8C00;color:#FB8C00}63.quick-filter-btn.active{background:#FB8C00;color:#fff;border-color:#FB8C00}64.main{padding:1.5rem 2rem 3rem}65.results-line{font-size:13px;color:#888;margin-bottom:1rem}66.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}67.loading{grid-column:1/-1;text-align:center;padding:4rem;color:#888}68.loading-spinner{width:32px;height:32px;border:3px solid #e8e8e5;border-top-color:#FB8C00;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 1rem}69@keyframes spin{to{transform:rotate(360deg)}}70.card{background:#fff;border:1px solid #e8e8e5;border-radius:14px;overflow:hidden;transition:border-color 0.15s,transform 0.1s;cursor:pointer}71.card:hover{border-color:#FB8C00;transform:translateY(-2px)}72.card-promotion{border:3px solid #F97316 !important;box-shadow:0 0 0 5px rgba(249,115,22,0.25)}73.card-psl{border:3px solid #8B5CF6 !important;box-shadow:0 0 0 5px rgba(139,92,246,0.25)}74.card-derniere{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}75.card-coupdecoeur{border:3px solid #EF4444 !important;box-shadow:0 0 0 5px rgba(239,68,68,0.25)}76.card-img{height:180px;background:#f0f0ee;position:relative;overflow:hidden}77.card-img img{width:100%;height:100%;object-fit:cover}78.card-img-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}79.badge-row{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:flex-end;align-items:flex-start;gap:6px}80.badge{font-size:11px;font-weight:600;padding:4px 9px;border-radius:12px;white-space:nowrap}81.badge-dispo{background:#FB8C00;color:#fff}82.badge-animal{background:#fff;color:#333;border:1px solid #ddd}83.card-heart{position:absolute;bottom:10px;left:10px;z-index:11;width:32px;height:32px;border-radius:50%;border:none;background:rgba(255,255,255,0.92);color:#bbb;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;box-shadow:0 1px 4px rgba(0,0,0,0.2)}84.card-heart.active{color:#EF4444}85.card-heart:hover{background:#fff;color:#EF4444}86.badge-etiquette{background:#F59E0B;color:#fff}87.card-body{padding:1rem 1.1rem 1.1rem}88.card-ville{font-size:11px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}89.card-title{font-size:15px;font-weight:600;margin-bottom:10px;line-height:1.3}90.card-tags{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}91.card-tag{font-size:11px;padding:3px 8px;border-radius:6px;font-weight:500}92.ct-size{background:#E6F1FB;color:#0C447C}93.ct-date{background:#FAEEDA;color:#633806}94.ct-etage{background:#EAF3DE;color:#27500A}95.ct-variantes{background:#EEEDFE;color:#3C3489;font-weight:600}96.ct-incl{background:#F1EFE8;color:#444441}97.price-from{font-size:.62em;font-weight:600;color:#777;vertical-align:middle}98/* Modèles de photos (variantes) */99.card-models-btn{display:inline-flex;align-items:center;gap:6px;margin:2px 0 10px;padding:6px 12px;border:1px solid #d9dde2;border-radius:9px;background:#fff;color:#3C3489;font-size:12.5px;font-weight:600;cursor:pointer;transition:.12s}100.card-models-btn:hover{background:#EEEDFE;border-color:#3C3489}101#d-photo-models{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px}102.d-models-box{border:1.5px solid #efbd7c;background:linear-gradient(180deg,#fff4e2,#fce6c6);border-radius:14px;padding:12px 16px;margin:2px 0 6px;box-shadow:0 2px 10px rgba(200,120,10,.1)}103.d-models-head{font-size:14px;font-weight:800;color:#b45309;margin-bottom:9px}104.d-models-head span{font-size:12.5px;font-weight:500;color:#a9743a}105.pv-model-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 14px;border:1.5px solid #edbf85;border-radius:10px;background:#fff;color:#7a4a12;font-size:14px;font-weight:700;cursor:pointer;transition:.12s;box-shadow:0 1px 3px rgba(200,120,10,.14);text-align:center}106.pv-model-btn:hover{background:#e5850f;color:#fff;border-color:#e5850f;transform:translateY(-1px)}107.pv-model-btn:hover span{color:#ffe8cf}108.pv-model-btn span{font-size:12px;font-weight:500;color:#b1936a}109.pv-model-btn.pv-model-cur{background:#e5850f;color:#fff;border-color:#e5850f;cursor:default;box-shadow:none}110.pv-model-btn.pv-model-cur span{color:#ffe8cf}111.pv-model-btn:disabled{opacity:1}112.lb-models{position:absolute;top:14px;left:50%;transform:translateX(-50%);display:none;gap:8px;flex-wrap:wrap;justify-content:center;z-index:5;max-width:92vw}113.lb-models.show{display:flex}114.lb-mbtn{padding:7px 15px;border-radius:20px;border:1px solid rgba(255,255,255,.45);background:rgba(0,0,0,.5);color:#fff;font-size:13px;font-weight:600;cursor:pointer}115.lb-mbtn:hover{background:rgba(0,0,0,.7)}116.lb-mbtn.active{background:#fff;color:#111;border-color:#fff}117/* PHASE 2 — Immeuble & unités (vue agent) */118.d-bld-meta{font-size:12.5px;color:#555;margin:0 0 10px}119.d-bld{border:2px solid #cfd6de;border-radius:12px;overflow:hidden;background:#fbfcfd;width:fit-content;max-width:100%;margin:0 auto}120.d-bld-floor{display:flex;align-items:stretch;border-bottom:1px solid #e7ebef}121.d-bld-floor:last-child{border-bottom:none}122.d-floor-lbl{flex:0 0 72px;background:#f1f5f9;font-size:9.5px;font-weight:700;color:#5b6b7c;display:flex;align-items:center;padding:6px 8px;text-transform:uppercase;letter-spacing:.01em}123.d-units{flex:1;display:flex;flex-wrap:wrap;gap:7px;padding:8px;justify-content:center}124.d-unit{flex:0 0 auto;width:104px;border:1.5px solid #ddd;border-radius:8px;overflow:hidden;cursor:pointer;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.05);transition:transform .12s,box-shadow .12s}125.d-unit:hover{transform:translateY(-2px);box-shadow:0 5px 13px rgba(0,0,0,.14)}126.d-unit-bar{display:flex;justify-content:space-between;align-items:center;gap:4px;padding:2px 7px;font-size:9px;font-weight:700}127.d-unit-body{padding:5px 7px 7px}128.d-unit-type{font-size:13px;font-weight:700;color:#1a1a1a}129.d-unit-etage{font-size:10px;color:#7a828b;margin-top:1px}130.d-unit-price{font-size:12.5px;font-weight:800;color:#E67700;margin-top:3px}131/* vue compacte : petites tuiles numérotées (comme un plan) */132.d-units.compact{gap:3px;padding:5px;align-items:stretch}133.d-units.compact.d-hasgroups{gap:8px;align-items:flex-start}134.d-posgroup{display:flex;flex-direction:column;gap:2px;border:1px dashed #cbd3da;border-radius:6px;padding:3px}135.d-poslbl{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.02em;color:#7a828b;text-align:center}136.d-postiles{display:flex;flex-wrap:wrap;gap:3px}137.d-unit.mini{width:50px;border-width:1px;border-radius:5px;padding:3px 2px;box-shadow:none;color:#1a2230;text-align:center;overflow:hidden;display:flex;flex-direction:column;justify-content:center;min-height:34px}138.d-unit.mini .mn{font-size:8.5px;font-weight:800;line-height:1.05}139.d-unit.mini .mm{font-size:6.5px;font-weight:600;color:#46525f;line-height:1.15;margin-top:1px;white-space:nowrap}140.d-unit.mini:hover{transform:translateY(-1px);box-shadow:0 2px 6px rgba(0,0,0,.16)}141.d-unit.d-unit-locked{cursor:default;opacity:.72}142.d-unit.d-unit-locked:hover{transform:none;box-shadow:none}143.d-unit.mini.d-unit-cur{outline:2px solid #E67700;outline-offset:1px;box-shadow:0 0 0 3px rgba(230,119,0,.18)}144/* Bouton + plan plein écran (grands immeubles) */145.d-bld-expand{display:block;width:100%;margin:0 0 10px;padding:11px;border:1.5px solid #cfd6de;border-radius:10px;background:#f4f7fa;color:#3a4653;font-size:13.5px;font-weight:700;cursor:pointer}146.d-bld-expand:hover{background:#e9eef3;border-color:#9fb0c0}147@media (min-width:769px){ .d-bld-expand{display:none !important} } /* plan plein écran : utile seulement sur mobile */148.bld-full-overlay{display:none;position:fixed;inset:0;z-index:180;background:#fff;flex-direction:column}149.bld-full-overlay.open{display:flex}150.bld-full-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 16px;border-bottom:1px solid #e8e8e5;font-weight:700;font-size:15px;flex:0 0 auto}151.bld-full-head button{background:none;border:none;font-size:28px;line-height:1;cursor:pointer;color:#888}152.bld-zoom-ctl{display:flex;align-items:center;gap:4px;flex:0 0 auto}153.bld-zoom-ctl button{width:38px;height:38px;border:1px solid #e2e2df;border-radius:10px;font-size:20px;font-weight:700;color:#444;background:#fff;display:flex;align-items:center;justify-content:center;padding:0}154.bld-zoom-ctl button:active{background:#eef2f4}155.bld-zoom-ctl .bld-zoom-close{border:none;font-size:28px;color:#888;width:36px}156.bld-full-body{flex:1;overflow:auto;padding:14px;-webkit-overflow-scrolling:touch;cursor:grab;background:#f5f7f9;overscroll-behavior:contain}157.bld-full-body.grabbing{cursor:grabbing}158.bld-full-body .d-bld{width:max-content;min-width:100%;margin:0}159.bld-full-body .d-units.compact{gap:8px;padding:10px;justify-content:flex-start}160.bld-full-body .d-floor-lbl{flex:0 0 92px;font-size:11px}161.bld-full-body .d-unit.mini{width:130px;padding:9px 8px 10px}162.bld-full-body .d-unit.mini .mn{font-size:15px}163.bld-full-body .d-unit.mini .mm{font-size:12.5px}164/* barre d'outils du visuel */165.d-bld-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px}166.d-seg{display:inline-flex;border:1px solid #d5dbe2;border-radius:8px;overflow:hidden}167.d-seg button{border:none;background:#fff;color:#556;font-size:12px;font-weight:600;padding:6px 13px;cursor:pointer}168.d-seg button.active{background:#E67700;color:#fff}169/* Panneau détail d'une unité */170.unit-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:190;align-items:flex-start;justify-content:center;padding:1.5rem;overflow-y:auto}171.unit-overlay.open{display:flex}172.unit-modal{background:#fff;border-radius:16px;width:100%;max-width:460px;margin:auto;padding:20px;position:relative}173.unit-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:26px;line-height:1;color:#999;cursor:pointer}174.up-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding-right:24px}175.up-num{font-size:20px;font-weight:800;color:#1a1a1a}176.up-sub{font-size:13px;color:#7a828b;margin-top:2px}177.up-stat{font-size:11px;font-weight:700;padding:3px 10px;border-radius:12px;white-space:nowrap}178.up-stat.st-dispo{background:#FFECCF;color:#E67700}.up-stat.st-loue{background:#FCE4E4;color:#B42318}179.up-price{font-size:18px;font-weight:800;color:#E67700;margin:12px 0 4px}180.up-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;margin-top:10px}181.up-row{display:flex;justify-content:space-between;gap:10px;font-size:14px;border-bottom:1px solid #f2f4f6;padding-bottom:5px}182.up-k{color:#7a828b}.up-v{font-weight:600;color:#1a1a1a}183.up-sec{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;color:#8a8f96;margin:16px 0 6px}184.up-caract{font-size:14px;color:#333;line-height:1.5;white-space:pre-line}185.up-note{font-size:14px;color:#333;line-height:1.5;background:#fffdf3;border:1px solid #f0d98b;border-radius:10px;padding:10px 12px}186.up-photos{display:flex;gap:6px;overflow-x:auto;padding:2px 0 8px;-webkit-overflow-scrolling:touch}187.up-photos img{height:76px;width:104px;object-fit:cover;border-radius:8px;flex:0 0 auto;cursor:pointer;background:#eee}188.up-introw{display:flex;flex-direction:column;gap:1px;font-size:13.5px;background:#fffdf3;border:1px solid #f0d98b;border-radius:8px;padding:7px 10px;margin-bottom:6px}189.up-intk{font-size:11px;font-weight:700;color:#8a5a00;text-transform:uppercase;letter-spacing:.02em}190.up-intv{color:#333;line-height:1.45}191/* Matcher (association front, sans compte) */192/* Test photo en fond (repli vert d'origine si l'image manque) */193.matcher{background-color:#E67700;background-image:url('hero-matcher.jpg');background-size:cover;background-position:center 27%;background-repeat:no-repeat;padding:52px 2rem 30px}194.matcher-inner{width:fit-content;max-width:100%;margin:0 auto;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.5)}195.matcher-inner h2{margin:0 0 14px;font-size:24px;font-weight:800;text-align:center}196.matcher-inner>.matcher-inner,.matcher-inner p{margin:0 0 16px;font-size:14px;opacity:.94;text-align:center}197.matcher-grid{display:flex;flex-wrap:wrap;align-items:flex-start;gap:16px;background:#fff;border-radius:16px;padding:18px 20px;color:#333;text-shadow:none;box-shadow:0 12px 34px rgba(0,0,0,.16)}198.mx-field{flex:0 0 auto}199.mx-field:has(#mx-secteur){width:186px}200.mx-field:has(#mx-type){width:168px}201.mx-field:has(#mx-budget){width:206px}202.mx-field:has(#mx-etage){width:176px}203.mx-field:has(#mx-animaux){width:104px}204.mx-actions-inline{align-self:center;margin-left:-4px}205.mx-btn-go{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#FB8C00,#E67700);color:#fff;border:none;border-radius:10px;padding:12px 26px;font-size:15px;font-weight:700;white-space:nowrap;cursor:pointer;box-shadow:0 4px 14px rgba(230,119,0,.28);transition:filter .12s,transform .12s}206.mx-btn-go:hover{filter:brightness(1.06);transform:translateY(-1px)}207.mx-btn-more{display:inline-flex;align-items:center;gap:6px;background:#fff;color:#E67700;border:1.5px solid #9ed9c3;border-radius:10px;padding:11px 15px;font-size:13.5px;font-weight:700;white-space:nowrap;cursor:pointer;margin-right:8px}208.mx-btn-more:hover{background:#FFECCF;border-color:#FB8C00}209.mx-more{display:none;flex:1 0 100%;border-top:1px dashed #e2e2df;margin-top:4px;padding-top:14px}210.mx-more.show{display:block}211.mx-more>label{display:block;font-size:12px;font-weight:700;color:#555;margin-bottom:8px;text-align:center}212.mx-field>label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:#777;margin-bottom:6px;text-align:center}213.mx-field>input,.mx-field>select{width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:8px;font-size:14px;color:#1a1a1a}214.mx-chips{display:flex;flex-wrap:wrap;gap:5px;justify-content:center}215.mx-chips label{display:inline-flex;align-items:center;gap:4px;font-size:12.5px;border:1px solid #ddd;border-radius:16px;padding:5px 11px;cursor:pointer;background:#fff;color:#333;position:relative}216.mx-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}217.mx-chips input{position:absolute;opacity:0;width:0;height:0}218.matcher-actions{display:flex;align-items:center;gap:16px;margin-top:16px;flex-wrap:wrap}219.mx-btn-primary{background:#fff;color:#E67700;border:none;border-radius:10px;padding:12px 22px;font-size:15px;font-weight:700;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.18);text-shadow:none}220.mx-btn-primary:hover{background:#FFF7EF}221.mx-btn-ghost{background:transparent;color:#fff;border:none;font-size:13px;font-weight:600;cursor:pointer;opacity:.92;text-decoration:underline}222.match-bar{background:#FFECCF;border:1px solid #FB8C00;border-radius:14px;padding:14px 18px;margin:18px 2rem 0;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}223.mx-sum-btn{border:1px solid #FB8C00;background:#fff;color:#E67700;border-radius:8px;padding:7px 13px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}224.mx-sum-btn:hover{background:#FFF7EF}225.mx-legend{display:flex;gap:14px;margin:10px 2rem 0;font-size:11.5px;color:#777;font-weight:600}226.mx-legend .lg{display:inline-flex;align-items:center;gap:5px}227.mx-legend .lg::before{content:'';width:13px;height:13px;border-radius:3px;display:inline-block}228.mx-legend .lg-p::before{background:#0B7A5E;box-shadow:0 0 0 1px #FB8C00}229.mx-legend .lg-e::before{background:#3FBF97}230.mx-legend .lg-g::before{background:#F59E0B}231.card.tier-perfect{outline:3px solid #0B7A5E;outline-offset:-3px;box-shadow:0 6px 20px rgba(11,122,94,.28)}232.card.tier-excellent{outline:2px solid #3FBF97;outline-offset:-2px}233.card.tier-good{outline:2px solid #F59E0B;outline-offset:-2px}234.mx-tier-badge{display:inline-block;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:3px 9px;border-radius:6px;margin-bottom:6px}235.mx-tier-badge.tier-perfect{background:#0B7A5E;color:#fff}236.mx-tier-badge.tier-excellent{background:#CBEEDF;color:#0B6E52}237.mx-tier-badge.tier-good{background:#F3B44A;color:#5F3B00}238.mx-chips-res{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}239.mx-chip{font-size:11px;padding:2px 8px;border-radius:20px;background:#FFECCF;color:#E67700;font-weight:600}240.mx-chip.soft{background:#FAEEDA;color:#633806}241.mx-chip.miss{background:#FCE4E4;color:#B42318}242.match-group-header{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:20px 0 4px;padding-bottom:8px;border-bottom:2px solid var(--gc,#ddd);font-size:16px;font-weight:800;color:var(--gc,#555)}243.match-group-header:first-child{margin-top:0}244.match-group-header .mgh-count{font-size:12px;font-weight:700;color:#fff;background:var(--gc,#888);border-radius:12px;padding:2px 11px;min-width:24px;text-align:center}245.vtable{width:100%;border-collapse:collapse;font-size:13px}246.vtable th,.vtable td{padding:9px 10px;text-align:left;border-bottom:1px solid #f0f0ee}247.vtable th{font-size:11px;text-transform:uppercase;letter-spacing:.03em;color:#888;font-weight:600;white-space:nowrap}248.vtable tbody tr:hover{background:#faf9f6}249.vtable .v-label{display:inline-block;min-width:22px;text-align:center;font-weight:700;color:#3C3489;background:#EEEDFE;border-radius:6px;padding:1px 7px;margin-right:2px}250.vtable .v-price{font-weight:700;color:#E67700;white-space:nowrap}251.vtable .v-note{color:#888;font-size:12px}252.vtable-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}253.card-footer{display:flex;align-items:center;justify-content:space-between;padding-top:10px;border-top:1px solid #f0f0ee}254.price{font-size:17px;font-weight:600}255.price span{font-size:12px;font-weight:400;color:#888}256.btn-visite-card{font-size:12px;font-weight:600;padding:7px 13px;border:none;border-radius:8px;background:#FFF1E0;color:#C2540E;cursor:pointer;transition:all 0.15s}257.btn-visite-card:hover{background:#FB8C00;color:#fff}258/* MODAL DÉTAIL */259.detail-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100;align-items:flex-start;justify-content:center;padding:1.5rem;overflow-y:auto}260.detail-overlay.open{display:flex}261.detail-modal{background:#fff;border-radius:18px;width:100%;max-width:680px;overflow:hidden;margin:auto}262.detail-close{position:absolute;top:1rem;right:1rem;background:rgba(0,0,0,0.4);border:none;color:#fff;width:32px;height:32px;border-radius:50%;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}263.gallery{position:relative;height:280px;background:#f0f0ee;overflow:hidden}264.gallery img{width:100%;height:100%;object-fit:cover;display:none}265.gallery img.active{display:block}266.gallery-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#ccc}267.gallery-nav{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px}268.gallery-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.5);cursor:pointer;border:none;transition:background 0.15s}269.gallery-dot.active{background:#fff}270.gallery-prev,.gallery-next{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.35);border:none;color:#fff;width:36px;height:36px;border-radius:50%;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}271.gallery-prev{left:12px}272.gallery-next{right:12px}273.gallery-count{position:absolute;top:12px;right:12px;background:rgba(0,0,0,0.4);color:#fff;font-size:12px;padding:3px 8px;border-radius:12px}274.detail-body{padding:1.5rem}275.detail-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1rem;gap:1rem}276.detail-ville{font-size:12px;color:#888;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px}277.detail-title{font-size:20px;font-weight:600;line-height:1.3}278.detail-price{font-size:22px;font-weight:700;color:#FB8C00;white-space:nowrap}279.detail-price span{font-size:13px;font-weight:400;color:#888}280.detail-badges{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1.25rem}281.detail-badge{font-size:12px;font-weight:600;padding:5px 12px;border-radius:20px}282.btn-staged{width:100%;margin:2px 0 1.25rem;padding:12px;border:1.5px solid #F5871F;border-radius:10px;background:linear-gradient(135deg,#FFF1E0,#FFE6CC);color:#C2410C;font-size:15px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .15s}283.btn-staged:hover{background:linear-gradient(135deg,#F5871F,#EA6D0B);color:#fff;border-color:#F5871F}284.staged-spark{position:absolute;top:12px;left:12px;z-index:6;width:40px;height:40px;border-radius:50%;border:none;cursor:pointer;font-size:20px;line-height:1;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#6366F1,#EC4899);box-shadow:0 2px 9px rgba(99,102,241,0.5);overflow:hidden}285.staged-spark:hover{filter:brightness(1.08);transform:scale(1.08)}286.card-staged{position:absolute;bottom:10px;right:10px;z-index:11;width:32px;height:32px;border-radius:50%;border:none;background:linear-gradient(135deg,#6366F1,#EC4899);font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 7px rgba(99,102,241,0.45);overflow:hidden}287.card-staged:hover{filter:brightness(1.08);transform:scale(1.08)}288@keyframes stagedShine{0%{transform:translateX(-160%) rotate(20deg)}55%,100%{transform:translateX(220%) rotate(20deg)}}289.staged-spark::after,.card-staged::after{content:"";position:absolute;top:0;left:0;width:55%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.8),transparent);pointer-events:none;animation:stagedShine 2.4s ease-in-out infinite}290@media (prefers-reduced-motion:reduce){.staged-spark::after,.card-staged::after{animation:none}}291.db-dispo{background:#FFECCF;color:#085041}292.db-animal{background:#F1EFE8;color:#444}293.db-size{background:#E6F1FB;color:#0C447C}294.db-etage{background:#EAF3DE;color:#27500A}295.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}296.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}297.db-ref{background:#F1F5F9;color:#475569;font-family:monospace;letter-spacing:0.05em}298.detail-section{margin-bottom:1.25rem}299.detail-section h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#888;margin-bottom:8px}300.detail-desc{font-size:14px;color:#444;line-height:1.7;white-space:pre-line}301.inclusions-grid{display:flex;flex-wrap:wrap;gap:6px}302.incl-tag{font-size:12px;padding:4px 10px;background:#f5f5f3;border:1px solid #e8e8e5;border-radius:20px;color:#555}303.detail-divider{border:none;border-top:1px solid #f0f0ee;margin:1.25rem 0}304305/* BOUTONS CONTACT */306.contact-btns{display:flex;gap:10px;margin-bottom:0.75rem}307.btn-call{flex:1;padding:12px;background:#F97316;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none}308.btn-call:hover{background:#EA6C0A}309.btn-email{flex:1;padding:12px;background:#3B82F6;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none}310.btn-email:hover{background:#2563EB}311.btn-visite-full{width:100%;padding:12px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;margin-bottom:0.75rem;display:flex;align-items:center;justify-content:center;gap:8px}312.btn-visite-full:hover{background:#E67700}313.btn-retour{width:100%;padding:10px;background:transparent;color:#666;border:1px solid #ddd;border-radius:10px;font-size:14px;cursor:pointer}314.btn-share{width:100%;padding:10px;background:transparent;color:#FB8C00;border:1.5px solid #FB8C00;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}315.btn-share:hover{background:#FFECCF}316/* NOUS JOINDRE */317#contact-section{background:#FFF7EF;color:#1a1a1a;padding:2.5rem 2rem;margin-top:1.5rem}318.contact-inner{max-width:820px;margin:0 auto;text-align:center}319.contact-inner h2{font-size:24px;font-weight:800;margin:0 0 6px}320.contact-lead{font-size:15px;color:#8a7f72;margin:0 0 1.75rem}321.contact-cards{display:flex;gap:16px;justify-content:center;flex-wrap:wrap}322.contact-card{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid #f0e2d2;border-radius:14px;padding:16px 22px;text-decoration:none;color:#1a1a1a;transition:box-shadow .15s,transform .12s,border-color .15s;min-width:260px}323.contact-card:hover{box-shadow:0 8px 22px rgba(0,0,0,.07);transform:translateY(-2px);border-color:#FB8C00}324.cc-icon{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;background:#FFECCF;color:#E67700;flex-shrink:0}325.cc-text{display:flex;flex-direction:column;text-align:left}326.cc-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;color:#a89a8a;font-weight:600}327.cc-value{font-size:16px;font-weight:700;color:#C25A0C}328/* MODAL VISITE */329.visite-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:200;align-items:center;justify-content:center;padding:1rem}330.visite-overlay.open{display:flex}331.vf-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px;padding:1.25rem;max-height:62vh;overflow-y:auto}332.vf-f{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:600;color:#555}333.vf-f.vf-full{grid-column:1/-1}334.vf-f input,.vf-f select,.vf-f textarea{padding:9px 11px;border:1px solid #ddd;border-radius:8px;font-size:14px;font-weight:400;color:#1a1a1a;font-family:inherit}335.vf-submit{display:block;width:calc(100% - 2.5rem);margin:0 1.25rem 1.25rem;padding:13px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer}336.vf-submit:hover{background:#E67700}.vf-submit:disabled{opacity:.6;cursor:default}337.vf-msg{margin:1rem 1.25rem 0;font-size:13px;font-weight:600}338.vf-msg:empty{display:none;margin:0}339.vf-msg.ok{color:#E67700;background:#FFECCF;padding:10px 12px;border-radius:8px}340.vf-msg.err{color:#B42318;background:#FCE4E4;padding:10px 12px;border-radius:8px}341.visite-modal{background:#fff;border-radius:16px;width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:1.5rem}342.modal-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1.25rem}343.modal-title{font-size:17px;font-weight:600}344.modal-sub{font-size:13px;color:#888;margin-top:2px}345.btn-close{background:none;border:none;font-size:22px;color:#888;cursor:pointer}346.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}347.form-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}348.form-field label{font-size:12px;color:#888;font-weight:500}349.form-field input,.form-field select,.form-field textarea{font-size:14px;padding:8px 10px;border:1px solid #ddd;border-radius:8px;color:#1a1a1a;width:100%}350.form-field textarea{resize:vertical;min-height:72px}351.err{font-size:11px;color:#A32D2D;display:none;margin-top:2px}352.divider{border:none;border-top:1px solid #f0f0ee;margin:1rem 0}353.btn-submit{width:100%;padding:10px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer}354.btn-submit:hover{background:#E67700}355.success-view{display:none;text-align:center;padding:1.5rem 0}356.success-icon{font-size:52px;margin-bottom:1rem}357.success-title{font-size:18px;font-weight:600;margin-bottom:6px}358.success-msg{font-size:14px;color:#666;line-height:1.6}359/* MINI CARTE MODAL */360.detail-map{height:220px;width:100%;border-radius:10px;overflow:hidden;margin-bottom:1.25rem}361/* LIGHTBOX */362.lightbox-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.95);z-index:300;align-items:center;justify-content:center}363.lightbox-overlay.open{display:flex}364.lightbox-img{max-width:95vw;max-height:92vh;object-fit:contain;border-radius:4px}365.lightbox-close{position:absolute;top:1rem;right:1rem;background:rgba(255,255,255,0.15);border:none;color:#fff;width:40px;height:40px;border-radius:50%;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center}366.lightbox-close:hover{background:rgba(255,255,255,0.3)}367.lightbox-prev,.lightbox-next{position:absolute;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.15);border:none;color:#fff;width:44px;height:44px;border-radius:50%;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center}368.lightbox-prev:hover,.lightbox-next:hover{background:rgba(255,255,255,0.3)}369.lightbox-prev{left:1rem}370.lightbox-next{right:1rem}371.lightbox-count{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.7);font-size:13px}372.gallery img{cursor:zoom-in}373/* RUBAN LOUÉ */374.ribbon-loue{position:absolute;top:28px;left:-45px;width:190px;background:#EF4444;color:#fff;font-size:14px;font-weight:900;text-align:center;padding:8px 0;transform:rotate(-45deg);box-shadow:0 3px 10px rgba(0,0,0,0.4);letter-spacing:0.15em;z-index:10;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,0.2)}375.ribbon-nouveau{position:absolute;top:28px;left:-45px;width:190px;background:#16A34A;color:#fff;font-size:13px;font-weight:700;text-align:center;padding:7px 0;transform:rotate(-45deg);box-shadow:0 2px 6px rgba(0,0,0,0.18);letter-spacing:0.12em;z-index:10;pointer-events:none}376/* RUBAN LOUÉ */377.card-img{overflow:hidden;position:relative}378379/* ===================== MOBILE ===================== */380@media (max-width:768px){381 /* Marges horizontales resserrées (2rem -> 1rem) */382 .header{padding:0.7rem 1rem;flex-wrap:wrap;gap:8px}383 .brand{font-size:16px}384 .hero{padding:1.1rem 1rem}385 .hero h1{font-size:20px}386 .hero p{font-size:13px}387 .filters-wrap{padding:0.8rem 1rem;gap:0.6rem}388 .tags-row{padding:0.55rem 1rem}389 .main{padding:1.1rem 1rem 2.5rem}390 /* Grille en 1 colonne */391 .grid{grid-template-columns:1fr;gap:14px}392 /* Filtres : 2 par rangée, prix et reset pleine largeur */393 .filter-group{min-width:0;flex:1 1 44%}394 .price-range{flex:1 1 100%;min-width:0}395 .price-inputs{min-width:0}396 .price-inputs input{flex:1 1 0;min-width:0;width:auto}397 .btn-reset{align-self:stretch;flex:1 1 100%;padding:9px 14px}398 /* Matcher : pleine largeur, bouton sous les filtres */399 .matcher{padding:26px 1rem 22px}400 .matcher-inner{width:100%}401 .matcher-inner h2{font-size:20px}402 .matcher-grid{padding:14px;gap:12px}403 .mx-field:has(#mx-secteur),.mx-field:has(#mx-type),.mx-field:has(#mx-budget),.mx-field:has(#mx-etage),.mx-field:has(#mx-animaux){width:100%}404 .mx-actions-inline{width:100%;margin-left:0;align-self:stretch}405 .mx-btn-go{width:100%}406 /* Modal détail : plein écran */407 .detail-overlay{padding:0}408 .detail-modal{border-radius:0;max-width:none;min-height:100vh}409 .detail-body{padding:1.1rem}410 .gallery{height:230px}411 .detail-header{flex-direction:column;gap:.3rem}412 .detail-title{font-size:18px}413 .detail-price{font-size:20px}414 /* Modèles de photos : 1 par rangée */415 .d-models-box{padding:12px}416 #d-photo-models{grid-template-columns:1fr}417 /* Panneau de correspondances : boutons empilés pleine largeur */418 .match-bar{margin:14px 1rem 0;padding:12px 14px}419 .mx-sum-actions{width:100%;flex-direction:column}420 .mx-sum-btn{width:100%}421 .mx-legend{margin:10px 1rem 0}422 /* Visuel immeuble : pleine largeur, tuiles plus grandes et lisibles */423 .d-bld{width:100%;max-width:none}424 .d-floor-lbl{flex:0 0 70px;font-size:9px;padding:6px 8px}425 .d-units.compact{gap:6px;padding:8px}426 .d-unit.mini{width:auto;flex:1 1 100px;min-width:88px;max-width:200px;padding:7px 6px 8px;border-radius:8px;border-width:2px}427 .d-unit.mini .mn{font-size:13px}428 .d-unit.mini .mm{font-size:10.5px}429 /* Panneau d'unité : quasi plein écran */430 .unit-overlay{padding:1rem}431 .unit-modal{max-width:none}432 /* Visite / lightbox */433 .vf-grid{grid-template-columns:1fr}434 /* Barre de recherche rue/ville : mise en évidence sur mobile */435 .search-wrap{padding:0.85rem 1rem;background:#FFF7EF;border-bottom:1px solid #FAD9B0}436 .search-wrap>div{max-width:none}437 .search-wrap input{border:2px solid #FB8C00!important;background:#fff;font-size:16px!important;padding:13px 12px 13px 42px!important;box-shadow:0 2px 8px rgba(251,140,0,.14)}438 .search-wrap svg{left:14px!important;color:#FB8C00!important}439 .lb-models{top:8px;gap:6px}440 .lb-mbtn{padding:6px 11px;font-size:12px}441}442@media (max-width:440px){443 .filter-group{flex:1 1 100%}444 .matcher-inner h2{font-size:18px}445 .gallery{height:200px}446}447/* Outil de partage agent — même sélecteur que la vue admin « Sélections » */448.sel-crit{background:#fff;border:1px solid #e8e8e5;border-radius:12px;padding:14px 16px}449.sel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;align-items:start}450.sel-field>label{display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:#777;margin-bottom:6px}451.sel-field>input,.sel-field>select{width:100%;padding:7px 9px;border:1px solid #ddd;border-radius:7px;font-size:13px;background:#fff;box-sizing:border-box}452.sel-chips{display:flex;flex-wrap:wrap;gap:5px}453.sel-chips label{display:inline-flex;align-items:center;gap:4px;font-size:12px;border:1px solid #ddd;border-radius:16px;padding:4px 10px;cursor:pointer;background:#fff;color:#333}454.sel-chips label:has(input:checked){background:#FFECCF;border-color:#FB8C00;color:#E67700;font-weight:600}455.sel-chips input{margin:0}456.sel-count{font-size:13px;color:#555;margin:2px 0 10px;font-weight:600}457.sel-row{display:flex;align-items:center;gap:10px;border:1px solid #e8e8e5;border-radius:10px;padding:8px 10px;background:#fff;margin-bottom:8px;cursor:pointer}458.sel-row.off{opacity:.5;background:#fafaf8}459.sel-row input[type=checkbox]{width:18px;height:18px;flex:0 0 auto;accent-color:#FB8C00;cursor:pointer}460.sel-row .selthumb{width:54px;height:42px;border-radius:6px;background:#f0f0ee no-repeat center/cover;flex:0 0 auto;object-fit:cover;display:block}461.sel-row .selinfo{flex:1;min-width:0}462.sel-row .selinfo b{font-size:13.5px}463.sel-row .selinfo .m{font-size:12px;color:#888;margin-top:1px}464.sel-linkbox{background:#fff;border:1px solid #FB8C00;border-radius:12px;padding:14px 16px}465.sel-linkbox input.lnk{padding:8px 10px;border:1px solid #ddd;border-radius:7px;font-size:12.5px;background:#f7f7f5;color:#333}466.sel-btn{border:1px solid #d5d5d0;background:#fff;color:#333;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer}467.sel-btn.primary{background:#FB8C00;border-color:#FB8C00;color:#fff}468/* Cartes de rail (partagées avec la section « Nouveautés » vue agent) */469.fr-card{flex:0 0 208px;background:#fff;border:1px solid #e8e8e5;border-radius:12px;overflow:hidden;cursor:pointer;transition:border-color .15s}470.fr-card:hover{border-color:#FB8C00}471.fr-img{height:112px;width:100%;display:block;object-fit:cover;background:#f0f0ee}472.fr-body{padding:8px 10px 10px}473.fr-t{font-size:12.5px;font-weight:600;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}474.fr-m{font-size:11.5px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}475.fr-p{font-size:13px;font-weight:700;color:#1a1a1a;margin-top:2px}476/* Section « Nouveautés » (vue agent) */477#agent-new-rail{margin:0 0 20px;background:#EEF6FF;border:1px solid #BFDBFE;border-radius:14px;padding:12px 14px}478#agent-new-rail .an-head{display:flex;align-items:baseline;gap:10px;margin:0 0 8px;flex-wrap:wrap}479#agent-new-rail .an-title{font-size:16px;font-weight:700;color:#1D4ED8}480#agent-new-rail .an-hint{font-size:11.5px;color:#64748b}481.an-scroll{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px}482.an-scroll .fr-card{flex:0 0 208px}483.fr-days{position:absolute;top:7px;right:7px;background:#1D4ED8;color:#fff;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;box-shadow:0 1px 4px rgba(0,0,0,.25)}484/* Bascule entre immeubles liés (groupe_immeuble) — au-dessus du plan des unités */485.d-bld-switch{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;margin:0 0 10px}486.d-switch-lbl{font-size:12px;color:#8a8f96}487.d-switch-seg{display:inline-flex;flex-wrap:wrap;border:1px solid #E67700;border-radius:9px;overflow:hidden}488.d-switch-btn{display:inline-flex;align-items:center;gap:4px;font-size:12.5px;font-weight:700;padding:6px 14px;color:#E67700;background:#fff;border:none;border-right:1px solid #bfe0d4;cursor:pointer;transition:background .12s}489.d-switch-btn:last-child{border-right:none}490.d-switch-btn:hover:not(.cur){background:#eef6f2}491.d-switch-btn.cur{background:#E67700;color:#fff;cursor:default}492.d-switch-btn[disabled]{opacity:1}493</style>494<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css"495 integrity="sha384-b8ANgTJvdlAnWM5YGMpKn7Kodm+1k7NYNG9zdjTCcZcKatzYHwZ0RLdWarbJJVzU" crossorigin="anonymous"/>496<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"497 integrity="sha384-u5N8qJeJOO2iqNjIKTdl6KeKsEikMAmCUBPc6sC6uGpgL34aPJ4VgNhuhumedpEk" crossorigin="anonymous"></script>498<style>499#map-section{display:none;background:#fff;border-bottom:1px solid #e8e8e5}500#map{height:480px;width:100%}501.map-toggle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;padding:7px 14px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#444;cursor:pointer;transition:all 0.15s}502.map-toggle:hover{border-color:#FB8C00;color:#FB8C00}503.map-toggle.active{background:#FB8C00;color:#fff;border-color:#FB8C00}504.leaflet-popup-content{font-family:system-ui,sans-serif;min-width:180px}505.popup-title{font-size:13px;font-weight:600;margin-bottom:4px;line-height:1.3}506.popup-price{font-size:15px;font-weight:700;color:#FB8C00;margin-bottom:4px}507.popup-dispo{font-size:11px;color:#888;margin-bottom:8px}508.popup-btn{display:block;width:100%;padding:6px;background:#FB8C00;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;text-align:center}509.popup-btn:hover{background:#E67700}510</style>511</head>512<body>513<div class="header">514 <div class="logo-row">515 <img src="data:image/png;base64,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" alt="Gestion ISR" style="height:38px;width:auto">516 </div>517 <div style="display:flex;gap:8px;align-items:center">518 <a class="map-toggle" href="/compte.html" style="text-decoration:none">519 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-3.5 3.6-6 8-6s8 2.5 8 6"/></svg>520 Mon profil521 </a>522 <button class="map-toggle" id="map-toggle-btn" onclick="toggleMap()">523 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>524 Voir la carte525 </button>526 <a class="map-toggle" href="/nous-joindre" style="text-decoration:none">527 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M4 4h16v12H5.2L4 17.2z"/></svg>528 Nous joindre529 </a>530 <button class="map-toggle" id="agent-share-top" onclick="window.openAgentSel&&window.openAgentSel()" style="display:none;background:#FB8C00;color:#fff;border-color:#FB8C00">531 🔗 Créer une sélection532 </button>533 </div>534</div>535<!-- CARTE -->536<div id="map-section">537 <div id="map"></div>538</div>539540<div class="hero">541 <div class="count-badge" id="count-badge">Chargement…</div>542 <h1>Trouvez votre prochain chez-vous</h1>543 <p>Parcourez nos logements disponibles et faites une demande de visite en quelques clics.</p>544</div>545546<!-- MATCHER : association d'appartements sans compte -->547<div id="matcher-panel" class="matcher">548 <div class="matcher-inner">549 <h2>🎯 Votre prochain logement se trouve ici !</h2>550 <div class="matcher-grid">551 <div class="mx-field"><label>Secteur visé</label><select id="mx-secteur" style="width:100%;font-size:14px;padding:9px 10px;border:1px solid #d8dce0;border-radius:8px;background:#fff;color:#333;cursor:pointer"><option value="">Tous les secteurs</option></select></div>552 <div class="mx-field"><label>Type recherché</label><div class="mx-chips" id="mx-type">553 <label><input type="checkbox" value="3½">3½</label><label><input type="checkbox" value="4½">4½</label><label><input type="checkbox" value="5½">5½</label><label><input type="checkbox" value="6½">6½</label><label><input type="checkbox" value="Maison">Maison</label>554 </div></div>555 <div class="mx-field"><label>Budget maximal ($/mois)</label><input id="mx-budget" type="number" min="0" step="1" placeholder="ex : 1200"></div>556 <div class="mx-field"><label>Étage</label><div class="mx-chips" id="mx-etage">557 <label><input type="checkbox" value="demi">Demi ss</label><label><input type="checkbox" value="rdc">RDC</label><label><input type="checkbox" value="1er">1er</label><label><input type="checkbox" value="2e">2e</label><label><input type="checkbox" value="3e">3e</label>558 </div></div>559 <div class="mx-field"><label>Animaux</label><div class="mx-chips" id="mx-animaux">560 <label><input type="checkbox" value="Chat">🐈 Chat</label><label><input type="checkbox" value="Chien">🐶 Chien</label>561 </div></div>562 <div class="mx-actions-inline">563 <button class="mx-btn-more" type="button" id="mx-more-btn" onclick="toggleMxMore()">➕ Plus de critères</button>564 <button class="mx-btn-go" onclick="runMatcher()">✨ Rechercher</button>565 </div>566 <div class="mx-more" id="mx-more">567 <label>Caractéristiques souhaitées</label>568 <div class="mx-chips" id="mx-extras">569 <label><input type="checkbox" value="meuble">🛋️ Meublé</label>570 <label><input type="checkbox" value="chauffage">🔥 Chauffage inclus</label>571 <label><input type="checkbox" value="terrain">🌿 Accès au terrain</label>572 <label><input type="checkbox" value="balcon">🔆 Balcon</label>573 <label><input type="checkbox" value="thermopompe">🍃 Thermopompe</label>574 <label><input type="checkbox" value="aspirateur">🧹 Aspirateur central</label>575 </div>576 </div>577 </div>578 </div>579</div>580<div id="match-summary" style="display:none"></div>581582<div id="browse-controls">583<div class="bed-wrap" style="background:#fff;border-bottom:1px solid #e8e8e5;padding:0.85rem 2rem;display:flex;align-items:center;gap:8px;flex-wrap:wrap">584 <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:2px">Chambres :</span>585 <button class="quick-filter-btn bed-btn active" data-bed="" onclick="setBedrooms('')">Toutes</button>586 <button class="quick-filter-btn bed-btn" data-bed="1" onclick="setBedrooms('1')">🛏️ 1 chambre</button>587 <button class="quick-filter-btn bed-btn" data-bed="2" onclick="setBedrooms('2')">🛏️ 2 chambres</button>588 <button class="quick-filter-btn bed-btn" data-bed="3" onclick="setBedrooms('3')">🛏️ 3 chambres</button>589 <button class="quick-filter-btn bed-btn" data-bed="4" onclick="setBedrooms('4')">🛏️ 4+ chambres</button>590 <button class="quick-filter-btn bed-btn" data-bed="maison" onclick="setBedrooms('maison')">🏠 Maison</button>591</div>592<div class="search-wrap" style="background:#fff;border-bottom:1px solid #e8e8e5;padding:1rem 2rem">593 <div style="position:relative;max-width:500px">594 <svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#aaa;pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>595 <input type="text" id="f-search" placeholder="Rechercher une rue, une ville…" oninput="applyFilters()" style="width:100%;font-size:14px;padding:9px 12px 9px 38px;border:1px solid #ddd;border-radius:10px;color:#1a1a1a;outline:none">596 </div>597</div>598<div class="filters-wrap">599 <div class="filter-group">600 <label>Ville</label>601 <select id="f-ville" onchange="applyFilters()"><option value="">Toutes les villes</option></select>602 </div>603 <div class="filter-group">604 <label>Grandeur</label>605 <select id="f-taille" onchange="applyFilters()"><option value="">Toutes</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select>606 </div>607 <div class="filter-group">608 <label>Étage</label>609 <select id="f-etage" onchange="applyFilters()">610 <option value="">Tous</option>611 <option value="demi">Demi sous-sol</option>612 <option value="rdc">Rez-de-chaussée</option>613 <option value="1er">1er étage</option>614 <option value="2e">2e étage</option>615 <option value="2etages-ss">2 étages + SS</option>616 <option value="3e">3e étage</option>617 </select>618 </div>619 <div class="filter-group">620 <label>Disponibilité</label>621 <select id="f-dispo" onchange="applyFilters()">622 <option value="">Toutes</option>623 <option value="maintenant">Dès maintenant</option>624 <option value="aout">Août 2026</option>625 </select>626 </div>627 <div class="filter-group">628 <label>À la une</label>629 <select id="f-etiquette" onchange="applyFilters()">630 <option value="">Toutes</option>631 <option value="Promotion">Promotion</option>632 <option value="Nouveauté">Nouveauté</option>633 <option value="PSL Accepté">PSL Accepté</option>634 <option value="Dernière unité">Dernière unité</option>635 <option value="Internet inclus">Internet inclus</option>636 <option value="Coup de coeur">Coup de coeur</option>637 <option value="Garage">Garage</option>638 <option value="Logement meublé">Logement meublé</option>639 </select>640 </div>641 <div class="filter-group">642 <label>Animaux</label>643 <select id="f-animaux" onchange="applyFilters()">644 <option value="">Peu importe</option>645 <option value="Aucun">🚫 Aucun animal</option>646 <option value="Chat">🐈 Chat accepté</option>647 <option value="Petits">🐈🐶 Petits compagnons</option>648 <option value="Animaux">🐈🐶 Animaux acceptés</option>649 </select>650 </div>651 <div class="price-range">652 <label>Prix ($/mois)</label>653 <div class="price-inputs">654 <input type="number" id="f-pmin" placeholder="Min" step="50" oninput="applyFilters()">655 <span>—</span>656 <input type="number" id="f-pmax" placeholder="Max" step="50" oninput="applyFilters()">657 </div>658 </div>659 <div class="filter-group">660 <label>Trier par</label>661 <select id="f-tri" onchange="applyFilters()">662 <option value="">Par défaut</option>663 <option value="prix-asc">Prix croissant ↑</option>664 <option value="prix-desc">Prix décroissant ↓</option>665 <option value="dispo">Disponibilité</option>666 </select>667 </div>668 <button class="btn-reset" onclick="resetFilters()">Réinitialiser</button>669</div>670<div class="tags-row" id="tags-row">671 <div class="quick-filters" id="quick-filters">672 <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>673 <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>674 <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>675 <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>676 <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>677 <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>678 <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>679 <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>680 </div>681</div>682</div><!-- /browse-controls -->683<div class="main">684 <div id="selection-banner" style="display:none"></div>685 <!-- Nouveautés (vue agent uniquement) : annonces des 10 derniers jours + compteur de jours -->686 <section id="agent-new-rail" style="display:none">687 <div class="an-head"><span class="an-title">🆕 Nouveautés <span style="font-weight:400;color:#64748b;font-size:12px">— vue agent</span></span><span class="an-hint">annonces publiées depuis moins de 10 jours · le badge indique depuis combien de jours</span></div>688 <div class="an-scroll" id="an-scroll"></div>689 </section>690 <div class="results-line" id="results-line"></div>691 <div class="grid" id="grid">692 <div class="loading"><div class="loading-spinner"></div>Chargement des logements…</div>693 </div>694</div>695696<!-- NOUS JOINDRE -->697<footer id="contact-section">698 <div class="contact-inner">699 <h2>Nous joindre</h2>700 <p class="contact-lead">Une question sur un logement ? Notre équipe est là pour vous répondre.</p>701 <div class="contact-cards">702 <a class="contact-card" href="mailto:info@gestion-isr.com">703 <span class="cc-icon" aria-hidden="true">704 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>705 </span>706 <span class="cc-text"><span class="cc-label">Par courriel</span><span class="cc-value">info@gestion-isr.com</span></span>707 </a>708 <a class="contact-card" href="tel:+18194721127">709 <span class="cc-icon" aria-hidden="true">710 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>711 </span>712 <span class="cc-text"><span class="cc-label">Par téléphone</span><span class="cc-value">819-472-1127</span></span>713 </a>714 </div>715 </div>716</footer>717718<!-- Indicateur « accès agent » (visible seulement pour le staff connecté) -->719<div id="staff-badge" style="position:fixed;left:14px;bottom:14px;z-index:1200;background:#E67700;color:#fff;padding:9px 15px;border-radius:30px;font-size:13px;font-weight:700;box-shadow:0 6px 18px rgba(0,0,0,.28);display:none;align-items:center;gap:8px;border:2px solid #fff">👔 Accès agent · infos internes visibles</div>720<!-- Outil de partage (agent) : accessible via le bouton « Créer une sélection » de l'en-tête -->721722<div id="agent-sel-overlay" style="display:none;position:fixed;inset:0;z-index:1500;background:#f5f5f3;flex-direction:column">723 <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;background:#E67700;color:#fff;flex-shrink:0">724 <div style="font-size:16px;font-weight:800">🔗 Sélections partageables</div>725 <button onclick="window.closeAgentSel&&window.closeAgentSel()" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.4);border-radius:8px;padding:7px 14px;font-size:13px;font-weight:600;cursor:pointer">Fermer</button>726 </div>727 <div style="flex-shrink:0;background:#fff;border-bottom:1px solid #e6e6e2;padding:12px 18px">728 <div style="position:relative;max-width:1040px;margin:0 auto">729 <svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#aaa;pointer-events:none" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>730 <input id="asel-search" type="text" placeholder="Rechercher une rue, une ville, un titre…" oninput="window.agentSelSearchInput&&window.agentSelSearchInput()" style="width:100%;font-size:14px;padding:10px 12px 10px 38px;border:1px solid #d5d5d0;border-radius:10px;box-sizing:border-box;outline:none">731 </div>732 </div>733 <div style="flex:1;overflow-y:auto;padding:16px 18px 32px;max-width:1040px;width:100%;margin:0 auto;box-sizing:border-box">734 <div class="sel-crit">735 <div class="sel-grid">736 <div class="sel-field"><label>Type</label><div class="sel-chips" id="asel-type">737 <label><input type="checkbox" value="3½" onchange="window.agentSelRender&&window.agentSelRender()">3½</label><label><input type="checkbox" value="4½" onchange="window.agentSelRender&&window.agentSelRender()">4½</label><label><input type="checkbox" value="5½" onchange="window.agentSelRender&&window.agentSelRender()">5½</label><label><input type="checkbox" value="6½" onchange="window.agentSelRender&&window.agentSelRender()">6½</label><label><input type="checkbox" value="Maison" onchange="window.agentSelRender&&window.agentSelRender()">Maison</label>738 </div></div>739 <div class="sel-field"><label>Budget maximal ($/mois)</label><input id="asel-budget" type="number" step="1" min="0" placeholder="ex : 1200" oninput="window.agentSelRender&&window.agentSelRender()"></div>740 <div class="sel-field"><label>Préférence d'étage</label><div class="sel-chips" id="asel-etage">741 <label><input type="checkbox" value="demi" onchange="window.agentSelRender&&window.agentSelRender()">Demi sous-sol</label><label><input type="checkbox" value="rdc" onchange="window.agentSelRender&&window.agentSelRender()">Rez-de-chaussée</label><label><input type="checkbox" value="1er" onchange="window.agentSelRender&&window.agentSelRender()">1er étage</label><label><input type="checkbox" value="2e" onchange="window.agentSelRender&&window.agentSelRender()">2e étage</label><label><input type="checkbox" value="3e" onchange="window.agentSelRender&&window.agentSelRender()">3e étage</label>742 </div></div>743 <div class="sel-field"><label>Animaux</label><div class="sel-chips" id="asel-animaux">744 <label><input type="checkbox" value="Aucun" onchange="window.agentSelRender&&window.agentSelRender()">Aucun</label><label><input type="checkbox" value="Chat" onchange="window.agentSelRender&&window.agentSelRender()">Chat</label><label><input type="checkbox" value="Chien" onchange="window.agentSelRender&&window.agentSelRender()">Chien</label><label><input type="checkbox" value="Autre" onchange="window.agentSelRender&&window.agentSelRender()">Autre</label>745 </div></div>746 </div>747 <div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">748 <button class="sel-btn primary" onclick="window.agentSelAll&&window.agentSelAll()">Sélectionner tout</button>749 <button class="sel-btn" onclick="window.agentSelNone&&window.agentSelNone()">Enlever la sélection</button>750 <button class="sel-btn" onclick="window.agentSelResetFilters&&window.agentSelResetFilters()">Réinitialiser les filtres</button>751 </div>752 </div>753 <div id="agent-sel-count" class="sel-count" style="margin-top:16px"></div>754 <div id="agent-sel-list"></div>755 <div id="agent-sel-share" style="margin-top:12px"></div>756 </div>757</div>758759<!-- MODAL DÉTAIL -->760<div class="detail-overlay" id="detail-overlay" onclick="closeDetailOutside(event)">761 <div class="detail-modal" id="detail-modal">762 <div class="gallery" id="detail-gallery"></div>763 <div class="detail-body">764 <div class="detail-header">765 <div>766 <div class="detail-ville" id="d-ville"></div>767 <div class="detail-title" id="d-title"></div>768 </div>769 <div style="text-align:right">770 <div class="detail-price" id="d-price"></div>771 </div>772 </div>773 <div class="detail-badges" id="d-badges"></div>774 <button class="btn-staged" id="d-staged-btn" style="display:none">🛋️ Voir meublé virtuellement</button>775 <div id="d-photo-models-sec" class="d-models-box" style="display:none">776 <div class="d-models-head">📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span></div>777 <div id="d-photo-models"></div>778 </div>779 <div class="detail-section" id="d-variants-sec" style="display:none">780 <h3>Modèles disponibles</h3>781 <div id="d-variants"></div>782 </div>783 <div class="detail-section" id="d-building-sec" style="display:none">784 <h3>🏢 Immeuble — unités <span style="font-size:12px;font-weight:400;color:#8a5a00">(vue agent · clique une unité)</span></h3>785 <div id="d-building"></div>786 </div>787 <div class="detail-section">788 <h3>Description</h3>789 <div class="detail-desc" id="d-desc"></div>790 </div>791 <hr class="detail-divider">792 <div class="detail-section">793 <h3>Localisation</h3>794 <div class="detail-map" id="detail-map"></div>795 </div>796 <div id="d-internal-wrap" style="display:none">797 <hr class="detail-divider">798 <div class="detail-section">799 <h3>🔒 Infos internes</h3>800 <div id="d-internal"></div>801 </div>802 </div>803 <hr class="detail-divider">804 <div class="contact-btns">805 <a class="btn-call" id="btn-call" href="tel:+18194721127">806 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8a19.79 19.79 0 01-3.07-8.67A2 2 0 012 .18H5a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14.92z"/></svg>807 Appeler808 </a>809 <a class="btn-email" id="btn-email" href="mailto:info@gestion-isr.com">810 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 01-2.06 0L2 7"/></svg>811 Courriel812 </a>813 </div>814 <button class="btn-visite-full" id="btn-visite-full">Formulaire de location</button>815 <button class="btn-share" id="btn-share" onclick="partagerFiche()">816 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>817 Partager cette fiche818 </button>819 <button class="btn-retour" onclick="closeDetail()">← Retour aux logements</button>820 </div>821 </div>822</div>823824<!-- DÉTAIL D'UNE UNITÉ (vue agent) -->825<div class="unit-overlay" id="unit-overlay" onclick="closeUnitPanel(event)">826 <div class="unit-modal" id="unit-modal"></div>827</div>828829<!-- PLAN DE L'IMMEUBLE EN PLEIN ÉCRAN (grands immeubles) -->830<div class="bld-full-overlay" id="bld-full-overlay">831 <div class="bld-full-head"><span>🏢 Plan de l'immeuble <span style="font-weight:400;color:#888;font-size:12px">— glisse pour te déplacer, pince pour zoomer</span></span>832 <div class="bld-zoom-ctl">833 <button onclick="bldZoomBy(0.8)" title="Dézoomer" aria-label="Dézoomer">−</button>834 <button onclick="bldZoomFit()" title="Tout voir" aria-label="Tout voir">⊡</button>835 <button onclick="bldZoomBy(1.25)" title="Zoomer" aria-label="Zoomer">+</button>836 <button onclick="closeBuildingFull()" title="Fermer" class="bld-zoom-close">×</button>837 </div>838 </div>839 <div class="bld-full-body" id="bld-full-body"></div>840</div>841842<!-- LIGHTBOX -->843<div class="lightbox-overlay" id="lightbox-overlay" onclick="closeLightboxOutside(event)">844 <button class="lightbox-close" onclick="closeLightbox()">×</button>845 <div class="lb-models" id="lightbox-models" onclick="event.stopPropagation()"></div>846 <button class="lightbox-prev" id="lb-prev" onclick="lightboxNav(-1)">‹</button>847 <img class="lightbox-img" id="lightbox-img" src="" alt="">848 <button class="lightbox-next" id="lb-next" onclick="lightboxNav(1)">›</button>849 <div class="lightbox-count" id="lightbox-count"></div>850</div>851852<!-- MODAL VISITE -->853<div class="visite-overlay" id="visite-overlay" onclick="closeVisiteOutside(event)">854 <div class="visite-modal" style="max-width:560px;padding:0;overflow:hidden;border-radius:16px">855 <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;background:#fff;border-bottom:1px solid #f0f0ee">856 <div>857 <div class="modal-title">Formulaire de location</div>858 <div class="modal-sub" id="modal-apt-name"></div>859 </div>860 <button class="btn-close" onclick="closeVisite()">×</button>861 </div>862 <form id="visite-form">863 <div class="vf-msg" id="visite-msg"></div>864 <div class="vf-grid">865 <label class="vf-f vf-full"><span>Nom complet *</span><input id="vf-nom" required></label>866 <label class="vf-f"><span>Courriel *</span><input id="vf-courriel" type="email" required></label>867 <label class="vf-f"><span>Téléphone *</span><input id="vf-tel" type="tel" required></label>868 <label class="vf-f"><span>Date d'emménagement souhaitée</span><input id="vf-date" placeholder="ex : 1 juillet 2026"></label>869 <label class="vf-f"><span>Type recherché</span><select id="vf-type"><option value="">—</option><option>3½</option><option>4½</option><option>5½</option><option>6½</option><option>Maison</option></select></label>870 <label class="vf-f"><span>Budget max ($/mois)</span><input id="vf-budget" type="number" min="0" step="1" placeholder="ex : 1300"></label>871 <label class="vf-f"><span>Animaux</span><select id="vf-animaux"><option value="">—</option><option>Aucun</option><option>Chat</option><option>Chien</option><option>Chat et chien</option></select></label>872 <label class="vf-f"><span>Enquête de crédit acceptée ?</span><select id="vf-credit"><option value="">—</option><option>Oui</option><option>Non</option><option>Incertain</option></select></label>873 <label class="vf-f vf-full"><span>Message / questions</span><textarea id="vf-message" rows="3" placeholder="Ta motivation, tes questions…"></textarea></label>874 </div>875 <button type="submit" class="vf-submit" id="vf-submit">Envoyer ma demande</button>876 </form>877 <div id="visite-success" style="display:none;padding:2.5rem 1.5rem;text-align:center">878 <div style="font-size:52px;line-height:1;margin-bottom:14px">✅</div>879 <div style="font-size:19px;font-weight:800;color:#E67700;margin-bottom:8px">Demande envoyée !</div>880 <div style="font-size:14px;color:#555;max-width:340px;margin:0 auto 22px">Merci, nous avons bien reçu ta demande. Notre équipe te contactera sous peu.</div>881 <button onclick="closeVisite()" style="padding:12px 26px;background:#FB8C00;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer">Fermer</button>882 </div>883 </div>884</div>885886<script>887// Source de données : Supabase (remplace l'ancien Google Sheet via opensheet)888const SUPABASE_URL = 'https://jfgkriojazaohgjezijj.supabase.co';889const SUPABASE_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw';890// On ne lit que les annonces actives (la sécurité côté base l'impose de toute façon)891const LISTINGS_URL = `${SUPABASE_URL}/rest/v1/listings?select=*&statut=eq.Actif`;892// Disponibilité d'une unité : seul « Disponible » est offert au public. Les autres893// statuts (Loué, Départ planifié, En construction, Réservé…) sont considérés non disponibles.894const NON_DISPO = ['Loué','Départ planifié','En construction','Réservé Gestion ISR','Réservé propriétaire'];895const U_STATUTS = ['Disponible', ...NON_DISPO]; // pour le sélecteur de statut (vue agent)896const isAvail = u => !NON_DISPO.includes(u && u.statut);897// Couleur de tuile par statut. Les rôles internes (staff) voient les couleurs distinctes ;898// le public voit « vert » pour les statuts réservés / départ planifié (non révélés).899const UNIT_COLORS_INT = {900 'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],901 'Départ planifié':['#EEE1FB','#8B5CF6'], 'Réservé Gestion ISR':['#FFE6CC','#F5871F'], 'Réservé propriétaire':['#FEF9C3','#EAB308']902};903const UNIT_COLORS_PUB = {904 'Disponible':['#DCF5EB','#16B981'], 'Loué':['#FCE4E4','#E0736C'], 'En construction':['#DCE9FF','#3B82F6'],905 'Départ planifié':['#DCF5EB','#16B981'], 'Réservé Gestion ISR':['#DCF5EB','#16B981'], 'Réservé propriétaire':['#DCF5EB','#16B981']906};907function unitColors(statut){ const map = window.__isStaff ? UNIT_COLORS_INT : UNIT_COLORS_PUB; return map[statut] || map['Disponible']; }908909let allListings = [], filtered = [], currentListing = null, currentPhotoIdx = 0;910911// Échappement HTML — protège contre les injections (XSS) provenant des données du Sheet.912// À utiliser pour TOUTE donnée du Sheet insérée dans innerHTML ou un attribut HTML.913function esc(s) {914 return String(s == null ? '' : s)915 .replace(/&/g, '&')916 .replace(/</g, '<')917 .replace(/>/g, '>')918 .replace(/"/g, '"')919 .replace(/'/g, ''');920}921922// Redimensionne les photos Supabase à la volée (render/image) — bien plus léger/rapide.923// Les URLs externes (data:) sont inchangées.924function simg(url, w, q){925 if (!url || typeof url !== 'string') return url || '';926 if (url.indexOf('/storage/v1/object/public/') !== -1)927 // resize=contain : indispensable, sinon Supabase écrase la largeur sans ajuster la hauteur (image déformée/zoomée)928 return url.replace('/storage/v1/object/public/', '/storage/v1/render/image/public/') + (url.indexOf('?') !== -1 ? '&' : '?') + 'width=' + w + '&resize=contain&quality=' + (q || 70);929 return url;930}931async function getFolderPhotos(l) {932 // Photos Supabase uniquement.933 if (Array.isArray(l.photos) && l.photos.length){934 // Photo principale (choisie via l'étoile dans l'admin) en tête935 if (l.main_photo && l.photos.includes(l.main_photo)) return [l.main_photo, ...l.photos.filter(p => p !== l.main_photo)];936 return l.photos.slice();937 }938 return [];939}940941function getEtage(l) {942 return l['Étage'] || l['etage'] || l['étage'] || l['ETAGE'] || l['Etage'] || '';943}944945function normalizeEtage(e) {946 if (!e) return '';947 const s = e.toLowerCase().trim()948 .replace(/é/g,'e').replace(/è/g,'e').replace(/ê/g,'e')949 .replace(/\s+/g,' ');950 if (951 s.includes('2 etages + ss') ||952 s.includes('2 etages ss') ||953 s.includes('2 etages sous-sol') ||954 s.includes('2 etages sous sol') ||955 s.includes('2 etages et sous-sol') ||956 s.includes('2 etages et sous sol') ||957 s.includes('deux etages sous-sol') ||958 s.includes('deux etages sous sol')959 ) return '2etages-ss';960 if (s.includes('demi')) return 'demi';961 if (s.includes('sous-sol') || s.includes('sous sol')) return 'sous-sol';962 if (s.includes('jardin')) return 'rdj';963 if (s.includes('rez') || s.includes('chaussee')) return 'rdc';964 if (s.includes('1er') || s.includes('premier')) return '1er';965 if (s.includes('2e') || s.includes('deuxieme')) return '2e';966 if (s.includes('3e') || s.includes('troisieme')) return '3e';967 return '';968}969970function etageLabel(e) {971 const map = {'sous-sol':'Sous-sol','demi':'Demi sous-sol','rdc':'Rez-de-chaussée','rdj':'Rez-de-jardins','1er':'1er étage','2e':'2e étage','2etages-ss':'2 étages + SS','3e':'3e étage'};972 return map[e] || e;973}974975// ---- Variantes (modèles regroupés dans une même fiche) ----976function hasVariants(l){ return Array.isArray(l.variants) && l.variants.length > 0; }977function variantEffPrices(v){ const fl = variantFloors(v).filter(f => Number(f.qte) > 0); if (fl.length) return fl.map(f => { const fp = parseFloat(f.loyer); return isNaN(fp) ? parseFloat(v.loyer) : fp; }).filter(x => !isNaN(x)); const b = parseFloat(v.loyer); return isNaN(b) ? [] : [b]; }978function variantLoyers(l){ return (l.variants || []).flatMap(variantEffPrices); }979// Prix des unités : DISPONIBLES d'abord (repli sur toutes si tout est loué). Le prix public980// « dès… » d'un immeuble doit refléter les unités réellement disponibles, pas les louées.981function unitLoyers(l){982 if (!Array.isArray(l.units) || !l.units.length) return [];983 const avail = l.units.filter(u => isAvail(u));984 const src = avail.length ? avail : l.units;985 return src.map(u => parseFloat(u.prix)).filter(x => !isNaN(x) && x > 0);986}987function minLoyer(l){ const u = unitLoyers(l); if (u.length) return Math.min(...u); if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return Math.min(...a); } return parseFloat(l.loyer) || 0; }988function maxLoyer(l){ const u = unitLoyers(l); if (u.length) return Math.max(...u); if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return Math.max(...a); } return parseFloat(l.loyer) || 0; }989function loyerSet(l){ const u = unitLoyers(l); if (u.length) return u; if (hasVariants(l)){ const a = variantLoyers(l); if (a.length) return a; } const x = parseFloat(l.loyer); return isNaN(x) ? [] : [x]; }990function variantFloors(v){ if (Array.isArray(v.floors) && v.floors.length) return v.floors.filter(f => f && f.etage); if (v.etage) return [{ etage: v.etage, qte: (v.qte != null ? v.qte : 1) }]; return []; }991function variantQte(v){ const fl = variantFloors(v); if (fl.length) return fl.reduce((s,f) => s + (Number(f.qte) > 0 ? Number(f.qte) : 0), 0); return Number(v.qte) > 0 ? Number(v.qte) : 0; }992function variantEtages(v){ return [...new Set(variantFloors(v).filter(f => Number(f.qte) > 0).map(f => f.etage))]; }993// ---- Unités individuelles (modèle immeuble) ----994function hasUnits(l){ return Array.isArray(l.units) && l.units.length > 0; }995// Étages des unités DISPONIBLES (repli sur toutes si tout est loué). Source de vérité pour996// la recherche + le matcher d'un immeuble : le champ « Étage » du haut devient inutile ici.997function unitEtagesNorm(l){998 if (!hasUnits(l)) return [];999 const avail = l.units.filter(u => isAvail(u));1000 const src = avail.length ? avail : l.units;1001 return [...new Set(src.map(u => normalizeEtage(u.etage)).filter(Boolean))];1002}1003function totalUnits(l){ return (l.variants || []).reduce((s,v) => s + variantQte(v), 0); }1004function distinct(arr){ return [...new Set(arr.filter(x => x != null && x !== ''))]; }1005function grandeurDisplay(l){ if (hasVariants(l)){ const s = distinct(l.variants.map(v => v.grandeur || l.grandeur)); if (s.length) return s.join(' · '); } return l.grandeur || ''; }1006function dispoDisplay(l){ if (hasVariants(l)){ const s = distinct(l.variants.map(v => v.dispo)); if (s.length > 1) return 'Plusieurs disponibilités'; if (s.length === 1) return s[0]; } return l.date_dispo_affichage || l.date_dispo || ''; }1007function variantMatchesGrandeur(l, taille){ return hasVariants(l) ? l.variants.some(v => (v.grandeur || l.grandeur) === taille) : (l.grandeur === taille); }1008function variantMatchesEtage(l, etage){1009 if (hasUnits(l)){ const es = unitEtagesNorm(l); if (es.length) return es.includes(etage); }1010 if (hasVariants(l)) return l.variants.some(v => { const es = variantEtages(v); return es.length ? es.some(e => normalizeEtage(e) === etage) : (normalizeEtage(getEtage(l)) === etage); });1011 return normalizeEtage(getEtage(l)) === etage;1012}1013function variantMatchesDispo(l, dispo){ return hasVariants(l) ? l.variants.some(v => normalizeDispo(v.dispo || l.date_dispo_affichage) === dispo) : (normalizeDispo(l.date_dispo_affichage) === dispo); }1014function dispoOrderIndex(l){ const order = ['maintenant','mars','mai','juin','juillet','aout','']; const ds = hasVariants(l) ? distinct(l.variants.map(v => v.dispo)) : [l.date_dispo_affichage]; let best = 99; (ds.length ? ds : ['']).forEach(d => { const i = order.indexOf(normalizeDispo(d)); if (i !== -1 && i < best) best = i; }); return best; }10151016// ===== MATCHER (association d'appartements, sans compte) =====1017const TIER = { perfect:{rank:0,label:'★ Parfaite',cls:'tier-perfect'}, excellent:{rank:1,label:'Très bonne',cls:'tier-excellent'}, good:{rank:2,label:'Bonne',cls:'tier-good'}, ok:{rank:3,label:'',cls:''} };1018let matchCriteria = null, matchByApp = {};1019function listingEtagesNorm(l){1020 if (hasUnits(l)){ const e = unitEtagesNorm(l); if (e.length) return e; }1021 if (hasVariants(l)){ const out=[]; l.variants.forEach(v => { const es=variantEtages(v); if (es.length) es.forEach(e=>out.push(normalizeEtage(e))); else { const n=normalizeEtage(getEtage(l)); if(n) out.push(n); } }); return [...new Set(out.filter(Boolean))]; }1022 const n=normalizeEtage(getEtage(l)); return n?[n]:[];1023}1024function petAllowed(a, pet){ if(!a) return false; if(a.includes('animaux')||a.includes('tous')) return true; if(pet==='Chat') return a.includes('chat'); if(pet==='Chien') return a.includes('chien'); return a.includes('animaux'); }1025// ---- Critères supplémentaires (caractéristiques) ----1026// `has` lit la donnée structurée d'une unité (unit.carac) ; `txt` est un repli sur le texte de1027// l'annonce, car la majorité des fiches n'ont pas encore de caractéristiques structurées.1028const EXTRA_CRIT = [1029 { key:'meuble', label:'Meublé', has:c => !!(c.meuble && c.meuble !== 'Non meublé'), txt:t => /meubl/.test(t) },1030 { key:'chauffage', label:'Chauffage inclus', has:c => !!(c.chauffage && c.chauffage !== 'Non inclus'), txt:t => /chauffage/.test(t) },1031 { key:'terrain', label:'Accès au terrain', has:c => !!c.terrain, txt:t => /acc[eè]s au terrain/.test(t) },1032 { key:'balcon', label:'Balcon', has:c => !!c.balcon, txt:t => /balcon/.test(t) },1033 { key:'thermopompe', label:'Thermopompe', has:c => /thermopompe/i.test(c.clim || ''), txt:t => /thermopompe/.test(t) },1034 { key:'aspirateur', label:'Aspirateur central', has:c => !!c.aspirateur_central, txt:t => /aspirateur central/.test(t) },1035];1036// Texte de l'annonce en minuscules, débarrassé des tournures négatives pour éviter les faux positifs1037// (« non meublé » ne doit pas matcher « meublé »).1038function _extraHay(l){1039 const units = Array.isArray(l.units) ? l.units : [];1040 const avail = units.filter(u => isAvail(u));1041 const src = avail.length ? avail : units;1042 return [l.description || '', ...src.map(u => u.caract || '')].join(' \n ').toLowerCase()1043 .replace(/non\s+meubl\w*/g, '')1044 .replace(/chauffage\s+non\s+inclus/g, '')1045 .replace(/(pas|aucun)\s+(de\s+)?balcon/g, '')1046 .replace(/(pas|aucun)\s+(d.)?acc[eè]s au terrain/g, '');1047}1048function listingHasExtra(l, def){1049 if (l.carac && def.has(l.carac)) return true; // fiche virtuelle (modèle) : carac directe1050 const units = Array.isArray(l.units) ? l.units : [];1051 const avail = units.filter(u => isAvail(u));1052 const src = avail.length ? avail : units;1053 if (src.some(u => u.carac && def.has(u.carac))) return true; // donnée structurée = source de vérité1054 return def.txt(_extraHay(l)); // repli : texte de l'annonce1055}1056function matchListing(c, l){1057 const chips=[], blocks=[]; let score=0, specified=0, satisfied=0, soft=false;1058 if (c.types.length){ specified++; const gs = hasVariants(l) ? distinct(l.variants.map(v=>v.grandeur||l.grandeur)) : [l.grandeur]; const hit = gs.find(g=>c.types.includes(g)); if(hit){ chips.push({txt:'✓ '+hit}); score+=3; satisfied++; } else blocks.push('type'); }1059 if (c.budget!=null && !isNaN(c.budget) && c.budget>0){ specified++; const min=minLoyer(l); if(min<=c.budget){ chips.push({txt:'✓ Dans le budget'}); score+=3; satisfied++; } else if(min<=c.budget*1.1){ chips.push({txt:'≈ Proche du budget',soft:true}); score+=1; satisfied++; soft=true; } else blocks.push('budget'); }1060 if (c.etages.length){ specified++; const es=listingEtagesNorm(l); const hit=es.find(e=>c.etages.includes(e)); if(hit){ chips.push({txt:'✓ Étage'}); score+=1; satisfied++; } else chips.push({txt:'✗ Étage',miss:true}); }1061 if (c.pets.length){ specified++; const a=(l.animaux||'').toLowerCase(); if(a.includes('aucun')) blocks.push('animaux'); else if(c.pets.some(p=>petAllowed(a,p))){ chips.push({txt:'✓ Animaux OK'}); score+=1; satisfied++; } else { chips.push({txt:'🐾 À confirmer',soft:true}); satisfied++; soft=true; } }1062 // Caractéristiques : pastille ✗ plutôt qu'un blocage — les données sont encore partielles,1063 // on ne masque donc jamais une annonce, elle est seulement moins bien classée.1064 (c.extras || []).forEach(k => {1065 const def = EXTRA_CRIT.find(x => x.key === k); if (!def) return;1066 specified++;1067 if (listingHasExtra(l, def)){ chips.push({txt:'✓ '+def.label}); score+=1; satisfied++; }1068 else chips.push({txt:'✗ '+def.label, miss:true});1069 });1070 const compatible = blocks.length===0; let tier='ok';1071 if (compatible && specified>0){ if(satisfied===specified&&!soft) tier='perfect'; else if(satisfied===specified) tier='excellent'; else if(satisfied/specified>=0.6) tier='good'; }1072 return { compatible, score, chips, tier };1073}1074function readMatcherForm(){1075 const chips = sel => [...document.querySelectorAll(sel+' input:checked')].map(i=>i.value);1076 const b = document.getElementById('mx-budget').value.trim();1077 const sect = document.getElementById('mx-secteur');1078 return { secteur: sect ? sect.value : '', types: chips('#mx-type'), budget: b===''?null:Number(b), etages: chips('#mx-etage'), pets: chips('#mx-animaux'), extras: chips('#mx-extras') };1079}1080function toggleMxMore(){1081 const p = document.getElementById('mx-more'), b = document.getElementById('mx-more-btn');1082 if (!p || !b) return;1083 const show = !p.classList.contains('show');1084 p.classList.toggle('show', show);1085 b.textContent = show ? '➖ Moins de critères' : '➕ Plus de critères';1086}1087window.toggleMxMore = toggleMxMore;1088function runMatcher(){1089 const c = readMatcherForm();1090 matchCriteria = c;1091 const avail = allListings.filter(l => (l.etiquette||'').toLowerCase() !== 'loué' && (!c.secteur || String(l.ville||'').trim() === c.secteur));1092 const scored = avail.map(l => ({ l, m: matchListing(c, l) })).filter(x => x.m.compatible)1093 .sort((a,b) => (TIER[a.m.tier].rank - TIER[b.m.tier].rank) || (b.m.score - a.m.score) || (minLoyer(a.l) - minLoyer(b.l)));1094 matchByApp = {}; scored.forEach(x => { matchByApp[x.l.ID_app || x.l.id_app] = x.m; });1095 filtered = scored.map(x => x.l);1096 document.getElementById('matcher-panel').style.display = 'none';1097 document.getElementById('browse-controls').style.display = 'none';1098 showMatchSummary(c, filtered);1099 render();1100 document.getElementById('match-summary').scrollIntoView({ behavior:'smooth', block:'start' });1101}1102function editMatcher(){1103 matchCriteria = null; matchByApp = {};1104 document.getElementById('matcher-panel').style.display = '';1105 document.getElementById('browse-controls').style.display = '';1106 document.getElementById('match-summary').style.display = 'none';1107 filtered = [...allListings]; render();1108 document.getElementById('matcher-panel').scrollIntoView({ behavior:'smooth', block:'start' });1109}1110function clearMatcher(){1111 matchCriteria = null; matchByApp = {};1112 document.getElementById('matcher-panel').style.display = '';1113 document.getElementById('browse-controls').style.display = '';1114 document.getElementById('match-summary').style.display = 'none';1115 filtered = [...allListings]; render(); renderTags();1116 window.scrollTo({ top:0, behavior:'smooth' });1117}1118function showMatchSummary(c, list){1119 const parts = [];1120 if (c.secteur) parts.push('📍 ' + c.secteur);1121 if (c.types.length) parts.push(c.types.join(', '));1122 if (c.budget) parts.push('≤ ' + c.budget + ' $');1123 if (c.etages.length) parts.push(c.etages.map(e => etageLabel(e)).join(', '));1124 if (c.pets.length) parts.push('🐾 ' + c.pets.join(', '));1125 if (c.extras && c.extras.length) parts.push(c.extras.map(k => (EXTRA_CRIT.find(x => x.key === k) || {}).label).filter(Boolean).join(', '));1126 const perfectN = list.filter(l => (matchByApp[l.ID_app||l.id_app]||{}).tier === 'perfect').length;1127 const el = document.getElementById('match-summary');1128 el.style.display = 'block';1129 el.innerHTML = `<div class="match-bar">1130 <div style="font-size:14px;color:#E67700"><b>${list.length}</b> logement${list.length>1?'s':''} correspond${list.length>1?'ent':''} à tes critères${perfectN?` — dont <b>${perfectN} parfaitement</b>`:''}${parts.length?`<div style="font-size:12px;color:#3a7d68;margin-top:2px">${esc(parts.join(' · '))}</div>`:''}</div>1131 <div class="mx-sum-actions" style="display:flex;gap:8px">1132 <button class="mx-sum-btn" onclick="editMatcher()">✏️ Modifier mes critères</button>1133 <button class="mx-sum-btn" onclick="clearMatcher()">Voir tous les logements</button>1134 </div>1135 </div>${list.length && (c.types.length||c.budget||c.etages.length||c.pets.length||(c.extras&&c.extras.length)) ? '<div class="mx-legend"><span class="lg lg-p">Parfait</span><span class="lg lg-e">Très bon</span><span class="lg lg-g">Bon</span></div>' : ''}`;1136}11371138// Sélection partagée par l'admin : /?apps=az101,az102 → n'affiche que ces logements1139function applySharedSelection() {1140 const appsParam = new URLSearchParams(location.search).get('apps');1141 if (!appsParam) return;1142 const want = new Set(appsParam.split(',').map(s => normIdApp(s)).filter(Boolean));1143 if (!want.size) return;1144 allListings = allListings.filter(l => {1145 const base = normIdApp(l.id_app || l.ID_app);1146 // cartes d'immeuble éclatées : id_app suffixé « base-N » ou « base-<modèle> »1147 return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1148 });1149 showSelectionBanner(allListings.length);1150}1151function showSelectionBanner(n, title) {1152 const el = document.getElementById('selection-banner');1153 if (!el) return;1154 el.style.display = 'block';1155 const back = '<a href="/" style="font-size:13px;color:#E67700;font-weight:600;text-decoration:none;border:1px solid #FB8C00;border-radius:8px;padding:6px 12px;background:#fff;white-space:nowrap">Voir tous les logements</a>';1156 const lead = title || '📋 Sélection personnalisée';1157 el.innerHTML = `<div style="background:#FFECCF;border:1px solid #FB8C00;border-radius:14px;padding:14px 18px;margin-bottom:18px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">1158 <div style="font-size:15px;color:#E67700">${n ? `<b>${lead}</b> — ${n} logement${n>1?'s':''} disponible${n>1?'s':''}.` : 'Aucun logement disponible pour l\'instant.'}</div>${back}</div>`;1159}1160// Lien propriétaire : /?p=<token> → n'affiche que les logements ACTIFS de ce propriétaire1161async function applyOwnerSelection() {1162 const token = new URLSearchParams(location.search).get('p');1163 if (!token) return;1164 try {1165 const resp = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_listings`, {1166 method: 'POST',1167 headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1168 body: JSON.stringify({ p_token: token.trim() })1169 });1170 const rows = await resp.json();1171 if (!Array.isArray(rows)) return;1172 const want = new Set(rows.map(r => normIdApp(r.id_app)).filter(Boolean));1173 allListings = allListings.filter(l => {1174 const base = normIdApp(l.id_app || l.ID_app);1175 // cartes d'immeuble éclatées : id_app suffixé « base-N »1176 return want.has(base) || [...want].some(w => base.startsWith(w + '-'));1177 });1178 // Coordonnées du propriétaire (profil) → affichées sur TOUTES ses fiches via le lien.1179 try {1180 const cr = await fetch(`${SUPABASE_URL}/rest/v1/rpc/owner_public_contact`, {1181 method: 'POST',1182 headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },1183 body: JSON.stringify({ p_token: token.trim() })1184 });1185 const cd = await cr.json();1186 if (Array.isArray(cd) && cd[0]) window.__ownerContact = cd[0];1187 } catch (_) {}1188 showOwnerTopLink(); // pas de grosse bannière : juste un lien discret en haut1189 } catch (_) {}1190}1191// Lien discret « Voir tous les logements » en haut, quand on est sur un lien propriétaire.1192function showOwnerTopLink(){1193 const actions = document.getElementById('map-toggle-btn') && document.getElementById('map-toggle-btn').parentElement;1194 if (!actions || document.getElementById('owner-alllink')) return;1195 const a = document.createElement('a');1196 a.id = 'owner-alllink';1197 a.href = '/';1198 a.className = 'map-toggle';1199 a.title = 'Voir l’ensemble des logements du site';1200 a.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg> Voir tous les logements';1201 a.style.cssText = 'background:#FB8C00;color:#fff;border-color:#FB8C00;text-decoration:none;white-space:nowrap;order:-1';1202 actions.insertBefore(a, actions.firstChild);1203}1204// Un immeuble (fiche_type immeuble) est éclaté en UNE carte par unité DISPONIBLE : photos du1205// modèle lié (★ en tête, sinon album de l'immeuble), type/prix/étage/caractéristiques de l'unité.1206// Chaque carte est une annonce virtuelle (recherche + matcher). Les unités louées n'apparaissent pas.1207function cleanIdApp(s){ return String(s || '').replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1208// Normalise un id_app pour la COMPARAISON des liens partagés : minuscules, sans espaces,1209// fractions converties → un ancien lien « …-3½ » (%C2%BD) correspond au nouvel « …-3.5 ».1210function normIdApp(s){ return String(s || '').toLowerCase().replace(/\s+/g, '').replace(/½/g,'.5').replace(/¼/g,'.25').replace(/¾/g,'.75'); }1211function expandBuildingCards(list){1212 const out = [];1213 (list || []).forEach(l => {1214 const avail = Array.isArray(l.units) ? l.units.map((u, idx) => ({ u, idx })).filter(x => x.u && isAvail(x.u)) : [];1215 const models = Array.isArray(l.photo_variants) ? l.photo_variants : [];1216 // Éclatement en cartes UNIQUEMENT si au moins une unité dispo est liée à un modèle de photo.1217 // Sinon l'immeuble garde son plan (une carte + le visuel des unités par étage).1218 const anyModel = avail.some(x => x.u.photoModel && models.some(m => m && m.label === x.u.photoModel));1219 if (l.fiche_type === 'appartement' || !avail.length || !anyModel){ out.push(l); return; }1220 // Regroupe les unités disponibles PAR MODÈLE de photo : une seule carte par modèle1221 // (plusieurs unités identiques = un seul choix affiché). Les unités sans modèle1222 // gardent une carte chacune.1223 const groups = new Map();1224 avail.forEach(({ u, idx }) => {1225 const hasModel = u.photoModel && models.some(m => m && m.label === u.photoModel);1226 const key = hasModel ? ('m:' + u.photoModel) : ('u:' + idx);1227 if (!groups.has(key)) groups.set(key, { label: hasModel ? u.photoModel : '', items: [] });1228 groups.get(key).items.push({ u, idx });1229 });1230 groups.forEach(g => {1231 const items = g.items, first = items[0].u;1232 const model = g.label ? models.find(m => m && m.label === g.label) : null;1233 const mp = model ? modelPhotos(model) : [];1234 const photos = mp.length ? mp : (Array.isArray(l.photos) && l.photos.length ? l.photos : []);1235 const prices = items.map(x => parseFloat(x.u.prix)).filter(p => !isNaN(p) && p > 0);1236 const minPrix = prices.length ? Math.min(...prices) : (first.prix != null ? first.prix : l.loyer);1237 const types = [...new Set(items.map(x => x.u.type).filter(Boolean))];1238 const typeLabel = types.length === 1 ? types[0] : ((model && model.grandeur) || types.join(' / '));1239 const single = items.length === 1;1240 const vl = Object.assign({}, l);1241 vl._virtual = true; vl._parentPk = l.pk; vl._modelLabel = g.label;1242 vl._unitIdx = items[0].idx; vl._unitId = single ? (first.id || null) : null;1243 vl._groupUnitIds = items.map(x => x.u.id).filter(Boolean);1244 vl._unitCount = items.length;1245 vl._bldUnits = l.units; // plan complet de l'immeuble sur la fiche1246 vl.pk = (l.pk != null ? l.pk : (l.id_app || 'x')) + ':' + (g.label ? ('m' + g.label) : ('u' + items[0].idx));1247 vl.id_app = cleanIdApp((l.id_app || l.ID_app || '') + '-' + (g.label || (items[0].idx + 1)));1248 vl.ID_app = vl.id_app;1249 vl.titre = (l.titre || '') + (typeLabel ? ' — ' + typeLabel : '');1250 vl.grandeur = typeLabel || l.grandeur || '';1251 if (minPrix != null) vl.loyer = minPrix;1252 vl.etage = single ? (first.etage || '') : '';1253 vl.units = null; vl.variants = null; vl.photo_variants = null;1254 vl.photos = photos.slice(); vl.main_photo = photos[0] || null;1255 // Meublé virtuellement : rendu propre au modèle si présent, sinon celui de la fiche (repli)1256 vl.staged_photos = (model && Array.isArray(model.staged) && model.staged.length) ? model.staged.slice() : (l.staged_photos || null);1257 vl.carac = single ? (first.carac || null) : null;1258 vl.description = single ? (first.caract || l.description || '') : (l.description || '');1259 vl.chambres = first.chambres; vl.sdb = first.sdb;1260 out.push(vl);1261 });1262 });1263 return out;1264}1265async function loadListings() {1266 try {1267 const resp = await fetch(LISTINGS_URL, {1268 headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON }1269 });1270 const json = await resp.json();1271 allListings = (Array.isArray(json) ? json : []).filter(l => l.pk || l.id);1272 // Compat : l'ancien code utilise l.ID_app ; en base la colonne s'appelle id_app1273 allListings.forEach(l => { if (l.ID_app === undefined) l.ID_app = l.id_app; });1274 allListings = expandBuildingCards(allListings); // une carte par unité disponible (immeubles)1275 // Les fiches créées par des propriétaires ne s'affichent PAS sur le site Gestion ISR ;1276 // elles ne sont visibles que via le lien de partage du propriétaire (?p=<token>).1277 if (!new URLSearchParams(location.search).get('p')) {1278 allListings = allListings.filter(l => !l.created_by_owner);1279 }1280 applySharedSelection();1281 await applyOwnerSelection();1282 filtered = [...allListings];1283 buildVilleFilter();1284 renderAgentNewRail();1285 render();1286 // Lien profond : ?fiche=<réf> ouvre directement la fiche de l'annonce1287 { const fiche = new URLSearchParams(location.search).get('fiche');1288 if (fiche){ const f = String(fiche).toLowerCase();1289 let idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase() === f);1290 if (idx < 0) idx = allListings.findIndex(l => String(l.id_app || l.ID_app || '').toLowerCase().split('-')[0] === f);1291 if (idx >= 0) openDetail(idx);1292 }1293 }1294 } catch(e) {1295 document.getElementById('grid').innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:4rem;color:#888">Impossible de charger les logements.</div>';1296 }1297}12981299function buildVilleFilter() {1300 const villes = [...new Set(allListings.map(l => l.ville).filter(Boolean))].sort();1301 const sel = document.getElementById('f-ville');1302 if (sel) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });1303 // Dropdown « Secteur visé » du matcher (mêmes secteurs = villes)1304 const ms = document.getElementById('mx-secteur');1305 if (ms) villes.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; ms.appendChild(o); });1306}13071308function normalizeDispo(d) {1309 if (!d) return '';1310 const s = d.toLowerCase();1311 if (s.includes('maintenant') || s.includes('dès')) return 'maintenant';1312 if (s.includes('mars')) return 'mars';1313 if (s.includes('mai')) return 'mai';1314 if (s.includes('juin')) return 'juin';1315 if (s.includes('juillet')) return 'juillet';1316 if (s.includes('août') || s.includes('aout')) return 'aout';1317 return '';1318}13191320let bedFilter = '';1321function grandeurToBedrooms(g){1322 const s = String(g || '');1323 const m = s.match(/(\d+)\s*(?:½|1\/2|\.5)?/);1324 if (m){ const n = parseInt(m[1], 10); if (!isNaN(n) && n >= 3 && n <= 12) return n - 2; }1325 return null;1326}1327function bedroomsOf(l){1328 const nums = new Set();1329 const addNum = v => { const m = String(v == null ? '' : v).match(/\d+/); if (m){ const n = parseInt(m[0], 10); if (!isNaN(n)) nums.add(n); } };1330 const addGrand = g => { const b = grandeurToBedrooms(g); if (b != null) nums.add(b); };1331 if (l.chambres != null && l.chambres !== '') addNum(l.chambres); else addGrand(l.grandeur);1332 if (Array.isArray(l.units)) l.units.forEach(u => { if (u && u.chambres != null && u.chambres !== '') addNum(u.chambres); else if (u) addGrand(u.type); });1333 if (Array.isArray(l.variants)) l.variants.forEach(v => { if (v && v.chambres != null && v.chambres !== '') addNum(v.chambres); else if (v) addGrand(v.grandeur || v.type); });1334 return nums;1335}1336function isMaison(l){1337 const has = g => /maison/i.test(String(g || ''));1338 if (has(l.grandeur)) return true;1339 if (Array.isArray(l.units) && l.units.some(u => has(u && u.type))) return true;1340 if (Array.isArray(l.variants) && l.variants.some(v => has(v && (v.grandeur || v.type)))) return true;1341 return false;1342}1343function matchesBedrooms(l, sel){1344 if (!sel) return true;1345 if (sel === 'maison') return isMaison(l);1346 const nums = bedroomsOf(l);1347 if (!nums.size) return false;1348 if (sel === '4') return [...nums].some(n => n >= 4);1349 return nums.has(parseInt(sel, 10));1350}1351function setBedrooms(v){1352 bedFilter = v;1353 document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === v));1354 applyFilters();1355}1356function etiquetteColor(e){1357 const s = String(e || '').toLowerCase();1358 if (s.includes('promo')) return '#F5871F';1359 if (s.includes('derni')) return '#DC2626';1360 if (s.includes('psl')) return '#7C3AED';1361 if (s.includes('internet')) return '#0EA5E9';1362 if (s.includes('coeur') || s.includes('cœur')) return '#DB2777';1363 if (s.includes('garage')) return '#64748B';1364 if (s.includes('meubl')) return '#0D9488';1365 return '#F59E0B';1366}1367function applyFilters() {1368 const ville = document.getElementById('f-ville').value;1369 const taille = document.getElementById('f-taille').value;1370 const etage = document.getElementById('f-etage').value;1371 const animaux = document.getElementById('f-animaux').value;1372 const dispo = document.getElementById('f-dispo').value;1373 const etiquette = document.getElementById('f-etiquette').value;1374 const search = document.getElementById('f-search').value.toLowerCase().trim();1375 const pmin = parseFloat(document.getElementById('f-pmin').value) || 0;1376 const pmax = parseFloat(document.getElementById('f-pmax').value) || Infinity;1377 filtered = allListings.filter(l => {1378 if (etiquette && (l.etiquette||'').toLowerCase() !== etiquette.toLowerCase()) return false;1379 if (search) {1380 const haystack = ((l.titre||'')+(l.ville||'')+(l.description||'')+(l.ID_app||'')).toLowerCase();1381 if (!haystack.includes(search)) return false;1382 }1383 if (ville && l.ville !== ville) return false;1384 if (bedFilter && !matchesBedrooms(l, bedFilter)) return false;1385 if (taille && !variantMatchesGrandeur(l, taille)) return false;1386 if (etage && !variantMatchesEtage(l, etage)) return false;1387 if (animaux) {1388 const a = (l.animaux || '').toLowerCase();1389 if (animaux === 'Aucun' && !a.includes('aucun')) return false;1390 if (animaux === 'Chat' && !a.includes('chat')) return false;1391 if (animaux === 'Petits' && !a.includes('petit')) return false;1392 if (animaux === 'Animaux' && (!a || a.includes('aucun'))) return false;1393 }1394 if (dispo && !variantMatchesDispo(l, dispo)) return false;1395 const loyers = loyerSet(l);1396 if (loyers.length && !loyers.some(x => x >= pmin && x <= pmax)) return false;1397 return true;1398 });1399 // Tri1400 const tri = document.getElementById('f-tri').value;1401 if (tri === 'prix-asc') {1402 filtered.sort((a,b) => minLoyer(a) - minLoyer(b));1403 } else if (tri === 'prix-desc') {1404 filtered.sort((a,b) => maxLoyer(b) - maxLoyer(a));1405 } else if (tri === 'dispo') {1406 filtered.sort((a,b) => dispoOrderIndex(a) - dispoOrderIndex(b));1407 }1408 render(); renderTags();1409}14101411function resetFilters() {1412 ['f-ville','f-taille','f-etage','f-animaux','f-dispo','f-etiquette','f-tri'].forEach(id => document.getElementById(id).value = '');1413 document.getElementById('f-pmin').value = '';1414 document.getElementById('f-pmax').value = '';1415 document.getElementById('f-search').value = '';1416 bedFilter = '';1417 document.querySelectorAll('.bed-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-bed') === ''));1418 filtered = [...allListings];1419 render(); renderTags();1420}14211422function renderTags() {1423 const tags = [];1424 const ville = document.getElementById('f-ville').value;1425 const taille = document.getElementById('f-taille').value;1426 const etage = document.getElementById('f-etage').value;1427 const animaux = document.getElementById('f-animaux').value;1428 const dispo = document.getElementById('f-dispo').value;1429 const etiquette = document.getElementById('f-etiquette').value;1430 const pmin = document.getElementById('f-pmin').value;1431 const pmax = document.getElementById('f-pmax').value;1432 const dispoLabels = {'maintenant':'Dès maintenant','mars':'Mars 2026','mai':'Mai 2026','juin':'Juin 2026','juillet':'Juillet 2026','aout':'Août 2026'};1433 if (ville) tags.push({label:'Ville : '+ville, clear:()=>{document.getElementById('f-ville').value='';applyFilters();}});1434 if (bedFilter) tags.push({label: bedFilter==='maison' ? 'Maison' : ((bedFilter==='4'?'4+':bedFilter)+' chambre'+(bedFilter==='1'?'':'s')), clear:()=>setBedrooms('')});1435 if (taille) tags.push({label:taille, clear:()=>{document.getElementById('f-taille').value='';applyFilters();}});1436 if (etage) tags.push({label:etageLabel(etage), clear:()=>{document.getElementById('f-etage').value='';applyFilters();}});1437 if (animaux) tags.push({label:animaux, clear:()=>{document.getElementById('f-animaux').value='';applyFilters();}});1438 if (etiquette) tags.push({label:'À la une : '+etiquette, clear:()=>{document.getElementById('f-etiquette').value='';applyFilters();}});1439 if (dispo) tags.push({label:'Dispo : '+(dispoLabels[dispo]||dispo), clear:()=>{document.getElementById('f-dispo').value='';applyFilters();}});1440 if (pmin||pmax) tags.push({label:'Prix : '+(pmin||'0')+' — '+(pmax||'∞')+' $', clear:()=>{document.getElementById('f-pmin').value='';document.getElementById('f-pmax').value='';applyFilters();}});1441 const row = document.getElementById('tags-row');1442 // Filtrer les tags pour exclure étiquette (géré par les chips)1443 const otherTags = tags.filter(t => !t.label.startsWith('À la une'));1444 1445 // Toujours afficher les chips1446 let html = `<div class="quick-filters" id="quick-filters">1447 <span style="font-size:11px;color:#aaa;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;margin-right:4px">À la une :</span>1448 <button class="quick-filter-btn" onclick="setEtiquette('Promotion')">🏷️ Promotion</button>1449 <button class="quick-filter-btn" onclick="setEtiquette('Nouveauté')">✨ Nouveauté</button>1450 <button class="quick-filter-btn" onclick="setEtiquette('PSL Accepté')">🔥 PSL Accepté</button>1451 <button class="quick-filter-btn" onclick="setEtiquette('Dernière unité')">⚡ Dernière unité</button>1452 <button class="quick-filter-btn" onclick="setEtiquette('Internet inclus')">🛜 Internet inclus</button>1453 <button class="quick-filter-btn" onclick="setEtiquette('Coup de coeur')">❤️ Coup de coeur</button>1454 <button class="quick-filter-btn" onclick="setEtiquette('Garage')">🚗 Garage</button>1455 </div>`;1456 1457 if (otherTags.length) {1458 html += otherTags.map((t,i)=>`<span class="tag">${esc(t.label)}<button onclick="clearTag(${i})">×</button></span>`).join('');1459 }1460 1461 row.innerHTML = html;1462 window._tagClears = otherTags.map(t=>t.clear);1463 updateQuickFilters();1464}14651466function clearTag(i){window._tagClears[i]();}14671468function setEtiquette(val) {1469 const sel = document.getElementById('f-etiquette');1470 if (sel.value === val) {1471 sel.value = '';1472 } else {1473 sel.value = val;1474 }1475 applyFilters();1476}14771478function updateQuickFilters() {1479 const val = document.getElementById('f-etiquette').value;1480 document.querySelectorAll('.quick-filter-btn').forEach(btn => {1481 const btnVal = btn.getAttribute('onclick').match(/setEtiquette\('(.+?)'\)/);1482 if (btnVal) {1483 btn.classList.toggle('active', btnVal[1] === val);1484 }1485 });1486}14871488window.refreshHearts = function(){1489 document.querySelectorAll('.card-heart[data-app]').forEach(h => {1490 h.classList.toggle('active', !!(window.__favs && window.__favs.has(h.getAttribute('data-app'))));1491 });1492};14931494// Nouveautés (vue agent) : annonces créées depuis ≤ 10 jours, avec le nombre de jours affiché.1495function renderAgentNewRail(){1496 const sec = document.getElementById('agent-new-rail'), track = document.getElementById('an-scroll');1497 if (!sec || !track) return;1498 if (!window.__isStaff){ sec.style.display = 'none'; return; } // agent uniquement1499 const sp = new URLSearchParams(location.search);1500 if (sp.get('apps') || sp.get('p')){ sec.style.display = 'none'; return; }1501 const now = Date.now(); const seenPk = new Set();1502 const items = (allListings || []).map(l => ({ l, days: l.created_at ? Math.floor((now - new Date(l.created_at).getTime()) / 86400000) : 999 }))1503 .filter(({ l, days }) => {1504 if (days < 0 || days > 10) return false;1505 if (String(l.etiquette || '').trim().toLowerCase() === 'loué') return false;1506 const pk = l._virtual ? l._parentPk : l.pk;1507 if (pk == null || seenPk.has(pk)) return false; seenPk.add(pk);1508 return true;1509 }).sort((a, b) => a.days - b.days).slice(0, 20);1510 if (!items.length){ sec.style.display = 'none'; return; }1511 const card = ({ l, days }) => {1512 const id = String(l.id_app || l.ID_app || '');1513 const photo = l.main_photo || (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : '');1514 const mn = minLoyer(l);1515 const meta = [grandeurDisplay(l), l.ville].filter(Boolean).join(' · ');1516 const prix = mn ? ((hasVariants(l) || hasUnits(l)) ? 'dès ' : '') + Number(mn).toLocaleString('fr-CA') + ' $' : '';1517 const img = photo ? `<img class="fr-img" loading="lazy" decoding="async" src="${esc(simg(photo, 420, 62))}" alt="">` : '<div class="fr-img"></div>';1518 const badge = `<span class="fr-days">${days < 1 ? 'Nouveau' : days + 'j'}</span>`;1519 return `<div class="fr-card" data-newapp="${esc(id)}"><div style="position:relative">${img}${badge}</div><div class="fr-body"><div class="fr-t">${esc(l.titre || id)}</div><div class="fr-m">${esc(meta)}</div><div class="fr-p">${esc(prix)}</div></div></div>`;1520 };1521 track.innerHTML = items.map(card).join('');1522 sec.style.display = '';1523 track.querySelectorAll('[data-newapp]').forEach(el => el.addEventListener('click', () => {1524 const id = el.getAttribute('data-newapp');1525 const idx = (allListings || []).findIndex(x => String(x.id_app || x.ID_app || '') === id);1526 if (idx >= 0) openDetail(idx);1527 }));1528}1529async function render() {1530 const g = document.getElementById('grid');1531 const n = filtered.length;1532 // Total réel de logements = somme des unités (les variantes comptent pour leur nombre d'unités)1533 const units = filtered.reduce((s,l) => s + (hasVariants(l) ? Math.max(totalUnits(l), 1) : 1), 0);1534 document.getElementById('count-badge').textContent = units+' logement'+(units!==1?'s':'')+' disponible'+(units!==1?'s':'');1535 document.getElementById('results-line').textContent = units+' logement'+(units!==1?'s':'')+' trouvé'+(units!==1?'s':'') + (units!==n ? ` · ${n} annonce${n!==1?'s':''}` : '');1536 if (!n){g.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:4rem;color:#888"><p style="font-size:2rem;margin-bottom:1rem">🔍</p><p>Aucun logement ne correspond.</p></div>';return;}1537 1538 // Tri par défaut prix croissant (sauf en mode correspondances, trié par pertinence)1539 if (!matchCriteria && !document.getElementById('f-tri').value) {1540 filtered.sort((a,b) => minLoyer(a) - minLoyer(b));1541 }15421543 const cards = filtered.map((l, idx) => {1544 // Vignette SYNCHRONE : photos Supabase déjà chargées → aucun appel réseau bloquant.1545 const thumb = (l.main_photo && Array.isArray(l.photos) && l.photos.includes(l.main_photo))1546 ? l.main_photo1547 : (Array.isArray(l.photos) && l.photos.length ? l.photos[0] : null);1548 const loyer = parseFloat(l.loyer)||0;1549 const etageNorm = normalizeEtage(getEtage(l));1550 const etageTag = etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : '';1551 // Immeuble à unités : badge « X dispo · Y unités » plutôt qu'un étage top-level trompeur.1552 const nU = hasUnits(l) ? l.units.length : 0;1553 const dispoU = hasUnits(l) ? l.units.filter(u => isAvail(u)).length : 0;1554 const etageCard = hasUnits(l)1555 ? `<span class="card-tag ct-variantes">${dispoU} dispo · ${nU} unité${nU>1?'s':''}</span>`1556 : (l._unitCount > 11557 ? `<span class="card-tag ct-variantes">${l._unitCount} logements disponibles</span>`1558 : (etageNorm ? `<span class="card-tag ct-etage">${etageLabel(etageNorm)}</span>` : ''));1559 const etiqLow = (l.etiquette || '').toLowerCase();1560 const cardClass = etiqLow === 'promotion' ? ' card-promotion'1561 : etiqLow === 'psl accepté' ? ' card-psl'1562 : etiqLow === 'dernière unité' ? ' card-derniere'1563 : etiqLow === 'coup de coeur' ? ' card-coupdecoeur'1564 : '';1565 const mm = matchCriteria ? (matchByApp[l.ID_app || l.id_app] || null) : null;1566 const mTier = (mm && TIER[mm.tier]) ? TIER[mm.tier] : null;1567 const tierCls = (mTier && mTier.cls) ? ' ' + mTier.cls : '';1568 const tierBadge = (mTier && mTier.label) ? `<div class="mx-tier-badge ${mTier.cls}">${mTier.label}</div>` : '';1569 const matchChips = mm ? `<div class="mx-chips-res">${mm.chips.map(c => `<span class="mx-chip ${c.miss?'miss':c.soft?'soft':''}">${esc(c.txt)}</span>`).join('')}</div>` : '';1570 return `<div class="card${cardClass}${tierCls}" onclick="openDetail(${allListings.indexOf(l)})">1571 <div class="card-img">1572 ${thumb?`<img src="${esc(simg(thumb,700,66))}" loading="lazy" alt="${esc(l.titre)}" onerror="this.parentNode.innerHTML='<div class=\\'card-img-placeholder\\'><svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' stroke=\\'currentColor\\' stroke-width=\\'1.2\\'><path d=\\'M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z\\'/><path d=\\'M9 21V12h6v9\\'/></svg></div>'">`:`<div class="card-img-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><path d="M9 21V12h6v9"/></svg></div>`}1573 <div class="badge-row">1574 ${l.etiquette&&l.etiquette.toLowerCase()!=='loué'&&l.etiquette.toLowerCase()!=='nouveauté'?`<span class="badge badge-etiquette" style="background:${etiquetteColor(l.etiquette)}">${l.etiquette.toLowerCase()==='coup de coeur'?'Coup de coeur ❤️':esc(l.etiquette)}</span>`:''}1575 </div>1576 ${l.etiquette&&l.etiquette.toLowerCase()==='loué'?`<div class="ribbon-loue">LOUÉ</div>`:''}1577 ${l.etiquette&&l.etiquette.toLowerCase()==='nouveauté'?`<div class="ribbon-nouveau">NOUVEAUTÉ</div>`:''}1578 ${(Array.isArray(l.staged_photos)&&l.staged_photos.length)?`<button class="card-staged" onclick="event.stopPropagation();window.openStaged&&window.openStaged(${allListings.indexOf(l)})" title="✨ Voir l'appartement en aménagement virtuel">✨</button>`:''}1579 <button class="card-heart${window.__favs&&window.__favs.has(l.ID_app||l.id_app||'')?' active':''}" data-app="${esc(l.ID_app||l.id_app||'')}" onclick="event.stopPropagation();window.toggleFavorite&&window.toggleFavorite(this.getAttribute('data-app'),this)" title="Ajouter aux favoris">❤</button>1580 </div>1581 <div class="card-body">1582 ${tierBadge}1583 <div class="card-ville">📍 ${esc(l.ville)}</div>1584 <div class="card-title">${esc(l.titre)}</div>1585 <div class="card-tags">1586 <span class="card-tag ct-size">${esc(grandeurDisplay(l))}</span>1587 <span class="card-tag ct-date">${esc(dispoDisplay(l))}</span>1588 ${hasVariants(l) ? `<span class="card-tag ct-variantes">${l.variants.length} modèle${l.variants.length>1?'s':''}${totalUnits(l)?` · ${totalUnits(l)} unité${totalUnits(l)>1?'s':''}`:''}</span>` : etageCard}1589 </div>1590 ${Array.isArray(l.photo_variants)&&l.photo_variants.length?`<button class="card-models-btn" onclick="event.stopPropagation();openVariantPhotos(${allListings.indexOf(l)})">📷 Voir les modèles (${l.photo_variants.length})</button>`:''}1591 ${matchChips}1592 <div class="card-footer">1593 <div class="price">${hasVariants(l)?`<span class="price-from">dès </span>`:''}${minLoyer(l).toLocaleString('fr-CA')} $<span> / mois</span></div>1594 <button class="btn-visite-card" onclick="event.stopPropagation();openVisiteFromCardIdx(${allListings.indexOf(l)})">Demander une visite</button>1595 </div>1596 </div>1597 </div>`;1598 });1599 if (matchCriteria){1600 const counts = {}; filtered.forEach(l => { const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok'; counts[t] = (counts[t]||0) + 1; });1601 let out = '', last = null;1602 filtered.forEach((l, idx) => {1603 const t = (matchByApp[l.ID_app||l.id_app]||{}).tier || 'ok';1604 if (t !== last){ last = t; out += matchGroupHeader(t, counts[t]); }1605 out += cards[idx];1606 });1607 g.innerHTML = out;1608 } else {1609 g.innerHTML = cards.join('');1610 }1611}1612function matchGroupHeader(tier, n){1613 const info = { perfect:{label:'Correspondances parfaites',color:'#0B7A5E'}, excellent:{label:'Très bonnes correspondances',color:'#FB8C00'}, good:{label:'Bonnes correspondances',color:'#B45309'}, ok:{label:'Autres logements compatibles',color:'#8a909a'} }[tier] || { label:'', color:'#8a909a' };1614 return `<div class="match-group-header" style="--gc:${info.color}"><span>${esc(info.label)}</span><span class="mgh-count">${n}</span></div>`;1615}16161617function renderDetailVariants(l){1618 const sec = document.getElementById('d-variants-sec');1619 const box = document.getElementById('d-variants');1620 if (!hasVariants(l)){ sec.style.display = 'none'; box.innerHTML = ''; return; }1621 const rows = l.variants.map((v, i) => {1622 const label = v.label || String.fromCharCode(65 + i);1623 const grandeur = v.grandeur || l.grandeur || '';1624 const sup = (v.superficie != null && v.superficie !== '') ? Number(v.superficie).toLocaleString('fr-CA') + ' pi²' : '—';1625 const floors = variantFloors(v).filter(f => Number(f.qte) > 0);1626 const et = floors.length1627 ? floors.map(f => { const p = (f.loyer != null && f.loyer !== '' && !isNaN(parseFloat(f.loyer))) ? ' · ' + parseFloat(f.loyer).toLocaleString('fr-CA') + ' $' : ''; return `${etageLabel(normalizeEtage(f.etage)) || esc(f.etage)} <b>×${Number(f.qte)}</b>${p}`; }).join('<br>')1628 : '—';1629 const tot = variantQte(v);1630 const dispo = v.dispo || '—';1631 const eff = variantEffPrices(v);1632 const prix = eff.length ? (Math.min(...eff) === Math.max(...eff) ? Math.min(...eff).toLocaleString('fr-CA') + ' $' : Math.min(...eff).toLocaleString('fr-CA') + '–' + Math.max(...eff).toLocaleString('fr-CA') + ' $') : '—';1633 const st = v.statut === 'Loué' ? 'Loué' : 'Disponible';1634 const stB = `<span style="display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:10px;margin-left:6px;${st==='Loué'?'background:#FCE4E4;color:#B42318':'background:#FFECCF;color:#E67700'}">${st}</span>`;1635 return `<tr>1636 <td><span class="v-label">${esc(label)}</span>${grandeur ? esc(grandeur) : ''}${stB}${v.note ? `<div class="v-note">${esc(v.note)}</div>` : ''}</td>1637 <td>${esc(sup)}</td>1638 <td>${et}</td>1639 <td>${esc(dispo)}</td>1640 <td>${tot ? esc(tot) + (tot > 1 ? ' unités' : ' unité') : '—'}</td>1641 <td class="v-price">${esc(prix)}</td>1642 </tr>`;1643 }).join('');1644 box.innerHTML = `<div class="vtable-scroll"><table class="vtable">1645 <thead><tr><th>Modèle</th><th>Superficie</th><th>Étages disponibles</th><th>Disponibilité</th><th>Total</th><th>Loyer</th></tr></thead>1646 <tbody>${rows}</tbody></table></div>`;1647 sec.style.display = '';1648}1649// ---- PHASE 2 — Immeuble & unités (vue agent) ----1650const FLOOR_ORDER = ['Penthouse','5e étage','4e étage','3e étage','2e étage','1er étage','Rez-de-chaussée','Demi sous-sol','Sous-sol'];1651function orderFloorsTop(names){ const idx = n => { const i = FLOOR_ORDER.indexOf(n); return i < 0 ? 50 : i; }; return [...names].sort((a,b) => idx(a) - idx(b)); }1652// Libellé court d'un immeuble pour les boutons de bascule : le texte entre parenthèses1653// du titre (« … (Immeuble 1635) » → « 1635 »), sinon la référence.1654function groupImmeubleLabel(s){1655 const t = String((s && s.titre) || '');1656 const m = t.match(/\(([^)]+)\)/);1657 if (m){ const inner = m[1].replace(/immeuble\s*/i, '').trim(); return inner || m[1].trim(); }1658 return (s && (s.id_app || s.ID_app)) || 'Immeuble';1659}1660function renderAgentBuilding(l){1661 const sec = document.getElementById('d-building-sec'), box = document.getElementById('d-building');1662 if (!sec || !box) return;1663 // Plan affiché pour une fiche IMMEUBLE ayant des unités — y compris sur une carte d'appartement1664 // (virtuelle), via les unités de l'immeuble parent (_bldUnits). Une fiche « appartement » jamais.1665 const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : null);1666 if (l.fiche_type === 'appartement' || !units){ sec.style.display = 'none'; return; }1667 const virt = !!l._virtual;1668 const vs = document.getElementById('d-variants-sec'); if (vs) vs.style.display = 'none'; // le visuel remplace le tableau des modèles1669 const h3 = sec.querySelector('h3');1670 if (h3) h3.innerHTML = virt1671 ? '🏢 Appartements de l\'immeuble <span style="font-size:12px;font-weight:400;color:#8a8f96">(simple clic : changer de modèle · double-clic : détails)</span>'1672 : (window.__isStaff1673 ? '🏢 Immeuble — unités <span style="font-size:12px;font-weight:400;color:#8a5a00">(vue agent · clique une unité)</span>'1674 : '🏢 Unités disponibles <span style="font-size:12px;font-weight:400;color:#8a8f96">(clique une unité pour les détails)</span>');1675 const byFloor = {}; units.forEach((u,i) => { const f = u.etage || 'Autre'; (byFloor[f] = byFloor[f] || []).push({ u, i }); });1676 const floors = orderFloorsTop(Object.keys(byFloor));1677 const dispo = units.filter(u => isAvail(u)).length;1678 const tileFn = ({ u, i }) => {1679 const loue = !isAvail(u);1680 const [bg, bd] = unitColors(u.statut);1681 // Le prix n'est jamais montré pour un logement non disponible.1682 const prix = (!loue && u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $' : '';1683 const staff = !!window.__isStaff;1684 // Statut affiché en infobulle : le public ne voit pas « réservé » (montré « Disponible »).1685 const tipStatut = staff ? (u.statut || (loue ? 'Loué' : 'Disponible')) : (u.statut === 'Loué' ? 'Loué' : (u.statut === 'En construction' ? 'En construction' : 'Disponible'));1686 // Le numéro d'appartement n'est visible que pour l'agent ; le candidat voit type + prix.1687 const line1 = staff ? (u.numero || ('App ' + (i + 1))) : (u.type || 'Logement');1688 const line2 = staff ? [u.type || '', prix].filter(Boolean).join(' · ') : prix;1689 const tip = [staff ? (u.numero || '') : '', u.type || '', prix, tipStatut].filter(Boolean).join(' · ');1690 // Sur une carte virtuelle : seuls les appartements DISPONIBLES sont cliquables (ils ont une fiche).1691 // Le staff peut TOUJOURS cliquer (pour changer le statut, même un logement loué).1692 const locked = staff ? false : (virt ? loue : loue);1693 // Sur une carte virtuelle : toutes les unités du MODÈLE ouvert sont marquées « courant ».1694 const cur = virt && (l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx));1695 return `<div class="d-unit mini${locked?' d-unit-locked':''}${cur?' d-unit-cur':''}" data-uidx="${i}" title="${esc(tip)}${u.statut === 'Loué' ? ' — déjà loué' : ''}" style="background:${bg};border-color:${bd}">1696 <div class="mn">${esc(line1)}</div>1697 <div class="mm">${line2 ? esc(line2) : ' '}</div>1698 </div>`;1699 };1700 const floorsHtml = floors.map(f => {1701 // Regroupe par position (Avant/Arrière…) si au moins une unité de l'étage en a une.1702 const groups = []; const gi = {};1703 byFloor[f].forEach(cell => { const p = (cell.u.position || ''); if (!(p in gi)){ gi[p] = groups.length; groups.push({ pos: p, cells: [] }); } groups[gi[p]].cells.push(cell); });1704 const anyPos = groups.some(g => g.pos);1705 const inner = anyPos1706 ? groups.map(g => `<div class="d-posgroup">${g.pos ? `<div class="d-poslbl">${esc(g.pos)}</div>` : ''}<div class="d-postiles">${g.cells.map(tileFn).join('')}</div></div>`).join('')1707 : byFloor[f].map(tileFn).join('');1708 return `<div class="d-bld-floor"><div class="d-floor-lbl">${esc(f)}</div><div class="d-units compact${anyPos?' d-hasgroups':''}">${inner}</div></div>`;1709 }).join('');1710 const bigBtn = units.length > 8 ? `<button class="d-bld-expand" id="d-bld-expand">🔍 Voir le plan en grand</button>` : '';1711 // Bascule entre immeubles liés (même valeur groupe_immeuble) — boutons au-dessus du plan1712 let switchHtml = '';1713 const grp = String(l.groupe_immeuble || '').trim();1714 if (grp){1715 const curPk = l._virtual ? l._parentPk : l.pk;1716 // On regroupe par immeuble PARENT (une entrée par immeuble), qu'il soit éclaté (cartes1717 // virtuelles) ou affiché en plan unique. Les cartes virtuelles héritent de groupe_immeuble.1718 const seenPk = new Set(); const uniq = [];1719 (allListings || []).forEach(x => {1720 if (String(x.groupe_immeuble || '').trim() !== grp) return;1721 const pk = x._virtual ? x._parentPk : x.pk;1722 if (pk == null || seenPk.has(pk)) return;1723 const hasUnits = (Array.isArray(x.units) && x.units.length) || (Array.isArray(x._bldUnits) && x._bldUnits.length);1724 if (!hasUnits) return;1725 seenPk.add(pk); uniq.push({ pk, titre: x.titre, id_app: x.id_app, ID_app: x.ID_app });1726 });1727 uniq.sort((a,b) => String(groupImmeubleLabel(a)).localeCompare(String(groupImmeubleLabel(b)), 'fr', { numeric:true }));1728 if (uniq.length > 1){1729 switchHtml = `<div class="d-bld-switch"><span class="d-switch-seg">` + uniq.map(s => {1730 const cur = String(s.pk) === String(curPk);1731 return `<button type="button" class="d-switch-btn${cur?' cur':''}" data-switchpk="${esc(String(s.pk))}"${cur?' disabled':''} title="${esc(s.titre||'')}">🏢 ${esc(groupImmeubleLabel(s))}</button>`;1732 }).join('') + `</span></div>`;1733 }1734 }1735 box.innerHTML = switchHtml + `<div class="d-bld-meta">${units.length} unité${units.length>1?'s':''} · <b style="color:#E67700">${dispo} disponible${dispo>1?'s':''}</b> · ${units.length-dispo} loué${(units.length-dispo)>1?'s':''}</div>${bigBtn}<div class="d-bld">${floorsHtml}</div>`;1736 // Ouvre le panneau d'information de l'unité (double-clic sur les cartes à modèles).1737 const openUnit = (i, u) => {1738 if (window.__isStaff){ if (u) openUnitPanel(l, u); return; } // staff : toujours (même loué)1739 if (u && !isAvail(u)) return; // public : pas de détails si non dispo1740 openUnitPanel(l, u);1741 };1742 // Simple clic sur une carte à modèles : bascule vers le modèle de l'unité cliquée.1743 const switchModel = (i, u) => {1744 if (!virt || !u || !isAvail(u)) return false;1745 const sameModel = l._modelLabel ? (u.photoModel === l._modelLabel) : (i === l._unitIdx);1746 if (sameModel) return false; // déjà le modèle affiché1747 const sib = allListings.find(x => x._virtual && x._parentPk === l._parentPk &&1748 ((u.photoModel && x._modelLabel) ? x._modelLabel === u.photoModel : x._unitIdx === i));1749 if (sib){ openDetail(allListings.indexOf(sib)); return true; }1750 return false;1751 };1752 const wireUnitClicks = root => root.querySelectorAll('.d-unit').forEach(el => {1753 const i = +el.getAttribute('data-uidx'), u = units[i];1754 el.addEventListener('click', () => {1755 // Cartes à modèles : simple clic = bascule de modèle ; les détails s'ouvrent au double-clic.1756 // Cartes simples (sans modèles) : simple clic ouvre directement (comportement inchangé).1757 if (virt){ switchModel(i, u); return; }1758 openUnit(i, u);1759 });1760 el.addEventListener('dblclick', () => openUnit(i, u));1761 });1762 wireUnitClicks(box);1763 box.querySelectorAll('[data-switchpk]').forEach(b => b.addEventListener('click', () => {1764 const pk = b.getAttribute('data-switchpk');1765 let idx = (allListings || []).findIndex(x => String(x.pk) === String(pk) && !x._virtual);1766 if (idx < 0) idx = (allListings || []).findIndex(x => String(x._parentPk) === String(pk)); // immeuble éclaté → 1re carte1767 if (idx >= 0) openDetail(idx);1768 }));1769 const eb = box.querySelector('#d-bld-expand'); if (eb) eb.addEventListener('click', () => openBuildingFull(l, floorsHtml, wireUnitClicks));1770 sec.style.display = '';1771}1772let _bldZoom = 1;1773function _bldEl(){ const b = document.getElementById('bld-full-body'); return b ? b.querySelector('.d-bld') : null; }1774function bldApplyZoom(){ const el = _bldEl(); if (el) el.style.zoom = _bldZoom; }1775function bldZoomBy(f){ _bldZoom = Math.min(1.6, Math.max(0.2, _bldZoom * f)); bldApplyZoom(); }1776window.bldZoomBy = bldZoomBy;1777function bldZoomFit(){1778 const body = document.getElementById('bld-full-body'), el = _bldEl();1779 if (!body || !el) return;1780 el.style.zoom = 1;1781 const cw = body.clientWidth - 28; // largeur dispo (padding 14px x2)1782 const w = el.scrollWidth;1783 if (cw <= 0 || w <= 0) return; // pas encore mesurable (mobile) : le fallback réessaiera1784 _bldZoom = (w > cw) ? Math.max(0.2, cw / w) : 1; // rentre à l'écran, sinon 100 %1785 bldApplyZoom();1786 body.scrollLeft = 0; body.scrollTop = 0;1787}1788window.bldZoomFit = bldZoomFit;1789function openBuildingFull(l, floorsHtml, wireUnitClicks){1790 const ov = document.getElementById('bld-full-overlay'), body = document.getElementById('bld-full-body');1791 if (!ov || !body) return;1792 body.innerHTML = `<div class="d-bld">${floorsHtml}</div>`;1793 wireUnitClicks(body);1794 _bldZoom = 1; bldApplyZoom();1795 ov.classList.add('open'); ovOpened();1796 // Ajuste une fois la mise en page faite. Sur mobile le double rAF peut mesurer trop tôt1797 // (scrollWidth pas prêt) → fallbacks temporisés qui garantissent l'auto-zoom.1798 requestAnimationFrame(() => { requestAnimationFrame(bldZoomFit); });1799 setTimeout(bldZoomFit, 120);1800 setTimeout(bldZoomFit, 350);1801}1802function closeBuildingFull(){ const ov = document.getElementById('bld-full-overlay'); if (ov && ov.classList.contains('open')){ ov.classList.remove('open'); ovClosedUI(); } }1803window.closeBuildingFull = closeBuildingFull;1804// Glisser pour déplacer le plan (souris) — le tactile défile déjà nativement1805(function(){1806 const body = document.getElementById('bld-full-body'); if (!body) return;1807 let down = false, sx = 0, sy = 0, sl = 0, st = 0;1808 body.addEventListener('mousedown', e => { if (e.target.closest('.d-unit')) return; down = true; sx = e.clientX; sy = e.clientY; sl = body.scrollLeft; st = body.scrollTop; body.classList.add('grabbing'); });1809 window.addEventListener('mousemove', e => { if (!down) return; body.scrollLeft = sl - (e.clientX - sx); body.scrollTop = st - (e.clientY - sy); });1810 window.addEventListener('mouseup', () => { down = false; body.classList.remove('grabbing'); });1811 // Molette + Ctrl (souris) et pincer (tactile) pour zoomer1812 body.addEventListener('wheel', e => { if (!e.ctrlKey) return; e.preventDefault(); bldZoomBy(e.deltaY < 0 ? 1.1 : 0.9); }, { passive:false });1813 let pinch = null;1814 const dist = t => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);1815 body.addEventListener('touchstart', e => { if (e.touches.length === 2){ pinch = { d: dist(e.touches), z: _bldZoom }; } }, { passive:true });1816 body.addEventListener('touchmove', e => {1817 if (pinch && e.touches.length === 2){1818 e.preventDefault();1819 const r = dist(e.touches) / pinch.d;1820 _bldZoom = Math.min(1.6, Math.max(0.2, pinch.z * r));1821 bldApplyZoom();1822 }1823 }, { passive:false });1824 body.addEventListener('touchend', e => { if (e.touches.length < 2) pinch = null; });1825})();1826// Libellés des notes internes par unité (vue agent). Mêmes clés que l'admin.1827const UNIT_INT_ORDER = ['adresse_exacte','directive_acces','loc_boite_lettres','loc_cabanon','borne_electrique','locataire_nom','locataire_tel','locataire_courriel','cout_electricite','tolerance_animaux','flexibilite_prix','promotion_possible','psl_accepte','complement_info','commentaire_honnete','problematique_connue','selection_candidat','notes'];1828const UNIT_INT_LABELS = { adresse_exacte:'Adresse exacte', directive_acces:'Directive d\'accès', loc_boite_lettres:'Boîte aux lettres', loc_cabanon:'Cabanon', borne_electrique:'Borne électrique', locataire_nom:'Locataire — nom', locataire_tel:'Locataire — téléphone', locataire_courriel:'Locataire — courriel', cout_electricite:'Coûts électricité (an)', tolerance_animaux:'Tolérance animaux', flexibilite_prix:'Flexibilité du prix', promotion_possible:'Promotion possible', psl_accepte:'PSL accepté', complement_info:'Complément d\'info', commentaire_honnete:'Commentaire honnête', problematique_connue:'Problématique connue', selection_candidat:'Sélection candidat', notes:'Notes' };1829function renderUnitIntNotes(raw){1830 if (raw == null || raw === '') return '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';1831 if (typeof raw === 'string') return `<div class="up-note">${esc(raw).replace(/\n/g,'<br>')}</div>`; // ancien format (chaîne)1832 const rows = UNIT_INT_ORDER.filter(k => raw[k] != null && String(raw[k]).trim())1833 .map(k => `<div class="up-introw"><span class="up-intk">${esc(UNIT_INT_LABELS[k]||k)}</span><span class="up-intv">${esc(String(raw[k])).replace(/\n/g,'<br>')}</span></div>`).join('');1834 return rows || '<div style="color:#999;font-size:13px">Aucune note interne pour cette unité.</div>';1835}1836window.renderUnitIntNotes = renderUnitIntNotes; // utilisée aussi par le bloc module (infos internes)1837async function openUnitPanel(l, u){1838 if (!u) return;1839 const ov = document.getElementById('unit-overlay'), m = document.getElementById('unit-modal');1840 const loue = !isAvail(u);1841 const row = (lb, val) => (val != null && val !== '') ? `<div class="up-row"><span class="up-k">${esc(lb)}</span><span class="up-v">${esc(val)}</span></div>` : '';1842 const prix = (u.prix != null && u.prix !== '') ? Number(u.prix).toLocaleString('fr-CA') + ' $ /mois' : '—';1843 const staff = !!window.__isStaff;1844 const headTitle = staff ? (u.numero || 'Unité') : (u.type || 'Logement');1845 const headSub = staff ? [u.type || '', u.etage || ''].filter(Boolean).join(' · ') : (u.etage || '');1846 // Photos propres à cette unité = le modèle de photo qui lui est lié1847 const model = (u.photoModel && Array.isArray(l.photo_variants)) ? l.photo_variants.find(v => v && v.label === u.photoModel) : null;1848 const uphotos = modelPhotos(model); // photo principale (★) en tête1849 window.__unitPhotos = uphotos;1850 const photosHtml = uphotos.length1851 ? `<div class="up-photos">${uphotos.slice(0,8).map((p,pi)=>`<img src="${esc(simg(p,600,66))}" loading="lazy" alt="Photo ${pi+1}" onclick="openLightbox(window.__unitPhotos,${pi})">`).join('')}</div>`1852 : '';1853 m.innerHTML = `<button class="unit-close" onclick="closeUnitPanel()">×</button>1854 <div class="up-head"><div><div class="up-num">${esc(headTitle)}</div><div class="up-sub">${esc(headSub)}</div></div>1855 <span class="up-stat ${loue?'st-loue':'st-dispo'}">${loue?'Loué':'Disponible'}</span></div>1856 ${photosHtml}1857 <div class="up-price">${prix}</div>1858 ${staff ? `<div class="up-sec" style="color:#E67700">Statut — disponibilité (agent)</div>1859 <select id="up-statut-sel" style="width:100%;font-size:14px;padding:9px 10px;border:1px solid #cfd6de;border-radius:8px;background:#fff;font-weight:600">${U_STATUTS.map(s=>`<option ${s===(u.statut||'Disponible')?'selected':''}>${esc(s)}</option>`).join('')}</select>1860 <div id="up-statut-msg" style="font-size:12px;margin-top:5px;min-height:16px"></div>` : ''}1861 <div class="up-grid">${row('Chambres',u.chambres)}${row('Salles de bain',u.sdb)}${row('Stationnements',u.stationnement)}${row('Superficie',(u.superficie!=null&&u.superficie!=='')?u.superficie+' pi²':'')}</div>1862 ${u.caract?`<div class="up-sec">Description</div><div class="up-caract">${esc(u.caract)}</div>`:''}1863 <div id="up-internal" class="up-internal"></div>`;1864 ov.classList.add('open'); ovOpened();1865 { const sel = document.getElementById('up-statut-sel'); if (sel) sel.addEventListener('change', () => agentSetUnitStatut(l, u, sel.value, sel)); }1866 // La VISIBILITÉ des infos internes est décidée par la RLS elle-même (source de vérité), pas1867 // seulement par le drapeau calculé au chargement : un candidat reçoit une ligne vide (aucun1868 // accès), un agent/admin reçoit la ligne. On affiche donc la note si la RLS a laissé lire1869 // la ligne (data non nul) OU si le drapeau staff est déjà connu.1870 try {1871 const { data } = await _sb.from('listings_internal').select('unit_notes').eq('listing_pk', l._virtual ? l._parentPk : l.pk).maybeSingle();1872 const wrap = document.getElementById('up-internal'); if (!wrap) return;1873 if (data || window.__isStaff){1874 const note = data && data.unit_notes && data.unit_notes[u.id];1875 wrap.innerHTML = `<div class="up-sec" style="color:#8a5a00">🔒 Notes internes (unité)</div>${renderUnitIntNotes(note)}`;1876 } else {1877 wrap.innerHTML = ''; // candidat : aucune info interne1878 }1879 } catch(e){ const wrap = document.getElementById('up-internal'); if (wrap) wrap.innerHTML = ''; }1880 // Diagnostic temporaire (visible si l'URL contient « diag ») — n'affiche que TES propres infos de session1881 try {1882 if (/diag/i.test(location.href)){1883 const { data: { session } } = await _sb.auth.getSession();1884 const { data: st, error: stErr } = await _sb.rpc('is_staff');1885 let lect = '?';1886 try { const { data: d2, error: e2 } = await _sb.from('listings_internal').select('unit_notes').eq('listing_pk', l.pk).maybeSingle();1887 lect = e2 ? ('ERREUR: '+e2.message) : (d2 ? ('ligne lue · note unité = ' + (d2.unit_notes && d2.unit_notes[u.id] ? '«'+(typeof d2.unit_notes[u.id]==='object'?JSON.stringify(d2.unit_notes[u.id]):d2.unit_notes[u.id])+'»' : 'vide')) : 'aucune ligne interne'); }1888 catch(ex){ lect = 'EXCEPTION: ' + ex.message; }1889 const wrap = document.getElementById('up-internal');1890 if (wrap) wrap.insertAdjacentHTML('beforeend', `<div style="margin-top:12px;padding:10px;border:1px dashed #b58900;border-radius:8px;font-size:12px;line-height:1.5;color:#444;background:#fffdf3">🩺 <b>DIAG</b><br>connecté : <b>${session ? 'OUI ('+esc(session.user.email)+')' : 'NON'}</b><br>is_staff (base) : <b>${st===true?'OUI':(st===false?'NON':'?')}</b>${stErr?(' — err: '+esc(stErr.message)):''}<br>window.__isStaff : <b>${String(window.__isStaff)}</b><br>listing_pk : ${esc(l.pk||'∅')}<br>unit_id : ${esc(u.id||'∅')}<br>lecture note : <b>${esc(lect)}</b></div>`);1891 }1892 } catch(e){}1893}1894// Agent : change le statut d'une unité (écrit les units en base + journalise l'historique)1895async function agentSetUnitStatut(l, u, newStatut, selEl){1896 const parentPk = l._virtual ? l._parentPk : l.pk;1897 const units = (Array.isArray(l.units) && l.units.length) ? l.units : (Array.isArray(l._bldUnits) ? l._bldUnits : []);1898 const target = units.find(x => String(x.id) === String(u.id));1899 const msg = document.getElementById('up-statut-msg');1900 if (!target || !parentPk){ if(msg){ msg.textContent = 'Impossible (fiche introuvable).'; msg.style.color='#b91c1c'; } return; }1901 const old = target.statut || 'Disponible';1902 if (old === newStatut) return;1903 if (selEl) selEl.disabled = true;1904 if (msg){ msg.textContent = 'Enregistrement…'; msg.style.color='#888'; }1905 target.statut = newStatut; // optimiste (affichage local)1906 // Passe par une RPC sécurisée : le staff ne peut changer QUE le statut d'une unité (pas toute la fiche).1907 const { error } = await _sb.rpc('agent_set_unit_statut', { p_pk: String(parentPk), p_unit_id: String(u.id||''), p_statut: newStatut });1908 if (error){1909 target.statut = old; if (selEl){ selEl.value = old; selEl.disabled = false; }1910 if (msg){ msg.textContent = 'Échec : ' + error.message + ' — 41-historique-statuts.sql exécuté ?'; msg.style.color='#b91c1c'; }1911 return;1912 }1913 u.statut = newStatut;1914 // Journalisation (best-effort)1915 try {1916 const { data: { session } } = await _sb.auth.getSession();1917 const par = session && session.user ? session.user.email : '';1918 const label = [u.numero || '', u.type || '', u.etage || ''].filter(Boolean).join(' · ');1919 _sb.from('unit_status_history').insert({ listing_pk: parentPk, listing_titre: l.titre || '', unit_id: String(u.id || ''), unite_label: label, ancien_statut: old, nouveau_statut: newStatut, modifie_par: par, source: 'agent' }).then(()=>{}, ()=>{});1920 } catch(e){}1921 if (selEl) selEl.disabled = false;1922 if (msg){ msg.textContent = '✅ Statut mis à jour'; msg.style.color='#E67700'; }1923 const badge = document.querySelector('#unit-modal .up-stat');1924 if (badge){ const loue = !isAvail(u); badge.className = 'up-stat ' + (loue ? 'st-loue' : 'st-dispo'); badge.textContent = loue ? 'Loué' : 'Disponible'; }1925 renderAgentBuilding(l); // rafraîchit les tuiles du plan1926 if (typeof render === 'function') render(); // recalcule les compteurs de la grille1927}1928function closeUnitPanel(e){ if (e && e.target && e.target !== document.getElementById('unit-overlay')) return; document.getElementById('unit-overlay').classList.remove('open'); ovClosedUI(); }1929window.closeUnitPanel = closeUnitPanel;19301931// ---- Bouton « retour » du téléphone : ferme le modal ouvert (au lieu de quitter la page) ----1932const _OV_IDS = ['lightbox-overlay','unit-overlay','bld-full-overlay','visite-overlay','detail-overlay']; // du plus « au-dessus » au plus « en dessous »1933let _ovPushed = 0, _ovIgnorePop = false;1934// Verrouille le défilement du fond dès qu'un overlay est ouvert → évite la 2e barre de défilement.1935function _syncBodyScroll(){ const anyOpen = _OV_IDS.some(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); }); document.body.style.overflow = anyOpen ? 'hidden' : ''; }1936function ovOpened(url){ _ovPushed++; try { history.pushState({ ov: _ovPushed }, '', url || undefined); } catch(e){} _syncBodyScroll(); }1937function ovClosedUI(){ if (_ovPushed > 0){ _ovPushed--; _ovIgnorePop = true; try { history.back(); } catch(e){ _ovIgnorePop = false; } } _syncBodyScroll(); }1938window.addEventListener('popstate', () => {1939 if (_ovIgnorePop){ _ovIgnorePop = false; _syncBodyScroll(); return; } // fermeture déclenchée par l'UI, déjà traitée1940 const id = _OV_IDS.find(x => { const el = document.getElementById(x); return el && el.classList.contains('open'); });1941 if (id){ _ovPushed = Math.max(0, _ovPushed - 1); document.getElementById(id).classList.remove('open'); }1942 _syncBodyScroll();1943});1944// Ferme sans bruit les overlays « enfants » d'une fiche (plan, unité, lightbox) restés ouverts.1945// Empêche une nouvelle fiche de s'ouvrir DERRIÈRE le plan plein écran (z-index plus élevé).1946function _closeChildOverlays(){1947 ['lightbox-overlay','unit-overlay','bld-full-overlay'].forEach(id => {1948 const el = document.getElementById(id);1949 if (el && el.classList.contains('open')){ el.classList.remove('open'); if (_ovPushed > 0) _ovPushed--; }1950 });1951}19521953// ---- Description agrégée : combine les caractéristiques des unités DISPONIBLES ----1954// Champs communs à toutes les dispos → affichés ; champs qui diffèrent → « plusieurs… ».1955// À mesure que des unités sont louées, elles sortent du calcul → la description se précise.1956const CARAC_SEL_DEF = { laveuse:['Non incluse','👕'], rangement:['Non inclus','🗄️'], clim:['Non incluse','🍃'], chauffage:['Non inclus','🔥'], eau:['Non incluse','💧'], meuble:['Non meublé','🛋️'], animaux:['Non acceptés','🐾'] };1957function genAggregateAnnonce(l){1958 const units = Array.isArray(l.units) ? l.units : [];1959 if (!units.length) return l.description || '';1960 const avail = units.filter(u => isAvail(u));1961 const U = avail.length ? avail : units;1962 const C = U.map(u => u.carac || {});1963 if (!C.some(c => c && Object.keys(c).length)) return l.description || '';1964 const num = x => { const v = parseFloat(x); return isNaN(v) ? null : v; };1965 const commonSel = k => { const vs = C.map(c => c[k] || ''); const f = vs[0]; return vs.every(v => v === f) ? f : null; };1966 const allBool = k => C.every(c => c[k]);1967 const lines = [];1968 const anyImmediate = C.some(c => c.dispoImmediate);1969 const dispos = [...new Set(C.map(c => c.dispo).filter(Boolean))];1970 const commonDispo = (!anyImmediate && dispos.length === 1) ? dispos[0] : null;1971 lines.push(anyImmediate ? 'Logement libre dès maintenant !' : (commonDispo ? `Logement libre dès ${commonDispo} !` : (U.length > 1 ? 'Plusieurs disponibilités !' : 'Logement libre dès maintenant !')));1972 const prices = U.map(u => num(u.prix)).filter(x => x != null);1973 if (prices.length){ const mn = Math.min(...prices), mx = Math.max(...prices); lines.push(mn === mx ? `Prix : ${mn} $` : `Prix : de ${mn} à ${mx} $`); }1974 lines.push('');1975 const rues = [...new Set(C.map(c => c.rue).filter(Boolean))];1976 if (rues.length === 1) lines.push(`📍 ${rues[0]} — ${anyImmediate ? 'Disponible dès maintenant' : (commonDispo ? `Disponible ${commonDispo}` : 'Disponible')}`);1977 const etages = [...new Set(U.map(u => u.etage).filter(Boolean))];1978 if (etages.length === 1) lines.push(`🏢 Situé au ${etages[0]}`);1979 else if (etages.length > 1) lines.push(`🏢 Plusieurs étages disponibles (${etages.join(', ')})`);1980 const types = [...new Set(U.map(u => u.type).filter(Boolean))];1981 if (types.length === 1) lines.push(`🏠 Type : ${types[0]}`);1982 else if (types.length > 1) lines.push(`🏠 Grandeurs disponibles : ${types.join(', ')}`);1983 const rng = (get, em, sing, plur) => { const vs = U.map(get).map(num).filter(x => x != null && x > 0); if (!vs.length) return; const mn = Math.min(...vs), mx = Math.max(...vs); lines.push(mn === mx ? `${em} ${mn} ${mn>1?plur:sing}` : `${em} ${mn} à ${mx} ${plur}`); };1984 rng(u => u.chambres, '🛏️', 'Chambre à coucher', 'Chambres à coucher');1985 rng(u => u.sdb, '🛁', 'Salle de bain', 'Salles de bain');1986 const sel = k => { const v = commonSel(k); return (v && v !== CARAC_SEL_DEF[k][0]) ? v : null; };1987 let v; v = sel('laveuse'); if (v) lines.push(`👕 ${v}`);1988 v = sel('rangement'); if (v) lines.push(`🗄️ ${v}`);1989 v = sel('clim'); if (v) lines.push(`🍃 ${v}`);1990 if (allBool('balcon')) lines.push('🔆 Espace balcon');1991 if (allBool('terrain')) lines.push('🌿 Accès au terrain');1992 v = sel('chauffage'); if (v) lines.push(`🔥 Chauffage ${v.toLowerCase()}`);1993 v = sel('eau'); if (v) lines.push('💧 Eau chaude incluse');1994 v = sel('meuble'); if (v) lines.push(`🛋️ ${v}`);1995 rng(u => u.stationnement, '🚗', 'Stationnement', 'Stationnements');1996 if (allBool('deneigement')) lines.push('❄️ Stationnement déneigé');1997 if (allBool('garage')) lines.push('🏠 Garage inclus');1998 if (allBool('plancher_chauffant')) lines.push('♨️ Plancher chauffant');1999 if (allBool('aspirateur_central')) lines.push('🧹 Aspirateur central');2000 if (allBool('electricite_inclus')) lines.push('💡 Électricité incluse');2001 if (allBool('internet_inclus')) lines.push('🌐 Internet inclus');2002 if (allBool('voisin_arriere')) lines.push('🌳 Sans voisin arrière');2003 if (allBool('non_fumeur')) lines.push('🚭 Logement non-fumeur');2004 const anim = commonSel('animaux'); if (anim === 'Non acceptés') lines.push('🚫 Animaux non acceptés'); else if (anim && anim !== CARAC_SEL_DEF.animaux[0]) lines.push(`🐾 ${anim}`);2005 if (allBool('credit')) lines.push('📋 Enquête de crédit et références demandées');2006 const kx = e => (e.emoji || '✨') + '|' + (e.texte || '').trim();2007 const sets = C.map(c => new Set((c.extras || []).filter(e => e && e.texte && e.texte.trim()).map(kx)));2008 if (sets.length){ const inter = [...(sets[0] || [])].filter(k => sets.every(s => s.has(k))); inter.forEach(k => { const p = k.split('|'); lines.push(`${p[0]} ${p.slice(1).join('|')}`); }); }2009 return lines.join('\n');2010}20112012async function openDetail(idx) {2013 _closeChildOverlays(); // ardoise propre : jamais une fiche derrière un plan resté ouvert2014 currentListing = allListings[idx];2015 currentPhotoIdx = 0;2016 const l = currentListing;2017 const photos = await getFolderPhotos(l);2018 const loyer = parseFloat(l.loyer)||0;2019 const etageNorm = normalizeEtage(getEtage(l));2020 const g = document.getElementById('detail-gallery');2021 // Photos « meublé virtuellement » : emoji ✨ sur la galerie + bouton, si présentes.2022 const staged = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2023 window.__stagedPhotos = staged;2024 const sparkHtml = staged.length ? `<button class="staged-spark" onclick="event.stopPropagation();openLightbox(window.__stagedPhotos,0)" title="✨ Voir l'appartement en aménagement virtuel">✨</button>` : '';2025 if (photos.length) {2026 window._currentPhotos = photos;2027 let imgs = photos.map((p,i)=>`<img src="${esc(simg(p,1280,70))}" ${i===0?'':'loading="lazy"'} class="${i===0?'active':''}" alt="Photo ${i+1}" onclick="openLightbox(window._currentPhotos,${i})">`).join('');2028 let dots = photos.length>1?`<div class="gallery-nav">${photos.map((_,i)=>`<button class="gallery-dot ${i===0?'active':''}" onclick="goPhoto(${i})"></button>`).join('')}</div>`:'';2029 let arrows = photos.length>1?`<button class="gallery-prev" onclick="prevPhoto()">‹</button><button class="gallery-next" onclick="nextPhoto()">›</button>`:'';2030 let count = photos.length>1?`<div class="gallery-count">1 / ${photos.length}</div>`:'';2031 g.innerHTML = `<button class="detail-close" onclick="closeDetail()">×</button>${sparkHtml}${imgs}${dots}${arrows}${count}`;2032 } else {2033 g.innerHTML = `<button class="detail-close" onclick="closeDetail()">×</button>${sparkHtml}<div class="gallery-placeholder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" style="width:56px;height:56px;color:#ccc"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z"/><path d="M9 21V12h6v9"/></svg></div>`;2034 }2035 document.getElementById('d-ville').textContent = '📍 '+(l.ville||'');2036 document.getElementById('d-title').textContent = l.titre||'';2037 document.getElementById('d-price').innerHTML = (hasVariants(l)?'<span class="price-from">dès </span>':'')+minLoyer(l).toLocaleString('fr-CA')+' $<span> / mois</span>';2038 document.getElementById('d-badges').innerHTML = `<span class="detail-badge db-size">${esc(grandeurDisplay(l))}</span>${hasVariants(l)?`<span class="detail-badge db-etage">${l.variants.length} modèle${l.variants.length>1?'s':''}${totalUnits(l)?` · ${totalUnits(l)} unité${totalUnits(l)>1?'s':''}`:''}</span>`:(etageNorm?`<span class="detail-badge db-etage">${etageLabel(etageNorm)}</span>`:'')}${(!hasVariants(l) && !(Array.isArray(l.units)&&l.units.length)) ? `${l.chambres?`<span class="detail-badge db-size">🛏️ ${esc(l.chambres)} ch.</span>`:''}${l.sdb?`<span class="detail-badge db-size">🛁 ${esc(l.sdb)} s.d.b.</span>`:''}${l.stationnement?`<span class="detail-badge db-size">🚗 ${esc(l.stationnement)} stat.</span>`:''}` : ''}<span class="detail-badge db-dispo">${esc(dispoDisplay(l))}</span>${l.animaux&&!/aucun/i.test(l.animaux)?`<span class="detail-badge db-animal">${esc(l.animaux)}</span>`:''}${(l.ID_app && !l.created_by_owner)?`<span class="detail-badge db-ref">Réf. ${esc(l.ID_app)}</span>`:''}`;2039 // Bouton « Meublé virtuellement » — visible seulement si des photos meublées existent.2040 const stagedBtn = document.getElementById('d-staged-btn');2041 if (stagedBtn){2042 if (staged.length){ stagedBtn.style.display = ''; stagedBtn.onclick = () => openLightbox(window.__stagedPhotos, 0); }2043 else { stagedBtn.style.display = 'none'; stagedBtn.onclick = null; }2044 }2045 renderDetailVariants(l);2046 renderAgentBuilding(l);2047 renderDetailPhotoModels(l);2048 // Description = texte écrit (proprio/admin) + résumé auto des logements, sans doublon.2049 // genAggregateAnnonce renvoie le résumé pour un immeuble avec caractéristiques,2050 // sinon il renvoie l.description (aucune donnée perdue).2051 (function(){2052 const typed = (l.description || '').trim();2053 const gen = (genAggregateAnnonce(l) || '').trim();2054 let out;2055 if (typed && gen && gen !== typed) {2056 // Évite le doublon quand la description écrite est (à peu de choses près) le même2057 // résumé que celui généré — souvent une version PÉRIMÉE. On compare ligne par ligne2058 // (sans emoji/casse/espaces) ; si ça se recoupe largement, on ne garde que le résumé à jour.2059 const normLine = s => s.replace(/^[^0-9A-Za-zÀ-ÿ]+/, '').replace(/\s+/g, ' ').trim().toLowerCase();2060 const typedSet = new Set(typed.split('\n').map(normLine).filter(Boolean));2061 const genLines = gen.split('\n').map(normLine).filter(Boolean);2062 const overlap = genLines.length ? genLines.filter(x => typedSet.has(x)).length / genLines.length : 0;2063 out = overlap >= 0.6 ? gen : (typed + '\n\n' + gen);2064 }2065 else out = typed || gen || '';2066 document.getElementById('d-desc').textContent = out;2067 })();20682069 // Section infos internes — visible seulement pour le staff connecté (admins + agents, données protégées par RLS)2070 window.__currentPk = l.pk;2071 const iw = document.getElementById('d-internal-wrap');2072 if (iw){2073 if (window.__isStaff){2074 iw.style.display = 'block';2075 const dint = document.getElementById('d-internal'); dint.innerHTML = '';2076 const parentPk = l._virtual ? l._parentPk : l.pk;2077 // Plus de bouton : les infos internes s'affichent directement pour le staff.2078 if (l._virtual && l._unitId){2079 // Carte d'appartement : infos 🏢 immeuble + 🔒 notes de CETTE unité2080 window.showApartmentInternal && window.showApartmentInternal(parentPk, l._unitId, l.pk);2081 } else {2082 // Immeuble (plan) → seulement le bloc 🏢 (les infos par logement sont par unité).2083 // Fiche appartement unique → toutes les infos internes de la fiche.2084 const isBuilding = l.fiche_type === 'immeuble' || (Array.isArray(l.units) && l.units.length > 0);2085 if (window.showInternalInfo) window.showInternalInfo(parentPk, { buildingOnly: isBuilding });2086 }2087 } else iw.style.display = 'none';2088 }20892090 // Contact : fiche PROPRIÉTAIRE → ses coordonnées + pas de formulaire de location.2091 // fiche Gestion ISR → coordonnées Gestion ISR + formulaire de location.2092 const btnCall = document.getElementById('btn-call'), btnEmail = document.getElementById('btn-email'), btnVis = document.getElementById('btn-visite-full');2093 // Mode lien propriétaire (?p=) : ses coordonnées (profil) sur TOUTES les fiches,2094 // y compris celles de Gestion ISR. Un contact propre à la fiche a priorité.2095 const oc = window.__ownerContact;2096 const ph = ((l.contact_phone||'').trim()) || (oc ? (oc.contact_phone||'').trim() : '');2097 const em = ((l.contact_email||'').trim()) || (oc ? (oc.contact_email||'').trim() : '');2098 if (oc || l.created_by_owner){2099 if (ph){ btnCall.href = 'tel:'+ph.replace(/[^\d+]/g,''); btnCall.style.display=''; } else { btnCall.style.display='none'; }2100 if (em){ btnEmail.href = 'mailto:'+em; btnEmail.style.display=''; } else { btnEmail.style.display='none'; }2101 btnVis.style.display = 'none'; // pas de formulaire de location2102 } else {2103 btnCall.href = 'tel:+18194721127'; btnCall.style.display='';2104 btnEmail.href = 'mailto:info@gestion-isr.com'; btnEmail.style.display='';2105 btnVis.style.display = ''; btnVis.onclick = ()=>openVisite(l);2106 }2107 // Mini-carte dans le modal2108 const detailMapEl = document.getElementById('detail-map');2109 detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Chargement de la carte…</div>';2110 if (window._detailMap) {2111 window._detailMap.remove();2112 window._detailMap = null;2113 }2114 const adresse = l.adresse_geo || l.id || '';2115 if (adresse) {2116 setTimeout(async () => {2117 const gpsMatch = adresse.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);2118 let coords;2119 if (gpsMatch) {2120 coords = {lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2])};2121 } else {2122 coords = await geocode(adresse);2123 }2124 if (coords && typeof L !== 'undefined') {2125 window._detailMap = L.map('detail-map', {zoomControl:true, scrollWheelZoom:false}).setView([coords.lat, coords.lng], 15);2126 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {2127 attribution: '© OpenStreetMap',2128 maxZoom: 192129 }).addTo(window._detailMap);2130 L.marker([coords.lat, coords.lng]).addTo(window._detailMap);2131 } else {2132 detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Localisation non disponible</div>';2133 }2134 }, 300);2135 } else {2136 detailMapEl.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:13px">Adresse non spécifiée</div>';2137 }2138 document.getElementById('btn-email').href = 'mailto:info@gestion-isr.com?subject=' + encodeURIComponent('Demande d\'information — ' + (l.titre||''));2139 document.getElementById('detail-overlay').classList.add('open'); ovOpened(l.ID_app ? '#' + l.ID_app : null);2140 if (window.logListingView) window.logListingView(l.ID_app || l.id_app || '', l.titre || '');2141}21422143let lightboxPhotos = [], lightboxIdx = 0;21442145// ---- Modèles de photos (variantes) — vue publique ----2146// Photos d'un modèle ordonnées avec la photo principale (★) en tête.2147function modelPhotos(v){ const ph = (v && Array.isArray(v.photos)) ? v.photos.filter(Boolean) : []; return (v && v.main && ph.includes(v.main)) ? [v.main, ...ph.filter(p => p !== v.main)] : ph.slice(); }2148function pvClean(l){ return Array.isArray(l && l.photo_variants) ? l.photo_variants.filter(v => v && Array.isArray(v.photos) && v.photos.length).map(v => ({ ...v, photos: modelPhotos(v) })) : []; }2149// Précharge la 1re photo de chaque modèle (taille visionneuse) → passer d'un modèle2150// à l'autre devient instantané, plus de délai au clic.2151const _pvPreloaded = new Set();2152function _preloadModelCovers(pv){2153 (pv || []).forEach(v => {2154 if (!v || !v.photos || !v.photos.length) return;2155 const url = simg(v.photos[0], 1600, 78);2156 if (_pvPreloaded.has(url)) return;2157 _pvPreloaded.add(url);2158 const im = new Image(); im.src = url;2159 });2160}2161// Ouvre la visionneuse avec des boutons pour passer d'un modèle à l'autre2162function openModelsLightbox(models, mi){2163 const pv = (models || []).filter(v => v && Array.isArray(v.photos) && v.photos.length);2164 if (!pv.length) return;2165 mi = Math.max(0, Math.min(mi || 0, pv.length - 1));2166 window.__lbModels = pv;2167 const bar = document.getElementById('lightbox-models');2168 if (bar){2169 bar.innerHTML = pv.map((v, i) => `<button class="lb-mbtn${i===mi?' active':''}" onclick="event.stopPropagation();switchLbModel(${i})">${esc(v.label || ('Modèle ' + String.fromCharCode(65 + i)))}</button>`).join('');2170 bar.classList.toggle('show', pv.length > 1); // barre visible dès qu'il y a plus d'un modèle2171 }2172 openLightbox(pv[mi].photos, 0, true);2173 _preloadModelCovers(pv); // les autres modèles seront prêts instantanément2174}2175window.switchLbModel = (i) => {2176 const pv = window.__lbModels || []; if (!pv[i]) return;2177 const bar = document.getElementById('lightbox-models');2178 if (bar) bar.querySelectorAll('.lb-mbtn').forEach((b, bi) => b.classList.toggle('active', bi === i));2179 lightboxPhotos = pv[i].photos; lightboxIdx = 0;2180 document.getElementById('lightbox-img').src = simg(lightboxPhotos[0], 1600, 78);2181 document.getElementById('lb-prev').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2182 document.getElementById('lb-next').style.display = lightboxPhotos.length > 1 ? 'flex' : 'none';2183 document.getElementById('lightbox-count').textContent = lightboxPhotos.length > 1 ? ('1 / ' + lightboxPhotos.length) : '';2184 _lbPreload();2185};2186window.openVariantPhotos = (idx) => { const pv = pvClean(allListings[idx]); if (pv.length) openModelsLightbox(pv, 0); };2187window.openModelPhotos = (i) => { const pv = window.__detailPv || []; if (pv.length) openModelsLightbox(pv, i); };2188function renderDetailPhotoModels(l){2189 const sec = document.getElementById('d-photo-models-sec'), box = document.getElementById('d-photo-models');2190 if (!sec || !box) return;2191 const head = sec.querySelector('.d-models-head');2192 // Fiche d'un appartement (virtuelle) : la navigation se fait via le plan (renderAgentBuilding).2193 if (l._virtual){ sec.style.display = 'none'; return; }2194 if (head) head.innerHTML = `📷 Modèles d'appartement <span>— cliquez pour voir les photos de chaque modèle 👇</span>`;2195 const pv = pvClean(l); window.__detailPv = pv;2196 if (!pv.length){ sec.style.display = 'none'; return; }2197 box.innerHTML = pv.map((v, i) => `<button class="pv-model-btn" onclick="openModelPhotos(${i})">Modèle ${esc(v.label || String.fromCharCode(65 + i))} <span>${v.photos.length} photo${v.photos.length>1?'s':''}</span></button>`).join('');2198 sec.style.display = '';2199 _preloadModelCovers(pv); // fiche ouverte : les modèles s'afficheront sans délai au clic2200}2201function _lbPreload(){2202 if (!lightboxPhotos || lightboxPhotos.length < 2) return;2203 [1, -1].forEach(d => { const j = (lightboxIdx + d + lightboxPhotos.length) % lightboxPhotos.length; const im = new Image(); im.src = simg(lightboxPhotos[j], 1600, 78); });2204}2205// Ouvre les photos « meublé virtuellement » d'un logement (depuis une carte de la liste).2206window.openStaged = function(idx){2207 const l = allListings[idx]; if (!l) return;2208 const s = Array.isArray(l.staged_photos) ? l.staged_photos.filter(Boolean) : [];2209 if (s.length){ window.__stagedPhotos = s; openLightbox(s, 0); }2210};2211function openLightbox(photos, idx, keepModels) {2212 if (!keepModels){ const bar = document.getElementById('lightbox-models'); if (bar){ bar.classList.remove('show'); bar.innerHTML = ''; } window.__lbModels = null; }2213 lightboxPhotos = photos;2214 lightboxIdx = idx;2215 document.getElementById('lightbox-img').src = simg(photos[idx], 1600, 78);2216 document.getElementById('lb-prev').style.display = photos.length > 1 ? 'flex' : 'none';2217 document.getElementById('lb-next').style.display = photos.length > 1 ? 'flex' : 'none';2218 document.getElementById('lightbox-count').textContent = photos.length > 1 ? (idx+1)+' / '+photos.length : '';2219 document.getElementById('lightbox-overlay').classList.add('open'); ovOpened();2220 _lbPreload();2221}22222223function closeLightbox() {2224 document.getElementById('lightbox-overlay').classList.remove('open');2225 ovClosedUI();2226}22272228function closeLightboxOutside(e) {2229 if (e.target === document.getElementById('lightbox-overlay')) closeLightbox();2230}22312232function lightboxNav(dir) {2233 lightboxIdx = (lightboxIdx + dir + lightboxPhotos.length) % lightboxPhotos.length;2234 document.getElementById('lightbox-img').src = simg(lightboxPhotos[lightboxIdx], 1600, 78);2235 document.getElementById('lightbox-count').textContent = (lightboxIdx+1)+' / '+lightboxPhotos.length;2236 _lbPreload();2237}22382239// Navigation clavier2240document.addEventListener('keydown', e => {2241 const lb = document.getElementById('lightbox-overlay');2242 if (lb.classList.contains('open')) {2243 if (e.key === 'ArrowRight') lightboxNav(1);2244 if (e.key === 'ArrowLeft') lightboxNav(-1);2245 if (e.key === 'Escape') closeLightbox();2246 }2247});22482249async function goPhoto(i){2250 const photos = await getFolderPhotos(currentListing);2251 currentPhotoIdx=i;2252 document.querySelectorAll('#detail-gallery img').forEach((img,idx)=>img.classList.toggle('active',idx===i));2253 document.querySelectorAll('.gallery-dot').forEach((d,idx)=>d.classList.toggle('active',idx===i));2254 const cnt=document.querySelector('.gallery-count');2255 if(cnt) cnt.textContent=(i+1)+' / '+photos.length;2256}2257async function nextPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx+1)%p.length);}2258async function prevPhoto(){const p=await getFolderPhotos(currentListing);goPhoto((currentPhotoIdx-1+p.length)%p.length);}2259function partagerFiche() {2260 const url = window.location.href;2261 navigator.clipboard.writeText(url).then(() => {2262 const btn = document.getElementById('btn-share');2263 btn.innerHTML = '✅ Lien copié !';2264 btn.style.background = '#FFECCF';2265 setTimeout(() => {2266 btn.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg> Partager cette fiche';2267 btn.style.background = 'transparent';2268 }, 2500);2269 });2270}22712272function closeDetail(){2273 _closeChildOverlays(); // ferme aussi le plan/unité restés ouverts par-dessus la fiche2274 document.getElementById('detail-overlay').classList.remove('open');2275 ovClosedUI();2276}2277function closeDetailOutside(e){if(e.target===document.getElementById('detail-overlay'))closeDetail();}22782279// ---- Outil de partage (agent) : mêmes filtres que la vue admin « Sélections » ----2280let agentSelChecked = new Set();2281function agentSelCriteria(){2282 const chips = id => [...document.querySelectorAll('#'+id+' input:checked')].map(i=>i.value);2283 const b = (document.getElementById('asel-budget')||{}).value;2284 return { types: chips('asel-type'), budget: (b==null||b==='')?null:Number(b), etages: chips('asel-etage'), pets: chips('asel-animaux') };2285}2286// Filtre « dur » identique à l'admin (selPass) : un logement est exclu si un critère coché n'est pas rempli.2287function agentSelPass(l, c){2288 if (c.types.length){ const gs = hasVariants(l) ? distinct(l.variants.map(v=>v.grandeur||l.grandeur)) : [l.grandeur]; if(!gs.some(g=>c.types.includes(g))) return false; }2289 if (c.budget!=null && !isNaN(c.budget)){ const m=minLoyer(l); if(m==null || m>c.budget) return false; }2290 if (c.etages.length){ const es=listingEtagesNorm(l); if(!es.some(e=>c.etages.includes(e))) return false; }2291 if (c.pets.length){ const a=(l.animaux||'').toLowerCase(); if(a.includes('aucun')) return false; if(!c.pets.some(p=>petAllowed(a,p))) return false; }2292 return true;2293}2294function agentSelVisible(){2295 const c = agentSelCriteria(); const seen=new Set();2296 const q = ((document.getElementById('asel-search')||{}).value||'').toLowerCase().trim();2297 return (allListings||[]).filter(l=>{2298 const idapp=String(l.id_app||l.ID_app||''); if(!idapp||seen.has(idapp)) return false; seen.add(idapp);2299 if((l.etiquette||'').toLowerCase()==='loué') return false; // partageable = disponible2300 if(!agentSelPass(l, c)) return false;2301 if(q){ const hay=((l.titre||'')+' '+(l.ville||'')+' '+grandeurDisplay(l)+' '+idapp).toLowerCase(); if(!hay.includes(q)) return false; }2302 return true;2303 });2304}2305let _aselSearchT=null;2306window.agentSelSearchInput = function(){ if(_aselSearchT) clearTimeout(_aselSearchT); _aselSearchT=setTimeout(()=>{ _aselSearchT=null; agentSelRender(); }, 180); };2307window.openAgentSel = function(){ const ov=document.getElementById('agent-sel-overlay'); if(!ov)return; ov.style.display='flex'; document.body.style.overflow='hidden'; agentSelRender(); };2308window.closeAgentSel = function(){ const ov=document.getElementById('agent-sel-overlay'); if(!ov)return; ov.style.display='none'; if(typeof _syncBodyScroll==='function') _syncBodyScroll(); else document.body.style.overflow=''; };2309window.agentSelRender = function(){2310 const list=document.getElementById('agent-sel-list'); if(!list)return;2311 const vis = agentSelVisible();2312 const cnt=document.getElementById('agent-sel-count');2313 if(cnt) cnt.innerHTML=`${vis.length} logement${vis.length>1?'s':''} affiché${vis.length>1?'s':''} · <b style="color:#E67700">${agentSelChecked.size} sélectionné${agentSelChecked.size!==1?'s':''}</b> — coche pour partager`;2314 list.innerHTML = vis.length ? vis.map(l=>{2315 const idapp=String(l.id_app||l.ID_app||''); const on=agentSelChecked.has(idapp); const mn=minLoyer(l);2316 const photo = l.main_photo || (Array.isArray(l.photos)&&l.photos.length?l.photos[0]:'');2317 const thumb = photo ? `<img class="selthumb" loading="lazy" decoding="async" src="${esc(photo)}" alt="">` : '<div class="selthumb"></div>';2318 const meta=[grandeurDisplay(l), l.ville, (mn?((hasVariants(l)||hasUnits(l))?'dès ':'')+Number(mn).toLocaleString('fr-CA')+' $':'')].filter(Boolean).join(' · ');2319 return `<label class="sel-row ${on?'':'off'}"><input type="checkbox" data-agentsel="${esc(idapp)}" ${on?'checked':''} onchange="window.agentSelToggle&&window.agentSelToggle(this)">${thumb}<div class="selinfo"><b>${esc(l.titre||idapp)}</b><div class="m">${esc(meta)}</div></div></label>`;2320 }).join('') : '<div style="color:#888;text-align:center;padding:2rem">Aucun logement ne correspond aux filtres.</div>';2321 agentSelUpdateShare();2322};2323window.agentSelToggle = function(cb){2324 const id=cb.getAttribute('data-agentsel');2325 if(cb.checked) agentSelChecked.add(id); else agentSelChecked.delete(id);2326 const row=cb.closest('.sel-row'); if(row) row.classList.toggle('off', !cb.checked);2327 const cnt=document.getElementById('agent-sel-count'); if(cnt) cnt.innerHTML=cnt.innerHTML.replace(/<b[^>]*>[\s\S]*?<\/b>/,`<b style="color:#E67700">${agentSelChecked.size} sélectionné${agentSelChecked.size!==1?'s':''}</b>`);2328 agentSelUpdateShare();2329};2330window.agentSelAll = function(){ agentSelVisible().forEach(l=>{ const id=String(l.id_app||l.ID_app||''); if(id) agentSelChecked.add(id); }); agentSelRender(); };2331window.agentSelNone = function(){ agentSelChecked.clear(); agentSelRender(); };2332window.agentSelResetFilters = function(){ ['asel-type','asel-etage','asel-animaux'].forEach(id=>{ const el=document.getElementById(id); if(el) el.querySelectorAll('input:checked').forEach(i=>i.checked=false); }); const b=document.getElementById('asel-budget'); if(b) b.value=''; const s=document.getElementById('asel-search'); if(s) s.value=''; agentSelRender(); };2333window.agentSelUpdateShare = function(){2334 const box=document.getElementById('agent-sel-share'); if(!box)return;2335 const ids=[...agentSelChecked];2336 if(!ids.length){ box.innerHTML='<div style="color:#888;font-size:13px">Coche un ou plusieurs appartements pour générer un lien personnalisé.</div>'; return; }2337 const link=location.origin+'/?apps='+ids.map(encodeURIComponent).join(',');2338 box.innerHTML=`<div class="sel-linkbox"><label style="font-size:12px;font-weight:700;color:#E67700;display:block;margin-bottom:6px">🔗 Lien personnalisé — ${ids.length} logement${ids.length>1?'s':''}</label>2339 <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">2340 <input class="lnk" id="agent-sel-link" readonly value="${esc(link)}" style="flex:1;min-width:220px">2341 <button class="sel-btn primary" onclick="window.agentSelCopy&&window.agentSelCopy(this)">Copier</button>2342 <button class="sel-btn" onclick="var i=document.getElementById('agent-sel-link');if(i)window.open(i.value,'_blank')">Aperçu</button>2343 </div>2344 <div style="font-size:11.5px;color:#888;margin-top:6px">Envoie ce lien à un candidat : il n'y verra que ces logements.</div></div>`;2345};2346window.agentSelCopy = function(btn){2347 const inp=document.getElementById('agent-sel-link'); if(!inp)return; inp.select();2348 const fb=()=>{ if(btn){ const t=btn.textContent; btn.textContent='Copié ✓'; setTimeout(()=>{ btn.textContent=t; },1500); } };2349 if(navigator.clipboard){ navigator.clipboard.writeText(inp.value).then(fb).catch(()=>{ try{document.execCommand('copy');}catch(e){} fb(); }); }2350 else { try{document.execCommand('copy');}catch(e){} fb(); }2351};23522353let visiteListing = null;2354function openVisite(l){2355 visiteListing = (l && typeof l === 'object') ? l : null;2356 const titre = visiteListing ? (visiteListing.titre || '') : (typeof l === 'string' ? l : '');2357 document.getElementById('modal-apt-name').textContent = titre ? ('Logement : ' + titre) : '';2358 const f = document.getElementById('visite-form'); if (f){ f.reset(); f.style.display = ''; }2359 const sc = document.getElementById('visite-success'); if (sc) sc.style.display = 'none';2360 const sb = document.getElementById('vf-submit'); if (sb){ sb.disabled = false; sb.textContent = 'Envoyer ma demande'; }2361 const m = document.getElementById('visite-msg'); if (m){ m.textContent = ''; m.className = 'vf-msg'; }2362 document.getElementById('visite-overlay').classList.add('open'); ovOpened();2363}2364function openVisiteFromCard(titre){ openVisite(titre); }2365function openVisiteFromCardIdx(idx){ openVisite(allListings[idx] || ''); }2366document.getElementById('visite-form').addEventListener('submit', async e => {2367 e.preventDefault();2368 const g = id => (document.getElementById(id).value || '').trim();2369 const btn = document.getElementById('vf-submit'), msg = document.getElementById('visite-msg');2370 btn.disabled = true; btn.textContent = 'Envoi…';2371 const lead = {2372 nom: g('vf-nom'), courriel: g('vf-courriel'), telephone: g('vf-tel'),2373 logement: visiteListing ? (visiteListing.titre || null) : null,2374 logement_ref: visiteListing ? (visiteListing.ID_app || visiteListing.id_app || null) : null,2375 date_emmenagement: g('vf-date') || null, type_appartement: g('vf-type') || null,2376 budget: g('vf-budget') ? parseInt(g('vf-budget')) : null,2377 animaux: g('vf-animaux') || null,2378 enquete_credit: g('vf-credit') || null, message: g('vf-message') || null2379 };2380 try {2381 const { error } = await _sb.from('leads').insert(lead);2382 if (error) throw error;2383 document.getElementById('visite-form').reset();2384 // Écran de succès plein : impossible à manquer, peu importe le défilement.2385 document.getElementById('visite-form').style.display = 'none';2386 document.getElementById('visite-success').style.display = 'block';2387 } catch(err){2388 msg.className = 'vf-msg err'; msg.textContent = 'Erreur : ' + (err.message || 'envoi impossible');2389 try { msg.scrollIntoView({ block:'center', behavior:'smooth' }); } catch(_){}2390 btn.disabled = false; btn.textContent = 'Envoyer ma demande';2391 }2392});2393function closeVisite(){document.getElementById('visite-overlay').classList.remove('open');ovClosedUI();}2394function closeVisiteOutside(e){if(e.target===document.getElementById('visite-overlay'))closeVisite();}23952396loadListings().then(() => {2397 let hash = window.location.hash.replace('#', '');2398 // Décodage : un id_app éclaté contient « ½ » → dans l'URL il devient %C2%BD.2399 // Sans décoder, la comparaison échouerait et la fiche ne s'ouvrirait pas.2400 try { hash = decodeURIComponent(hash); } catch(e) {}2401 if (hash) {2402 const idx = allListings.findIndex(l => normIdApp(l.ID_app || l.id_app) === normIdApp(hash));2403 if (idx !== -1) openDetail(idx);2404 }2405});24062407// ===== CARTE =====2408let map = null, markers = [], geocodeCache = {};2409let mapVisible = false;24102411function toggleMap() {2412 mapVisible = !mapVisible;2413 const section = document.getElementById('map-section');2414 const btn = document.getElementById('map-toggle-btn');2415 section.style.display = mapVisible ? 'block' : 'none';2416 btn.classList.toggle('active', mapVisible);2417 btn.innerHTML = mapVisible2418 ? `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg> Masquer la carte`2419 : `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg> Voir la carte`;2420 if (mapVisible) {2421 if (!map) initMap();2422 else updateMapMarkers();2423 }2424}24252426function initMap() {2427 if (typeof L === 'undefined') {2428 console.error('Leaflet non chargé');2429 return;2430 }2431 map = L.map('map').setView([45.8797, -72.4939], 11);2432 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {2433 attribution: '© OpenStreetMap contributors',2434 maxZoom: 192435 }).addTo(map);2436 updateMapMarkers();24372438 // Légende2439 const legend = L.control({position: 'bottomright'});2440 legend.onAdd = function() {2441 const div = L.DomUtil.create('div');2442 div.style.cssText = 'background:#fff;padding:10px 12px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.15);font-family:system-ui,sans-serif;font-size:12px;line-height:1.8';2443 div.innerHTML = `2444 <div style="font-weight:600;margin-bottom:4px;color:#444">Grandeur</div>2445 <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#3B82F6;margin-right:6px"></span>3½</div>2446 <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#FB8C00;margin-right:6px"></span>4½</div>2447 <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#F97316;margin-right:6px"></span>5½</div>2448 <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#8B5CF6;margin-right:6px"></span>6½</div>2449 <div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#EF4444;margin-right:6px"></span>Maison</div>2450 `;2451 return div;2452 };2453 legend.addTo(map);2454}24552456async function geocode(address) {2457 if (geocodeCache[address]) return geocodeCache[address];2458 try {2459 const q = encodeURIComponent(address + ', Québec, Canada');2460 const url = `https://nominatim.openstreetmap.org/search?format=json&q=${q}&limit=1`;2461 const resp = await fetch(url, {headers:{'Accept-Language':'fr'}});2462 const data = await resp.json();2463 if (data.length > 0) {2464 const coords = {lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon)};2465 geocodeCache[address] = coords;2466 return coords;2467 }2468 } catch(e) {}2469 return null;2470}24712472async function updateMapMarkers() {2473 if (!map) return;2474 markers.forEach(m => map.removeLayer(m));2475 markers = [];24762477 const grandeurColors = {2478 '3½': '#3B82F6',2479 '4½': '#FB8C00',2480 '5½': '#F97316',2481 '6½': '#8B5CF6',2482 'Maison': '#EF4444'2483 };24842485 // Géocoder uniquement les logements filtrés2486 const promises = filtered.map(async (l, idx) => {2487 const address = l.adresse_geo || l.id || '';2488 if (!address) return;2489 // Détecter si c'est des coordonnées GPS (ex: "45.8797, -72.4939")2490 const gpsMatch = address.match(/^\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*$/);2491 let coords;2492 if (gpsMatch) {2493 coords = {lat: parseFloat(gpsMatch[1]), lng: parseFloat(gpsMatch[2])};2494 } else {2495 coords = await geocode(address);2496 }2497 if (!coords) return;2498 const loyer = parseFloat(l.loyer)||0;2499 const color = grandeurColors[l.grandeur] || '#FB8C00';2500 const markerIcon = L.divIcon({2501 className: '',2502 html: `<div style="background:${color};color:#fff;border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)">${esc(l.grandeur||'?')}</div>`,2503 iconSize: [32, 32],2504 iconAnchor: [16, 16],2505 popupAnchor: [0, -18]2506 });2507 const marker = L.marker([coords.lat, coords.lng], {icon: markerIcon})2508 .bindPopup(`2509 <div class="popup-title">${esc(l.titre)}</div>2510 <div class="popup-price">${hasVariants(l)?'dès ':''}${minLoyer(l).toLocaleString('fr-CA')} $ / mois</div>2511 <div class="popup-dispo">${esc(dispoDisplay(l))}</div>2512 <button class="popup-btn" onclick="document.getElementById('map-section').style.display='none';document.getElementById('map-toggle-btn').classList.remove('active');mapVisible=false;openDetail(${allListings.indexOf(l)})">Voir la fiche</button>2513 `);2514 marker.addTo(map);2515 markers.push(marker);2516 });25172518 await Promise.all(promises);25192520 // Ajuster la vue sur les marqueurs2521 if (markers.length > 0) {2522 const group = L.featureGroup(markers);2523 map.fitBounds(group.getBounds().pad(0.1));2524 }2525}25262527// Mettre à jour la carte quand les filtres changent2528const origRenderTags = renderTags;2529renderTags = function() {2530 origRenderTags();2531 if (mapVisible && map) updateMapMarkers();2532};2533</script>25342535<!-- Journalisation des vues d'appartement (visiteur connecté) -->2536<script type="module">2537import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm';2538const _sb = createClient('https://jfgkriojazaohgjezijj.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpmZ2tyaW9qYXphb2hnamV6aWpqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODEzNDE5NjQsImV4cCI6MjA5NjkxNzk2NH0.VVLfhvpkQ9WgJOhCYW3y6kRZIJbAQXTCANv6mXce1Yw');2539// Exposé en global : le 1er <script> (classique) référence `_sb` (formulaire de location,2540// note interne par unité). Sans ça, `_sb` reste confiné au module → « _sb is not defined ».2541window._sb = _sb;2542let _lastView = null;2543window.logListingView = async (id_app, titre) => {2544 try {2545 if (id_app && id_app === _lastView) return;2546 _lastView = id_app;2547 const { data: { session } } = await _sb.auth.getSession();2548 if (!session) return;2549 await _sb.from('events').insert({ user_id: session.user.id, type: 'listing_view', detail: { id_app: id_app || null, titre: titre || null } });2550 } catch(e) {}2551};25522553// ---- Favoris ----2554window.__favs = new Set();2555async function _loadFavs(){2556 try {2557 const { data: { session } } = await _sb.auth.getSession();2558 if (!session) return;2559 const { data } = await _sb.from('favorites').select('id_app').eq('user_id', session.user.id);2560 window.__favs = new Set((data || []).map(f => f.id_app));2561 if (window.refreshHearts) window.refreshHearts();2562 } catch(e) {}2563}2564window.toggleFavorite = async (id_app, btn) => {2565 if (!id_app) return;2566 const { data: { session } } = await _sb.auth.getSession();2567 if (!session){ if (confirm('Connecte-toi pour enregistrer tes favoris. Aller à la page profil ?')) location.href = '/compte.html'; return; }2568 try {2569 if (window.__favs.has(id_app)){2570 await _sb.from('favorites').delete().eq('user_id', session.user.id).eq('id_app', id_app);2571 window.__favs.delete(id_app);2572 } else {2573 await _sb.from('favorites').insert({ user_id: session.user.id, id_app });2574 window.__favs.add(id_app);2575 }2576 if (btn) btn.classList.toggle('active', window.__favs.has(id_app));2577 } catch(e) {}2578};2579_loadFavs();25802581// ---- Infos internes (admins + agents de location — RLS protège les données) ----2582window.__isStaff = false;2583(async () => {2584 try {2585 const { data: { session } } = await _sb.auth.getSession();2586 if (!session) return;2587 const { data } = await _sb.rpc('is_staff');2588 window.__isStaff = data === true;2589 if (window.__isStaff){ const sb = document.getElementById('staff-badge'); if (sb) sb.style.display = 'inline-flex'; const ast = document.getElementById('agent-share-top'); if (ast) ast.style.display = 'inline-flex'; if (typeof renderAgentNewRail === 'function') renderAgentNewRail(); }2590 // Si une fiche est déjà ouverte au moment où l'on apprend qu'on est staff, on révèle le bouton2591 const iw = document.getElementById('d-internal-wrap');2592 if (window.__isStaff && iw && document.getElementById('detail-overlay').classList.contains('open') && window.__currentPk){2593 iw.style.display = 'block';2594 const b = document.getElementById('d-internal-btn'); if (b){ b.style.display=''; b.onclick = () => window.showInternalInfo(window.__currentPk); }2595 if (typeof currentListing !== 'undefined' && currentListing) renderAgentBuilding(currentListing); // révèle le visuel immeuble2596 }2597 } catch(e) {}2598})();2599const INT_VIEW = [2600 ['🏢 Immeuble', [['date_construction','Date de construction'],['directive_acces_immeuble',"Directive d'accès à l'immeuble"],['qualite_locataires',"Qualité des locataires dans l'immeuble"],['notes','Notes internes générales']]],2601 ['🏠 Logement — Accès & emplacements', [['adresse_exacte','Adresse exacte'],['directive_acces',"Directive d'accès"],['loc_boite_lettres','Boîte aux lettres'],['loc_cabanon','Cabanon'],['borne_electrique','Borne électrique — conditions']]],2602 ['Locataire en place', [['locataire_nom','Nom'],['locataire_tel','Téléphone'],['locataire_courriel','Courriel']]],2603 ['Coûts & négociation', [['cout_electricite',"Coûts d'électricité (annuel, estimé)"],['tolerance_animaux','Tolérance animaux (réelle)'],['flexibilite_prix','Flexibilité du prix'],['promotion_possible','Promotion possible'],['psl_accepte','PSL accepté']]],2604 ['Appréciation & sélection', [['complement_info',"Complément d'information"],['commentaire_honnete','Commentaire honnête'],['problematique_connue','Problématique connue'],['selection_candidat','Sélection candidat']]]2605];2606const _intE2 = s => String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');2607const _intLbl = t => `<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#999;margin-bottom:2px">${t}</div>`;2608const _intSec = t => `<div style="font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;color:#8a5a00;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #f0d98b">${t}</div>`;2609const _intBox = inner => `<div style="background:#fffdf3;border:1px solid #f0d98b;border-radius:12px;padding:14px;font-size:14px;line-height:1.5;color:#333">${inner}</div>`;2610// Rend les groupes de champs internes (INT_VIEW) présents dans la ligne listings_internal.2611function _intGroupsHtml(d, groups){2612 let html = '', any = false;2613 (groups || []).forEach(([title, fields]) => {2614 const rows = fields.filter(([k]) => d[k]).map(([k, label]) => `<div style="margin-bottom:10px">${_intLbl(_intE2(label))}${_intE2(d[k])}</div>`).join('');2615 if (rows){ any = true; html += _intSec(_intE2(title)) + rows; }2616 });2617 return { html, any };2618}2619// Immeuble (plan) ou fiche appartement unique. buildingOnly=true → seulement le bloc 🏢 Immeuble.2620window.showInternalInfo = async (pk, opts) => {2621 opts = opts || {};2622 const box = document.getElementById('d-internal'); if (!box) return;2623 const btn = document.getElementById('d-internal-btn'); if (btn) btn.style.display = 'none';2624 box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';2625 try {2626 const { data, error } = await _sb.from('listings_internal').select('*').eq('listing_pk', pk).maybeSingle();2627 if (error) throw error;2628 const d = data || {};2629 const r = _intGroupsHtml(d, opts.buildingOnly ? [INT_VIEW[0]] : INT_VIEW);2630 let html = r.html, any = r.any;2631 if (!opts.buildingOnly){2632 const vn = d.variant_notes || {};2633 const vnRows = Object.keys(vn).filter(k => vn[k]).map(k => { const p = k.split('||'); return `<div style="margin-bottom:8px">${_intLbl((p[0]?('Variante '+_intE2(p[0])):'') + (p[1]?' · '+_intE2(p[1]):''))}${_intE2(vn[k])}</div>`; }).join('');2634 if (vnRows){ any = true; html += _intSec('Notes par étage') + vnRows; }2635 }2636 box.innerHTML = any ? _intBox(html) : _intBox(`<div style="color:#999;font-size:13px">${opts.buildingOnly ? 'Aucune info interne au niveau immeuble. Les infos par logement : clique un appartement dans le plan.' : 'Aucune information interne saisie.'}</div>`);2637 } catch(err) { box.innerHTML = '<div style="color:#b91c1c;font-size:13px">Impossible de charger les infos internes.</div>'; }2638};2639// Carte d'un appartement (virtuelle) : infos 🏢 IMMEUBLE + 🔒 notes de CETTE unité.2640window.showApartmentInternal = async (pk, unitId, viewPk) => {2641 const box = document.getElementById('d-internal'); if (!box) return;2642 box.innerHTML = '<div style="color:#888;font-size:13px">Chargement…</div>';2643 try {2644 const { data } = await _sb.from('listings_internal').select('*').eq('listing_pk', pk).maybeSingle();2645 if (!document.getElementById('detail-overlay').classList.contains('open') || (viewPk != null && window.__currentPk !== viewPk)) return;2646 const d = data || {};2647 const bldg = _intGroupsHtml(d, [INT_VIEW[0]]);2648 const note = d.unit_notes && d.unit_notes[unitId];2649 const unitHtml = window.renderUnitIntNotes ? window.renderUnitIntNotes(note) : '';2650 box.innerHTML = (bldg.any ? _intBox(bldg.html) : '') + `<div class="up-sec" style="color:#8a5a00">🔒 Notes internes (cet appartement)</div>${unitHtml}`;2651 } catch(e){ box.innerHTML = ''; }2652};26532654// ============ Widget de clavardage (visiteur <-> équipe) ============2655(function(){2656 const $c = id => document.getElementById(id);2657 const bubble = $c('chatw-bubble'), panel = $c('chatw-panel'), body = $c('chatw-body');2658 if (!bubble) return;2659 let cwOpen = false, cwUid = null, cwSub = null, cwUnread = 0, cwLastTs = null, cwPollTimer = null, cwNote = '';2660 const cwShown = new Set();2661 const cwEsc = s => String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');2662 const cwFmt = t => new Date(t).toLocaleString('fr-CA',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});2663 function cwBadge(n){ cwUnread = n; const b = $c('chatw-badge'); if (!b) return; if (n > 0){ b.textContent = n; b.style.display = 'flex'; } else b.style.display = 'none'; }2664 bubble.addEventListener('click', () => cwOpen ? cwClose() : cwOpenPanel());2665 $c('chatw-close').addEventListener('click', cwClose);2666 function cwOpenPanel(){ cwOpen = true; panel.style.display = 'flex'; cwRoute(); }2667 function cwClose(){ cwOpen = false; panel.style.display = 'none'; }2668 async function cwRoute(){ const { data:{ session } } = await _sb.auth.getSession(); if (session){ cwUid = session.user.id; cwShowChat(); } else cwShowStart(); }2669 function cwShowStart(){2670 body.innerHTML = `<div class="chatw-pad"><p class="chatw-intro">Bonjour 👋 Laisse ton prénom et ton courriel pour discuter avec notre équipe. On t'enverra aussi un lien pour activer ton compte (mot de passe + profil).</p>2671 <div class="chatw-field"><label>Prénom</label><input id="cw-prenom" autocomplete="given-name"></div>2672 <div class="chatw-field"><label>Courriel</label><input id="cw-courriel" type="email" autocomplete="email"></div>2673 <div class="chatw-err" id="cw-err"></div>2674 <button class="chatw-btn primary" id="cw-start">Accéder au chat</button></div>`;2675 $c('cw-start').onclick = cwStart;2676 }2677 function cwErr(m){ const e = $c('cw-err'); if (e){ e.textContent = m; e.style.display = 'block'; } }2678 async function cwStart(){2679 const prenom = ($c('cw-prenom').value||'').trim(), courriel = ($c('cw-courriel').value||'').trim();2680 if (!prenom || !courriel){ cwErr("Merci d'indiquer ton prénom et ton courriel."); return; }2681 if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(courriel)){ cwErr('Adresse courriel invalide.'); return; }2682 $c('cw-start').disabled = true;2683 const { data, error } = await _sb.auth.signInAnonymously();2684 if (error){ $c('cw-start').disabled = false; cwErr('Erreur : ' + error.message); return; }2685 cwUid = data.user.id;2686 await _sb.from('profiles').upsert({ user_id: cwUid, nom: prenom, courriel, source: 'chat' });2687 try {2688 const { error: ue } = await _sb.auth.updateUser({ email: courriel, data: { full_name: prenom } }, { emailRedirectTo: location.origin + '/compte.html?activation=1' });2689 cwNote = ue ? '' : "📧 Un courriel d'activation t'a été envoyé — clique le lien pour choisir ton mot de passe et finaliser ton compte.";2690 } catch(_){ cwNote = ''; }2691 cwShowChat();2692 }2693 async function cwShowChat(){2694 try { const { data:{ session } } = await _sb.auth.getSession(); if (session && _sb.realtime && _sb.realtime.setAuth) await _sb.realtime.setAuth(session.access_token); } catch(_){}2695 body.innerHTML = `<div id="chatw-msgs"></div><form id="chatw-form"><input id="chatw-input" placeholder="Écris ton message…" autocomplete="off"><button type="submit" aria-label="Envoyer">➤</button></form>`;2696 cwShown.clear(); cwLastTs = null;2697 const { data } = await _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');2698 const box = $c('chatw-msgs');2699 if (!data || !data.length){ box.innerHTML = '<div id="cw-empty" style="color:#999;font-size:13px;text-align:center;margin:auto">Écris-nous, on te répond rapidement 👇</div>'; }2700 else data.forEach(cwAppend);2701 if (cwNote){ const bx = $c('chatw-msgs'); if (bx) bx.insertAdjacentHTML('afterbegin', '<div style="background:#FFF7EF;border:1px solid #F3E0CC;color:#8a5a2b;font-size:12.5px;border-radius:10px;padding:8px 10px;margin-bottom:6px">' + cwNote + '</div>'); }2702 cwScroll();2703 _sb.rpc('mark_my_messages_read');2704 cwBadge(0); cwEnsurePoll();2705 $c('chatw-form').addEventListener('submit', cwSend);2706 if (!cwSub){2707 cwSub = _sb.channel('cw-' + cwUid).on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: 'user_id=eq.' + cwUid }, p => {2708 if (p.new.sender === 'admin'){2709 if (cwOpen && $c('chatw-msgs')){ cwAppend(p.new); cwScroll(); _sb.rpc('mark_my_messages_read'); }2710 else cwBadge(cwUnread + 1);2711 }2712 }).subscribe();2713 }2714 }2715 function cwAppend(m){ const box = $c('chatw-msgs'); if (!box) return; if (cwShown.has(m.id)) return; cwShown.add(m.id); if (!cwLastTs || m.created_at > cwLastTs) cwLastTs = m.created_at; const e = $c('cw-empty'); if (e) e.remove(); const d = document.createElement('div'); d.className = 'cw-msg ' + (m.sender === 'visitor' ? 'visitor' : 'admin'); d.innerHTML = cwEsc(m.body) + `<span class="cw-tm">${cwFmt(m.created_at)}</span>`; box.appendChild(d); }2716 function cwEnsurePoll(){ if (!cwPollTimer) cwPollTimer = setInterval(cwPoll, 4000); }2717 async function cwPoll(){2718 if (!cwUid) return;2719 if (!cwOpen || !$c('chatw-msgs')){2720 const { count } = await _sb.from('messages').select('id', { count: 'exact', head: true }).eq('user_id', cwUid).eq('sender', 'admin').eq('read', false);2721 cwBadge(count || 0); return;2722 }2723 let q = _sb.from('messages').select('*').eq('user_id', cwUid).order('created_at');2724 if (cwLastTs) q = q.gt('created_at', cwLastTs);2725 const { data } = await q;2726 if (data && data.length){ data.forEach(cwAppend); cwScroll(); _sb.rpc('mark_my_messages_read'); }2727 }2728 function cwScroll(){ const box = $c('chatw-msgs'); if (box) box.scrollTop = box.scrollHeight; }2729 async function cwSend(e){ e.preventDefault(); const inp = $c('chatw-input'); const b = (inp.value||'').trim(); if (!b || !cwUid) return; inp.value = ''; const { data, error } = await _sb.from('messages').insert({ user_id: cwUid, sender: 'visitor', body: b }).select().single(); if (!error && data){ cwAppend(data); cwScroll(); } }2730 (async () => { const { data:{ session } } = await _sb.auth.getSession(); if (session){ cwUid = session.user.id; cwEnsurePoll(); const { count } = await _sb.from('messages').select('id', { count: 'exact', head: true }).eq('user_id', cwUid).eq('sender', 'admin').eq('read', false); if (count) cwBadge(count); } })();2731})();2732</script>27332734<!-- Widget de clavardage (Gestion ISR) -->2735<style>2736#chatw{position:fixed;right:18px;bottom:18px;z-index:3000;font-family:system-ui,-apple-system,sans-serif}2737#chatw-bubble{width:58px;height:58px;border-radius:50%;border:none;background:#FB8C00;color:#fff;font-size:24px;cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;position:relative}2738#chatw-bubble:hover{background:#E67700}2739#chatw-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;background:#EF4444;color:#fff;border-radius:10px;font-size:12px;font-weight:700;display:none;align-items:center;justify-content:center;padding:0 5px;border:2px solid #fff}2740#chatw-panel{position:absolute;right:0;bottom:70px;width:340px;max-width:calc(100vw - 32px);height:480px;max-height:calc(100vh - 120px);background:#fff;border:1px solid #e8e8e5;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;overflow:hidden}2741.chatw-head{background:#FB8C00;color:#fff;padding:14px 16px;display:flex;justify-content:space-between;align-items:center}2742.chatw-head b{font-size:15px}2743.chatw-sub{font-size:12px;opacity:.85}2744.chatw-head button{background:rgba(255,255,255,.2);border:none;color:#fff;width:28px;height:28px;border-radius:8px;cursor:pointer;font-size:15px}2745#chatw-body{flex:1;display:flex;flex-direction:column;min-height:0}2746.chatw-pad{padding:18px 16px;overflow-y:auto}2747.chatw-intro{font-size:14px;color:#555;line-height:1.5;margin:0 0 16px}2748.chatw-btn{display:block;width:100%;padding:12px;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;border:none;margin-bottom:10px}2749.chatw-btn.primary{background:#FB8C00;color:#fff}2750.chatw-btn.primary:hover{background:#E67700}2751.chatw-btn.ghost{background:#fff;border:1px solid #ddd;color:#444}2752.chatw-btn.ghost:hover{border-color:#FB8C00;color:#E67700}2753.chatw-field{margin-bottom:10px}2754.chatw-field label{display:block;font-size:12px;font-weight:600;color:#555;margin-bottom:4px}2755.chatw-field input{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid #d8d8d4;border-radius:9px;font-size:14px;outline:none}2756.chatw-field input:focus{border-color:#FB8C00}2757.chatw-back{background:none;border:none;color:#888;font-size:13px;cursor:pointer;padding:0;margin-bottom:12px}2758.chatw-err{font-size:13px;color:#B91C1C;margin:4px 0 8px;display:none}2759#chatw-msgs{flex:1;overflow-y:auto;padding:14px;background:#faf9f7;display:flex;flex-direction:column;gap:8px}2760.cw-msg{max-width:80%;padding:9px 12px;border-radius:14px;font-size:14px;line-height:1.4;white-space:pre-wrap;word-break:break-word}2761.cw-msg.visitor{align-self:flex-end;background:#FB8C00;color:#fff;border-bottom-right-radius:4px}2762.cw-msg.admin{align-self:flex-start;background:#fff;border:1px solid #e8e8e5;color:#1a1a1a;border-bottom-left-radius:4px}2763.cw-tm{display:block;font-size:10px;opacity:.7;margin-top:2px}2764#chatw-form{display:flex;gap:8px;padding:10px;border-top:1px solid #f0f0ee;background:#fff}2765#chatw-form input{flex:1;padding:10px 12px;border:1px solid #ddd;border-radius:9px;font-size:14px;outline:none}2766#chatw-form button{background:#FB8C00;color:#fff;border:none;border-radius:9px;padding:0 16px;font-weight:700;cursor:pointer;font-size:16px}2767@media(max-width:480px){#chatw-panel{width:calc(100vw - 24px)}}2768</style>2769<div id="chatw">2770 <button id="chatw-bubble" title="Clavardage" aria-label="Ouvrir le clavardage">💬<span id="chatw-badge"></span></button>2771 <div id="chatw-panel" style="display:none">2772 <div class="chatw-head"><div><b>Clavardage</b><div class="chatw-sub">Gestion ISR · réponse rapide</div></div><button id="chatw-close" aria-label="Fermer">✕</button></div>2773 <div id="chatw-body"></div>2774 </div>2775</div>2776</body>2777</html>2778