Gestion-des-Marches-RLA/index-old-statique-page.html

1360 lines
167 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Marchés RLA - Zone Sud | Tunisie Telecom</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.1/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
:root {
--primary: #0b2a55; --primary-light: #1e40af; --accent: #00d4ff;
--success: #10b981; --warning: #f59e0b; --danger: #ef4444;
--bg-dark: #0f172a; --bg-card: rgba(255,255,255,0.05);
--text: #f1f5f9; --text-muted: #94a3b8;
--bg-body: linear-gradient(135deg, #0f172a 0%, #1a1a2e 50%, #16213e 100%);
--table-header: rgba(0,0,0,0.3); --border-color: rgba(255,255,255,0.1);
}
[data-theme="light"] {
--primary: #0b2a55; --primary-light: #1e40af; --accent: #0284c7;
--bg-dark: #f8fafc; --bg-card: rgba(255,255,255,0.9);
--text: #1e293b; --text-muted: #64748b;
--bg-body: linear-gradient(135deg, #e2e8f0 0%, #f1f5f9 50%, #ffffff 100%);
--table-header: rgba(11,42,85,0.1); --border-color: rgba(0,0,0,0.1);
}
[data-theme="professional"] {
--primary: #1f2937; --primary-light: #374151; --accent: #2563eb;
--bg-dark: #ffffff; --bg-card: #f9fafb;
--text: #111827; --text-muted: #6b7280;
--bg-body: #f3f4f6; --table-header: #e5e7eb; --border-color: #d1d5db;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg-body); min-height: 100vh; color: var(--text); overflow-x: hidden; transition: all 0.3s ease; }
.login-container { position: fixed; inset: 0; background: var(--bg-body); display: flex; justify-content: center; align-items: center; z-index: 10000; }
.login-box { background: var(--bg-card); padding: 40px; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); border: 1px solid var(--border-color); width: 100%; max-width: 400px; text-align: center; }
.login-box .login-logo { height: 70px; margin-bottom: 20px; border-radius: 0; box-shadow: none; background: none; object-fit: contain; }
.login-box h2 { color: var(--primary); margin-bottom: 10px; }
.login-box p { color: var(--text-muted); margin-bottom: 25px; }
.login-box input { width: 100%; padding: 14px 18px; margin-bottom: 15px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 1em; background: var(--bg-dark); color: var(--text); transition: all 0.3s; }
.login-box input:focus { border-color: var(--accent); outline: none; }
.login-box button { width: 100%; padding: 14px; background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white; border: none; border-radius: 10px; font-size: 1.1em; cursor: pointer; transition: all 0.3s; }
.login-box button:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(0,0,0,0.3); }
.login-error { color: var(--danger); margin-bottom: 15px; font-size: 0.9em; display: none; }
.header { background: linear-gradient(90deg, var(--primary), var(--primary-light)); padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 20px rgba(0,0,0,0.3); position: sticky; top: 0; z-index: 100; flex-wrap: wrap; gap: 15px; }
.logo-section { display: flex; align-items: center; gap: 15px; }
.logo-section img { height: 50px; border-radius: 0; box-shadow: none; background: none; object-fit: contain; }
.logo-section h1 { font-size: 1.3em; font-weight: 600; color: white; line-height: 1.2; }
.header-controls { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; }
.theme-selector { display: flex; gap: 5px; background: rgba(255,255,255,0.1); padding: 5px; border-radius: 25px; }
.theme-btn { padding: 8px 12px; border: none; border-radius: 20px; cursor: pointer; font-size: 0.85em; background: transparent; color: white; transition: all 0.3s; }
.theme-btn.active { background: rgba(255,255,255,0.2); }
.theme-btn:hover { background: rgba(255,255,255,0.15); }
.user-info { display: flex; align-items: center; gap: 10px; color: white; font-size: 0.9em; }
.user-badge { background: rgba(255,255,255,0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8em; }
.user-badge.super-admin { background: linear-gradient(135deg, #f59e0b, #d97706); }
.admin-btn { padding: 8px 15px; background: rgba(37,99,235,0.85); border: none; border-radius: 20px; color: white; cursor: pointer; font-size: 0.85em; transition: all 0.3s; display: none; }
.admin-btn:hover { background: #1d4ed8; }
.logout-btn { padding: 8px 15px; background: rgba(239,68,68,0.85); border: none; border-radius: 20px; color: white; cursor: pointer; font-size: 0.85em; transition: all 0.3s; }
.logout-btn:hover { background: #dc2626; }
.header-info { text-align: right; font-size: 0.85em; color: rgba(255,255,255,0.8); }
.header-info .date { color: var(--accent); font-weight: 700; }
.slide-nav { display: flex; justify-content: center; gap: 8px; padding: 15px 20px; background: rgba(0,0,0,0.2); flex-wrap: wrap; }
[data-theme="light"] .slide-nav, [data-theme="professional"] .slide-nav { background: rgba(0,0,0,0.05); }
.slide-nav button { padding: 10px 18px; border: none; border-radius: 25px; background: var(--bg-card); color: var(--text); cursor: pointer; transition: all 0.3s ease; font-size: 0.85em; display: flex; align-items: center; gap: 8px; border: 1px solid var(--border-color); user-select: none; }
.slide-nav button:hover { background: var(--primary-light); color: white; transform: translateY(-2px); }
.slide-nav button.active { background: linear-gradient(135deg, var(--accent), var(--primary-light)); color: white; box-shadow: 0 4px 15px rgba(0,212,255,0.3); }
.nav-separator { width: 2px; height: 30px; background: var(--border-color); margin: 0 5px; }
.export-btn { background: linear-gradient(135deg, #dc2626, #b91c1c) !important; color: white !important; }
.export-pptx-btn { background: linear-gradient(135deg, #C65D21, #E07832) !important; color: white !important; }
.refresh-btn { background: linear-gradient(135deg, #0891b2, #0e7490) !important; color: white !important; }
.slides-container { position: relative; max-width: 1400px; margin: 0 auto; padding: 25px; min-height: 75vh; }
.slide { display: none; animation: slideIn 0.4s ease-out; }
.slide.active { display: block; }
@keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 25px; }
.kpi-card { background: var(--bg-card); border-radius: 16px; padding: 22px; border: 1px solid var(--border-color); transition: all 0.3s ease; position: relative; overflow: hidden; }
.kpi-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--accent); }
.kpi-card.capex::before { background: var(--success); }
.kpi-card.opex::before { background: var(--warning); }
.kpi-card.alertes::before { background: var(--danger); }
.kpi-card.clotures::before { background: #6b7280; }
.kpi-card.proactif-normal::before { background: #059669; }
.kpi-card.proactif-sous::before { background: #DC2626; }
.kpi-card.proactif-depasse::before { background: #D97706; }
.kpi-card.proactif-none::before { background: #64748B; }
.kpi-card:hover { transform: translateY(-3px); box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
.kpi-card .icon { font-size: 2.2em; margin-bottom: 12px; opacity: 0.85; }
.kpi-card .value { font-size: 2em; font-weight: 800; color: var(--accent); }
.kpi-card .label { font-size: 0.9em; color: var(--text-muted); margin-top: 5px; }
.kpi-card .sub { font-size: 0.8em; color: var(--text-muted); margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-color); }
.section-title { font-size: 1.4em; margin-bottom: 20px; padding-left: 15px; border-left: 4px solid var(--accent); display: flex; align-items: center; gap: 12px; }
.filters-bar { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.filter-group { display: flex; align-items: center; gap: 8px; }
.filter-group label { font-size: 0.85em; color: var(--text-muted); font-weight: 600; }
.filter-group select { padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-card); color: var(--text); font-size: 0.85em; min-width: 160px; cursor: pointer; }
.filter-group select:focus { outline: none; border-color: var(--accent); }
.filters-panels { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; }
.filter-panel { position: relative; background: var(--bg-card); border-radius: 14px; border: 2px solid #10b981; padding: 14px 16px 22px 16px; box-shadow: 0 4px 18px rgba(0,0,0,0.18); }
.filter-panel-header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 1px solid var(--border-color); padding-bottom: 6px; margin-bottom: 8px; gap: 10px; }
.filter-panel-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 0.95em; color: var(--text); }
.filter-panel-title i { color: #10b981; }
.filter-panel-sub { font-size: 0.75em; color: var(--text-muted); text-align: right; }
.filter-panel-body { margin-top: 4px; }
.filter-panel-reset { position: absolute; right: 10px; bottom: 6px; border: none; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 0.75em; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; transition: all 0.2s ease; }
.filter-panel-reset i { font-size: 0.8em; }
.filter-panel-reset:hover { background: rgba(255,255,255,0.06); color: #10b981; }
.filter-group-chips { align-items: flex-start; }
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; width: 100%; }
.filter-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text); font-size: 0.8em; cursor: pointer; transition: all 0.2s ease; user-select: none; }
.filter-chip:hover { background: rgba(255,255,255,0.06); transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.2); }
.filter-chip-icon { width: 16px; height: 16px; border-radius: 50%; border: 2px solid #10b981; display: flex; align-items: center; justify-content: center; font-size: 9px; color: #10b981; background: transparent; transition: all 0.2s; }
.filter-chip-label { white-space: nowrap; }
.filter-chip.selected { background: linear-gradient(135deg, #047857, #10b981); border-color: transparent; color: #ffffff; box-shadow: 0 3px 10px rgba(0,0,0,0.35); }
.filter-chip.selected .filter-chip-icon { background: #ffffff; color: #047857; border-color: #ffffff; }
.filter-entrepreneur-bar { display: flex; align-items: center; gap: 12px; background: var(--bg-card); border: 2px solid #10b981; border-radius: 14px; padding: 14px 16px; box-shadow: 0 4px 18px rgba(0,0,0,0.18); flex-wrap: nowrap; }
.filter-entrepreneur-bar .filter-bar-label { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 0.95em; color: var(--text); margin-right: auto; white-space: nowrap; }
.filter-entrepreneur-bar .filter-bar-label i { color: #10b981; }
.filter-entrepreneur-bar select { padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-dark); color: var(--text); font-size: 0.85em; min-width: 200px; cursor: pointer; transition: border-color 0.3s; }
.filter-entrepreneur-bar select:focus { outline: none; border-color: #10b981; }
.filter-entrepreneur-reset { border: none; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 0.85em; display: inline-flex; align-items: center; gap: 4px; padding: 6px 10px; border-radius: 999px; transition: all 0.2s ease; }
.filter-entrepreneur-reset:hover { background: rgba(255,255,255,0.06); color: #10b981; }
.filter-panel.proactif-theme { border-color: #6366F1; }
.filter-panel.proactif-theme .filter-panel-title i { color: #6366F1; }
.filter-panel.proactif-theme .filter-panel-reset:hover { color: #6366F1; }
.filter-chip.proactif-theme .filter-chip-icon { border-color: #6366F1; color: #6366F1; }
.filter-chip.proactif-theme:hover { box-shadow: 0 2px 8px rgba(99,102,241,0.25); }
.filter-chip.proactif-theme.selected { background: linear-gradient(135deg, #4F46E5, #6366F1); border-color: transparent; color: #fff; box-shadow: 0 3px 12px rgba(99,102,241,0.4); }
.filter-chip.proactif-theme.selected .filter-chip-icon { background: #fff; color: #4F46E5; border-color: #fff; }
.filter-entrepreneur-bar.proactif-theme { border-color: #6366F1; }
.filter-entrepreneur-bar.proactif-theme .filter-bar-label i { color: #6366F1; }
.filter-entrepreneur-bar.proactif-theme select:focus { border-color: #6366F1; }
.filter-entrepreneur-bar.proactif-theme .filter-entrepreneur-reset:hover { color: #6366F1; }
.table-container { background: var(--bg-card); border-radius: 14px; overflow: hidden; border: 1px solid var(--border-color); margin-bottom: 20px; }
.table-header { background: linear-gradient(90deg, var(--primary), var(--primary-light)); padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; color: white; }
.table-header h3 { font-size: 1em; display: flex; align-items: center; gap: 10px; }
.table-header .badge { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 20px; font-size: 0.85em; white-space: nowrap; }
.table-wrapper { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 700px; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border-color); vertical-align: top; }
th { background: var(--table-header); font-weight: 800; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); }
tr:hover { background: rgba(0,212,255,0.05); }
tr.infructueux-row { background: rgba(239,68,68,0.05); }
tr.infructueux-row:hover { background: rgba(239,68,68,0.1); }
tr.multi-region-row { background: rgba(37,99,235,0.05); }
tr.multi-region-row:hover { background: rgba(37,99,235,0.1); }
tr.modernisation-row { background: rgba(168,85,247,0.05); }
tr.modernisation-row:hover { background: rgba(168,85,247,0.1); }
.region-bandeau { background: linear-gradient(90deg, rgba(99,102,241,0.12), transparent); border-left: 4px solid #6366F1; }
.region-bandeau td { font-weight: 700; font-size: 0.95em; color: var(--text); padding: 10px 15px; letter-spacing: 0.3px; }
.progress-bar { display: flex; align-items: center; gap: 10px; }
.progress-track { flex: 1; height: 8px; background: var(--border-color); border-radius: 4px; overflow: hidden; min-width: 70px; }
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.8s ease-out; }
.progress-fill.green { background: linear-gradient(90deg, #10b981, #34d399); }
.progress-fill.orange { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.progress-fill.red { background: linear-gradient(90deg, #ef4444, #f87171); }
.progress-value { font-weight: 800; min-width: 42px; text-align: right; font-size: 0.9em; }
.status-badge { padding: 4px 10px; border-radius: 15px; font-size: 0.75em; font-weight: 800; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.status-badge.critique { background: rgba(239,68,68,0.2); color: #ef4444; }
.status-badge.attention { background: rgba(245,158,11,0.2); color: #f59e0b; }
.status-badge.ok { background: rgba(16,185,129,0.2); color: #10b981; }
.status-badge.cloture { background: rgba(107,114,128,0.2); color: #6b7280; }
.status-badge.infructueux { background: rgba(239,68,68,0.15); color: #dc2626; }
.status-badge.multi-region { background: rgba(37,99,235,0.15); color: #2563eb; }
.status-badge.modernisation { background: rgba(168,85,247,0.15); color: #a855f7; }
.status-badge.verdict-normal { background: rgba(5,150,105,0.15); color: #059669; }
.status-badge.verdict-sous { background: rgba(220,38,38,0.15); color: #DC2626; }
.status-badge.verdict-depasse { background: rgba(217,119,6,0.15); color: #D97706; }
.status-badge.verdict-none { background: rgba(100,116,139,0.15); color: #64748B; }
.status-badge.risque-faible { background: rgba(16,185,129,0.15); color: #10b981; }
.status-badge.risque-modere { background: rgba(245,158,11,0.15); color: #f59e0b; }
.status-badge.risque-eleve { background: rgba(239,68,68,0.15); color: #ef4444; }
.region-tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.7em; margin-left: 6px; background: rgba(0,212,255,0.15); color: var(--accent); white-space: nowrap; }
.csc-tag { display: block; font-size: 0.8em; color: var(--text-muted); margin-top: 3px; font-weight: normal; }
.periode-tag { font-size: 0.8em; color: var(--text-muted); white-space: nowrap; }
.regions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.region-card { background: var(--bg-card); border-radius: 14px; padding: 18px; border: 2px solid #0066CC; transition: all 0.3s ease; }
.region-card:hover { transform: scale(1.01); box-shadow: 0 6px 25px rgba(0,102,204,0.25); border-color: #0088FF; }
.region-card.zone-sud-card { border: 2px dashed var(--accent); background: rgba(0,212,255,0.05); }
.region-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; padding-bottom: 12px; border-bottom: 1px solid var(--border-color); }
.region-dot { width: 12px; height: 12px; border-radius: 50%; }
.region-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.region-stat { background: var(--table-header); padding: 10px; border-radius: 8px; text-align: center; }
.region-stat .value { font-size: 1.3em; font-weight: 900; color: var(--accent); }
.region-stat .label { font-size: 0.7em; color: var(--text-muted); margin-top: 2px; }
.details-btn { margin-top: 12px; width: 100%; padding: 10px; background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 0.85em; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.3s; }
.details-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.2); }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; justify-content: center; align-items: center; z-index: 9999; padding: 20px; }
.modal-overlay.active { display: flex; }
.modal-content { background: var(--bg-card); border-radius: 16px; width: 100%; max-width: 980px; max-height: 85vh; overflow: hidden; border: 1px solid var(--border-color); }
.modal-header { background: linear-gradient(90deg, var(--primary), var(--primary-light)); padding: 18px 25px; display: flex; justify-content: space-between; align-items: center; color: white; }
.modal-header h3 { font-size: 1.1em; display: flex; align-items: center; gap: 10px; }
.modal-close { background: rgba(255,255,255,0.2); border: none; color: white; width: 35px; height: 35px; border-radius: 50%; cursor: pointer; font-size: 1.2em; transition: all 0.3s; }
.modal-close:hover { background: rgba(255,255,255,0.3); }
.modal-body { padding: 20px; max-height: calc(85vh - 70px); overflow-y: auto; }
.map-container { background: var(--bg-card); border-radius: 14px; padding: 20px; border: 1px solid var(--border-color); margin-bottom: 20px; }
.map-svg-wrapper { position: relative; width: 100%; max-width: 800px; margin: 0 auto; }
.map-svg-wrapper svg { width: 100%; height: auto; }
.region-path { cursor: pointer; transition: all 0.3s ease; stroke: #fff; stroke-width: 2; }
.region-path:hover { filter: brightness(1.2); stroke-width: 3; }
.region-tooltip { position: absolute; background: var(--bg-dark); color: var(--text); padding: 15px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); border: 1px solid var(--border-color); pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 1000; min-width: 380px; max-width: 480px; max-height: 450px; overflow-y: auto; }
.region-tooltip.visible { opacity: 1; }
.region-tooltip h4 { margin-bottom: 12px; color: var(--accent); display: flex; align-items: center; gap: 8px; font-size: 1.1em; padding-bottom: 8px; border-bottom: 1px solid var(--border-color); }
.region-tooltip .projet-group { margin-bottom: 12px; }
.region-tooltip .projet-title { font-weight: 700; color: var(--accent); font-size: 0.9em; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
.region-tooltip .projet-title .count { background: rgba(0,212,255,0.2); padding: 2px 8px; border-radius: 10px; font-size: 0.85em; }
.region-tooltip .marche-line { font-size: 0.82em; padding: 4px 0 4px 12px; border-left: 2px solid var(--border-color); margin-left: 4px; color: var(--text-muted); }
.region-tooltip .marche-line .ref { color: var(--text); font-weight: 500; }
.region-tooltip .marche-line .entrepreneur { color: var(--text-muted); }
.region-tooltip .marche-line .pct { font-weight: 700; margin-left: 4px; }
.region-tooltip .marche-line .pct.green { color: #10b981; }
.region-tooltip .marche-line .pct.orange { color: #f59e0b; }
.region-tooltip .marche-line .pct.red { color: #ef4444; }
.region-tooltip .no-marche { color: var(--text-muted); font-style: italic; padding: 10px 0; }
.map-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-top: 20px; }
.legend-item { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 10px; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.3s; }
.legend-item:hover { border-color: var(--accent); transform: translateY(-2px); }
.legend-left { display: flex; align-items: center; gap: 10px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
.legend-title { font-weight: 700; }
.legend-sub { font-size: 0.8em; color: var(--text-muted); }
.legend-right .v { font-weight: 800; color: var(--accent); }
.bullet-charts-container { margin-top: 25px; }
.bullet-region-group { background: var(--bg-card); border-radius: 12px; padding: 18px; margin-bottom: 15px; border: 1px solid var(--border-color); }
.bullet-region-title { font-size: 1.1em; font-weight: 700; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; color: var(--text); padding-bottom: 10px; border-bottom: 2px solid var(--border-color); }
.bullet-region-title .region-dot { width: 14px; height: 14px; border-radius: 50%; }
.bullet-project-group { margin-left: 15px; margin-bottom: 15px; }
.bullet-project-title { font-size: 0.9em; font-weight: 600; color: var(--accent); margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
.bullet-item { display: flex; align-items: center; gap: 12px; padding: 8px 12px; margin-bottom: 6px; background: var(--table-header); border-radius: 8px; font-size: 0.85em; }
.bullet-item .ref-container { min-width: 220px; }
.bullet-item .ref-main { font-weight: 600; color: var(--text); }
.bullet-item .ref-csc { font-size: 0.8em; color: var(--text-muted); margin-top: 2px; }
.bullet-item .entrepreneur { min-width: 130px; color: var(--text-muted); font-size: 0.85em; }
.bullet-chart { flex: 1; height: 22px; background: var(--border-color); border-radius: 4px; position: relative; min-width: 150px; overflow: visible; }
.bullet-chart .objectif-bar { position: absolute; height: 100%; background: rgba(100,100,100,0.25); border-radius: 4px; }
.bullet-chart .avancement-bar { position: absolute; height: 14px; top: 4px; left: 0; border-radius: 3px; transition: width 0.5s ease; }
.bullet-chart .avancement-bar.green { background: linear-gradient(90deg, #10b981, #34d399); }
.bullet-chart .avancement-bar.orange { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.bullet-chart .avancement-bar.red { background: linear-gradient(90deg, #ef4444, #f87171); }
.bullet-chart .objectif-marker { position: absolute; width: 3px; height: 26px; top: -2px; background: #333; border-radius: 2px; }
.bullet-item .pct-value { min-width: 50px; text-align: right; font-weight: 700; font-size: 0.9em; }
.bullet-item .pct-value.green { color: #10b981; }
.bullet-item .pct-value.orange { color: #f59e0b; }
.bullet-item .pct-value.red { color: #ef4444; }
.narrative-container { margin-bottom: 25px; }
.narrative-region-block { background: var(--bg-card); border-radius: 12px; padding: 16px 20px; margin-bottom: 15px; border: 1px solid var(--border-color); border-left: 4px solid var(--accent); transition: all 0.3s; }
.narrative-region-block:hover { box-shadow: 0 4px 18px rgba(0,0,0,0.12); }
.narrative-region-block.risk-high { border-left-color: #DC2626; }
.narrative-region-block.risk-moderate { border-left-color: #D97706; }
.narrative-region-block.risk-low { border-left-color: #059669; }
.narrative-header { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1em; margin-bottom: 10px; color: var(--text); }
.narrative-body { font-size: 0.88em; color: var(--text-muted); line-height: 1.75; }
.narrative-body .narr-line { margin-bottom: 4px; padding-left: 8px; border-left: 2px solid var(--border-color); }
.narrative-body .narr-ref { color: var(--text); font-weight: 600; }
.narrative-body .narr-neg { color: #DC2626; font-weight: 700; }
.narrative-body .narr-pos { color: #D97706; font-weight: 700; }
.narrative-body .narr-warn { color: #7C3AED; font-style: italic; }
.narrative-body .narr-intro { font-weight: 600; color: var(--text); margin-bottom: 6px; }
.gantt-container { margin-bottom: 25px; }
.gantt-region-group { background: var(--bg-card); border-radius: 14px; padding: 16px 18px; margin-bottom: 18px; border: 1px solid var(--border-color); overflow: hidden; }
.gantt-region-header { display: flex; align-items: center; gap: 10px; font-size: 1.05em; font-weight: 700; padding-bottom: 12px; margin-bottom: 4px; border-bottom: 2px solid var(--border-color); color: var(--text); }
.gantt-region-header .gantt-summary { font-size: 0.8em; font-weight: 400; color: var(--text-muted); margin-left: auto; }
.gantt-row { display: flex; align-items: center; padding: 4px 0; min-height: 34px; border-bottom: 1px solid rgba(255,255,255,0.03); cursor: default; position: relative; }
[data-theme="light"] .gantt-row { border-bottom-color: rgba(0,0,0,0.04); }
[data-theme="professional"] .gantt-row { border-bottom-color: #e5e7eb; }
.gantt-row:hover { background: rgba(0,212,255,0.04); }
.gantt-label-left { width: 245px; min-width: 245px; font-size: 0.78em; padding-right: 10px; overflow: hidden; }
.gantt-label-left .g-ref { font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 235px; }
.gantt-label-left .g-entr { font-size: 0.88em; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gantt-bar-area { flex: 1; height: 22px; position: relative; background: var(--table-header); border-radius: 3px; }
.gantt-bar-track { position: absolute; height: 100%; border-radius: 3px; overflow: visible; }
.gantt-bar-elapsed { position: absolute; height: 100%; left: 0; border-radius: 3px 0 0 3px; transition: width 0.6s ease; min-width: 2px; }
.gantt-bar-elapsed.v-normal { background: linear-gradient(90deg, #059669, #34d399); }
.gantt-bar-elapsed.v-sous { background: linear-gradient(90deg, #DC2626, #F87171); }
.gantt-bar-elapsed.v-depasse { background: linear-gradient(90deg, #D97706, #FBBF24); }
.gantt-bar-elapsed.v-none { background: linear-gradient(90deg, #64748B, #94A3B8); }
.gantt-bar-remaining { position: absolute; height: 100%; right: 0; background: repeating-linear-gradient(-45deg, transparent, transparent 3px, rgba(255,255,255,0.04) 3px, rgba(255,255,255,0.04) 6px); border-radius: 0 3px 3px 0; }
.gantt-today-marker { position: absolute; top: -3px; height: calc(100% + 6px); width: 0; border-left: 2px dashed #7C3AED; z-index: 5; }
.gantt-exhaustion-marker { position: absolute; top: 50%; transform: translate(-50%, -50%) rotate(45deg); width: 9px; height: 9px; background: #DC2626; z-index: 6; box-shadow: 0 0 6px rgba(220,38,38,0.6); border: 1px solid #fff; }
.gantt-label-right { width: 175px; min-width: 175px; text-align: right; font-size: 0.76em; padding-left: 10px; }
.gantt-label-right .g-verdict { font-weight: 700; display: block; }
.gantt-label-right .g-verdict.v-normal { color: #059669; }
.gantt-label-right .g-verdict.v-sous { color: #DC2626; }
.gantt-label-right .g-verdict.v-depasse { color: #D97706; }
.gantt-label-right .g-verdict.v-none { color: #64748B; }
.gantt-label-right .g-amount { color: var(--text-muted); }
.gantt-tt { position: fixed; background: var(--bg-dark); color: var(--text); padding: 14px 16px; border-radius: 10px; box-shadow: 0 10px 35px rgba(0,0,0,0.45); border: 1px solid var(--border-color); z-index: 9999; pointer-events: none; opacity: 0; transition: opacity 0.12s; font-size: 0.82em; min-width: 240px; max-width: 320px; }
.gantt-tt.visible { opacity: 1; }
.gantt-tt h5 { margin-bottom: 8px; color: var(--accent); font-size: 0.95em; }
.gantt-tt .ttr { display: flex; justify-content: space-between; padding: 3px 0; gap: 12px; }
.gantt-tt .ttl { color: var(--text-muted); }
.gantt-tt .ttv { font-weight: 700; text-align: right; }
.gantt-tt .ttv.neg { color: #DC2626; }
.gantt-tt .ttv.pos { color: #D97706; }
.gantt-legend { display: flex; gap: 16px; flex-wrap: wrap; padding: 10px 0; font-size: 0.8em; color: var(--text-muted); }
.gantt-legend-item { display: flex; align-items: center; gap: 6px; }
.gantt-legend-dot { width: 12px; height: 12px; border-radius: 3px; }
.gantt-legend-diamond { width: 10px; height: 10px; transform: rotate(45deg); }
.proactif-alerts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 15px; margin-bottom: 25px; }
.proactif-alert-card { background: var(--bg-card); border-radius: 12px; padding: 16px; border: 1px solid var(--border-color); border-left: 4px solid var(--danger); transition: all 0.3s; }
.proactif-alert-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
.ecart-badge { padding: 2px 8px; border-radius: 10px; font-size: 0.8em; font-weight: 700; white-space: nowrap; }
.ecart-badge.negative { background: rgba(220,38,38,0.12); color: #DC2626; }
.ecart-badge.positive { background: rgba(217,119,6,0.12); color: #D97706; }
.reco-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; margin-top: 15px; }
.reco-card { background: var(--bg-card); border-radius: 12px; padding: 16px 16px 16px 20px; border: 1px solid var(--border-color); border-left: 4px solid #6366F1; transition: all 0.3s; }
.reco-card:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
.priority-rank { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: 800; font-size: 0.85em; color: white; }
.priority-rank.rk-danger { background: linear-gradient(135deg, #DC2626, #EF4444); }
.priority-rank.rk-warning { background: linear-gradient(135deg, #D97706, #F59E0B); }
.priority-rank.rk-info { background: linear-gradient(135deg, #6366F1, #818CF8); }
.action-recommandee { font-size: 0.82em; font-weight: 600; color: var(--accent); }
.kpi-sub-impact { font-size: 0.95em; font-weight: 700; margin-top: 4px; }
.kpi-sub-impact.impact-neg { color: #DC2626; }
.kpi-sub-impact.impact-pos { color: #D97706; }
.kpi-sub-impact.impact-ok { color: #059669; }
.loading-overlay { position: fixed; inset: 0; background: rgba(15,23,42,0.9); display: none; justify-content: center; align-items: center; flex-direction: column; gap: 20px; z-index: 9998; }
.loading-overlay.active { display: flex; }
.spinner { width: 50px; height: 50px; border: 4px solid var(--border-color); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.error-toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: #dc2626; color: white; padding: 15px 30px; border-radius: 10px; z-index: 9999; font-size: 0.95em; box-shadow: 0 5px 20px rgba(0,0,0,0.3); display: none; max-width: 92vw; text-align: center; }
.error-toast.active { display: block; }
.admin-panel table { min-width: auto; }
.admin-panel .action-btn { padding: 6px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.8em; margin-right: 5px; transition: all 0.3s; }
.admin-panel .delete-btn { background: #ef4444; color: white; }
.admin-panel .add-user-btn { padding: 10px 20px; background: linear-gradient(135deg, var(--success), #059669); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 0.9em; margin-bottom: 20px; }
.pdf-preview-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 10001; padding: 20px; }
.pdf-preview-modal.active { display: flex; }
.pdf-preview-content { background: white; border-radius: 12px; width: 100%; max-width: 900px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
.pdf-preview-header { background: linear-gradient(90deg, #dc2626, #b91c1c); padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; color: white; }
.pdf-preview-header h3 { font-size: 1.1em; display: flex; align-items: center; gap: 10px; }
.pdf-preview-actions { display: flex; gap: 10px; }
.pdf-preview-actions button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; display: flex; align-items: center; gap: 6px; transition: all 0.3s; }
.pdf-download-btn { background: white; color: #dc2626; }
.pdf-download-btn:hover { background: #fee2e2; }
.pdf-close-btn { background: rgba(255,255,255,0.2); color: white; }
.pdf-close-btn:hover { background: rgba(255,255,255,0.3); }
.pdf-preview-body { flex: 1; overflow-y: auto; padding: 20px; background: #f3f4f6; }
.pdf-page-preview { background: white; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 0 auto 20px; padding: 40px; max-width: 800px; }
.pdf-page-preview .pdf-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #002855; padding-bottom: 15px; margin-bottom: 20px; }
.pdf-page-preview .pdf-title { text-align: right; }
.pdf-page-preview .pdf-title h1 { color: #002855; font-size: 1.4em; margin: 0; }
.pdf-page-preview .pdf-title p { color: #6b7280; font-size: 0.9em; margin: 5px 0 0; }
.pdf-page-preview .pdf-section { margin-bottom: 25px; }
.pdf-page-preview .pdf-section-title { background: #002855; color: white; padding: 8px 15px; font-size: 1em; font-weight: 600; margin-bottom: 15px; border-radius: 4px; }
.pdf-page-preview table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
.pdf-page-preview th { background: #e5e7eb; color: #374151; padding: 10px 8px; text-align: left; font-weight: 600; border: 1px solid #d1d5db; }
.pdf-page-preview td { padding: 8px; border: 1px solid #d1d5db; color: #374151; }
.pdf-page-preview tr:nth-child(even) td { background: #f9fafb; }
.pdf-page-preview .pdf-footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #d1d5db; text-align: center; font-size: 0.8em; color: #6b7280; }
.pdf-page-preview .pdf-summary { background: #f0f4ff; border: 1px solid #c7d2fe; border-radius: 8px; padding: 14px 18px; margin-bottom: 18px; font-size: 0.9em; line-height: 1.7; color: #1e293b; }
.pdf-page-preview .pdf-summary strong { color: #002855; }
.pdf-page-preview .pdf-note { background: #fffbeb; border-left: 4px solid #f59e0b; padding: 10px 14px; margin-top: 12px; font-size: 0.82em; color: #92400e; border-radius: 0 6px 6px 0; }
.footer { text-align: center; padding: 25px; color: var(--text-muted); font-size: 0.85em; border-top: 1px solid var(--border-color); margin-top: 30px; }
.footer img { width: 45px; height: 45px; border-radius: 50%; border: 2px solid var(--accent); margin-bottom: 8px; object-fit: cover; }
@media (max-width: 768px) {
.header { flex-direction: column; padding: 12px; }
.header-controls { width: 100%; justify-content: center; }
.slide-nav { padding: 10px; }
.slide-nav button { padding: 8px 14px; font-size: 0.75em; }
.slides-container { padding: 12px; }
.kpi-card .value { font-size: 1.6em; }
th, td { padding: 8px 10px; font-size: 0.8em; }
.login-box { padding: 25px; margin: 15px; }
.filters-bar { flex-direction: column; align-items: stretch; }
.filter-group select { width: 100%; }
.filters-panels { gap: 10px; }
.bullet-item { flex-wrap: wrap; }
.bullet-item .ref-container { min-width: 100%; margin-bottom: 5px; }
.bullet-item .entrepreneur { min-width: 100%; }
.region-tooltip { min-width: 300px; max-width: 340px; }
.pdf-preview-content { max-width: 100%; }
.pdf-page-preview { padding: 20px; }
.filter-entrepreneur-bar { flex-wrap: wrap; }
.filter-entrepreneur-bar .filter-bar-label { width: 100%; margin-right: 0; }
.filter-entrepreneur-bar select { min-width: 100%; width: 100%; }
.proactif-alerts-grid { grid-template-columns: 1fr; }
.reco-grid { grid-template-columns: 1fr; }
}
.fade-in { animation: fadeIn 0.5s ease-out both; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
.app-content { display: none; }
.app-content.active { display: block; }
@media print {
body { background: white !important; }
.header, .slide-nav, .footer, .modal-overlay, .pdf-preview-modal { display: none !important; }
.app-content { display: block !important; }
.slide { display: block !important; page-break-after: always; }
.slide:last-child { page-break-after: avoid; }
}
</style>
</head>
<body data-theme="light">
<div class="login-container" id="loginPage">
<div class="login-box">
<img src="logo-TT.png" alt="Tunisie Telecom" class="login-logo">
<h2>Marchés RLA</h2>
<p>Zone Sud — Tableau de Bord</p>
<div class="login-error" id="loginError"><i class="fas fa-exclamation-circle"></i> Identifiants incorrects</div>
<input type="text" id="username" placeholder="Nom d'utilisateur" autocomplete="username">
<input type="password" id="password" placeholder="Mot de passe" autocomplete="current-password">
<button type="button" onclick="handleLogin()"><i class="fas fa-sign-in-alt"></i> Se connecter</button>
<p style="margin-top:20px;font-size:0.8em;color:var(--text-muted);">Accès réservé aux utilisateurs autorisés</p>
</div>
</div>
<div class="app-content" id="appContent">
<div class="loading-overlay" id="loadingOverlay"><div class="spinner"></div><div style="color:var(--accent);font-size:1.1em;">Chargement des données...</div></div>
<div class="error-toast" id="errorToast"></div>
<div class="modal-overlay" id="regionModal"><div class="modal-content"><div class="modal-header"><h3><i class="fas fa-map-marker-alt"></i> <span id="modalRegionName">Région</span></h3><button class="modal-close" onclick="closeModal()"><i class="fas fa-times"></i></button></div><div class="modal-body" id="modalBody"></div></div></div>
<div class="modal-overlay" id="adminModal"><div class="modal-content" style="max-width:700px;"><div class="modal-header" style="background: linear-gradient(90deg, #f59e0b, #d97706);"><h3><i class="fas fa-users-cog"></i> Gestion des Utilisateurs</h3><button class="modal-close" onclick="closeAdminModal()"><i class="fas fa-times"></i></button></div><div class="modal-body" id="adminModalBody"></div></div></div>
<div class="pdf-preview-modal" id="pdfPreviewModal"><div class="pdf-preview-content"><div class="pdf-preview-header"><h3><i class="fas fa-file-pdf"></i> Aperçu PDF</h3><div class="pdf-preview-actions"><button class="pdf-download-btn" onclick="downloadPDF()"><i class="fas fa-download"></i> Télécharger</button><button class="pdf-close-btn" onclick="closePDFPreview()"><i class="fas fa-times"></i> Fermer</button></div></div><div class="pdf-preview-body" id="pdfPreviewBody"></div></div></div>
<header class="header">
<div class="logo-section"><img src="logo-TT.png" alt="Tunisie Telecom"><div><h1>Marchés RLA</h1><div style="font-size:0.8em;color:rgba(255,255,255,0.7);">Zone Sud — Tableau de Bord</div></div></div>
<div class="header-controls">
<div class="theme-selector"><button class="theme-btn" data-theme="dark" onclick="setTheme('dark')" title="Sombre"><i class="fas fa-moon"></i></button><button class="theme-btn" data-theme="light" onclick="setTheme('light')" title="Clair"><i class="fas fa-sun"></i></button><button class="theme-btn" data-theme="professional" onclick="setTheme('professional')" title="Professionnel"><i class="fas fa-briefcase"></i></button></div>
<div class="user-info"><i class="fas fa-user-circle" style="font-size: 1.5em;"></i><span id="currentUser">Utilisateur</span><span class="user-badge" id="userRole">Rôle</span><button class="admin-btn" id="adminBtn" onclick="openAdminModal()"><i class="fas fa-users-cog"></i> Utilisateurs</button><button class="logout-btn" onclick="handleLogout()"><i class="fas fa-sign-out-alt"></i> Déconnexion</button></div>
</div>
<div class="header-info"><div>Dernière mise à jour</div><div class="date" id="currentDate">--/--/---- --:--</div></div>
</header>
<nav class="slide-nav">
<button class="active" onclick="showSlide(0)"><i class="fas fa-map"></i> Cartographie</button>
<button onclick="showSlide(1)"><i class="fas fa-exclamation-triangle"></i> Alertes</button>
<button onclick="showSlide(2)"><i class="fas fa-check-circle"></i> En Service</button>
<button onclick="showSlide(3)"><i class="fas fa-rocket"></i> Pilotage Proactif</button>
<button onclick="showSlide(4)"><i class="fas fa-map-marker-alt"></i> Par Région</button>
<button onclick="showSlide(5)"><i class="fas fa-clock"></i> En Cours</button>
<span class="nav-separator"></span>
<button class="export-btn" onclick="exportPDF()" title="Exporter en PDF"><i class="fas fa-file-pdf"></i> PDF</button>
<button class="export-pptx-btn" id="btnExportPPTX" onclick="exportPPTX()" title="Exporter en PowerPoint" style="display: none;"><i class="fas fa-file-powerpoint"></i> PPTX</button>
<span class="nav-separator"></span>
<button class="refresh-btn" onclick="loadData()" title="Rafraîchir"><i class="fas fa-sync-alt"></i></button>
</nav>
<main class="slides-container">
<!-- SLIDE 0: CARTOGRAPHIE -->
<section class="slide active" id="slide-0" data-title="Cartographie Marchés RLA">
<h2 class="section-title fade-in"><i class="fas fa-map"></i> Cartographie Marchés RLA</h2>
<div class="kpi-grid">
<div class="kpi-card fade-in"><div class="icon" style="color:var(--accent);"><i class="fas fa-file-contract"></i></div><div class="value" id="kpi-total">--</div><div class="label">Marchés Actifs</div><div class="sub" id="kpi-budget">--</div></div>
<div class="kpi-card capex fade-in"><div class="icon" style="color:var(--success);"><i class="fas fa-hard-hat"></i></div><div class="value" id="kpi-capex">--</div><div class="label">CAPEX</div><div class="sub" id="kpi-capex-budget">--</div></div>
<div class="kpi-card opex fade-in"><div class="icon" style="color:var(--warning);"><i class="fas fa-tools"></i></div><div class="value" id="kpi-opex">--</div><div class="label">OPEX</div><div class="sub" id="kpi-opex-budget">--</div></div>
<div class="kpi-card alertes fade-in"><div class="icon" style="color:var(--danger);"><i class="fas fa-exclamation-triangle"></i></div><div class="value" id="kpi-alertes">--</div><div class="label">Alertes</div></div>
<div class="kpi-card clotures fade-in"><div class="icon" style="color:#6b7280;"><i class="fas fa-archive"></i></div><div class="value" id="kpi-clotures">--</div><div class="label">Clôturés</div></div>
</div>
<div class="map-container fade-in">
<div class="map-svg-wrapper" id="mapWrapper">
<svg viewBox="0 0 400 500" id="mapSvg">
<path id="path-Tozeur" class="region-path" d="M50,80 L120,60 L140,100 L100,140 L40,120 Z" fill="#818CF8"/>
<path id="path-Gafsa" class="region-path" d="M120,60 L200,50 L220,110 L140,100 Z" fill="#22C55E"/>
<path id="path-Kebili" class="region-path" d="M100,140 L140,100 L220,110 L240,180 L160,200 L100,180 Z" fill="#9333EA"/>
<path id="path-Sfax" class="region-path" d="M200,50 L280,30 L320,80 L340,160 L280,200 L220,110 Z" fill="#002855"/>
<path id="path-Gabes" class="region-path" d="M220,110 L280,200 L300,280 L240,300 L160,260 L160,200 L240,180 Z" fill="#17A2B8"/>
<path id="path-Medenine" class="region-path" d="M280,200 L340,160 L380,220 L360,320 L300,360 L240,320 L240,300 L300,280 Z" fill="#0EA5E9"/>
<path id="path-Tataouine" class="region-path" d="M160,260 L240,300 L240,320 L300,360 L280,450 L180,480 L100,400 L100,300 Z" fill="#14B8A6"/>
<text x="75" y="100" fill="white" font-size="11" font-weight="bold">Tozeur</text>
<text x="155" y="85" fill="white" font-size="11" font-weight="bold">Gafsa</text>
<text x="155" y="160" fill="white" font-size="11" font-weight="bold">Kebili</text>
<text x="255" y="100" fill="white" font-size="11" font-weight="bold">Sfax</text>
<text x="230" y="220" fill="white" font-size="11" font-weight="bold">Gabes</text>
<text x="300" y="270" fill="white" font-size="11" font-weight="bold">Medenine</text>
<text x="180" y="370" fill="white" font-size="11" font-weight="bold">Tataouine</text>
</svg>
<div class="region-tooltip" id="regionTooltip"></div>
</div>
</div>
<div class="map-legend" id="map-legend"></div>
<div class="bullet-charts-container">
<h3 class="section-title" style="margin-top:30px;"><i class="fas fa-chart-bar"></i> Avancement vs Objectif - Marchés en Service</h3>
<div id="bulletChartsContent"></div>
</div>
</section>
<!-- SLIDE 1: ALERTES -->
<section class="slide" id="slide-1" data-title="Alertes">
<h2 class="section-title"><i class="fas fa-exclamation-triangle" style="color:var(--danger)"></i> Alertes Consommation</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #b91c1c, #dc2626);"><h3><i class="fas fa-fire"></i> Marchés à Surveiller</h3><span class="badge" id="alertes-count">0 alertes</span></div>
<div class="table-wrapper"><table><thead><tr><th>Référence</th><th>Entrepreneur</th><th>Projet</th><th>Période</th><th>Avancement</th><th>Délai</th><th>Statut</th></tr></thead><tbody id="alertes-table"></tbody></table></div>
</div>
<h2 class="section-title" style="margin-top:30px;"><i class="fas fa-tools" style="color:#a855f7"></i> Suivi Modernisation par Région</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #7c3aed, #a855f7);"><h3><i class="fas fa-hammer"></i> Marché en vigueur + Pipeline</h3><span class="badge" id="modernisation-count">0 régions</span></div>
<div class="table-wrapper"><table><thead><tr>
<th>Région</th>
<th>Marché en vigueur</th>
<th>Av. Phy</th>
<th>Estimation</th>
<th>Statut Pipeline</th>
</tr></thead><tbody id="modernisation-table"></tbody></table></div>
</div>
<h2 class="section-title" style="margin-top:30px;"><i class="fas fa-rocket" style="color:var(--danger)"></i> Alerte Lancement</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #b91c1c, #dc2626);"><h3><i class="fas fa-exclamation-circle"></i> Marchés à lancer par la Zone</h3><span class="badge" id="infructueux-count">0 marchés</span></div>
<div class="table-wrapper"><table><thead><tr><th>Région</th><th>Référence</th><th>Projet</th><th>Observation</th></tr></thead><tbody id="infructueux-table"></tbody></table></div>
</div>
<h2 class="section-title" style="margin-top:30px;"><i class="fas fa-layer-group" style="color:#6366F1"></i> Pipeline de Lancement</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #4F46E5, #6366F1);"><h3><i class="fas fa-rocket"></i> Projets en Préparation (Table 872)</h3><span class="badge" id="pipeline-count">0 projets</span></div>
<div class="table-wrapper"><table><thead><tr>
<th>Projet</th>
<th>Régions</th>
<th>Estimation</th>
<th>Durée</th>
<th>Statut DCA</th>
</tr></thead><tbody id="pipeline-table"></tbody></table></div>
</div>
</section>
<!-- SLIDE 2: EN SERVICE -->
<section class="slide" id="slide-2" data-title="Marchés En Service">
<h2 class="section-title"><i class="fas fa-check-circle" style="color:var(--success)"></i> Marchés En Service</h2>
<div class="filters-panels">
<section class="filter-panel"><div class="filter-panel-header"><div class="filter-panel-title"><i class="fas fa-filter"></i><span>Filtres Projets</span></div><div class="filter-panel-sub">Clic pour (dé)sélectionner — aucune sélection = tous les projets</div></div><div class="filter-panel-body"><div id="filter-projet-chips" class="filter-chips"></div></div><button class="filter-panel-reset" data-target="projet" onclick="resetServiceFilters('projet')" title="Réinitialiser les projets"><i class="fas fa-undo"></i></button></section>
<section class="filter-panel"><div class="filter-panel-header"><div class="filter-panel-title"><i class="fas fa-map-marker-alt"></i><span>Filtres Régions</span></div><div class="filter-panel-sub">Clic pour (dé)sélectionner — aucune sélection = toutes les régions</div></div><div class="filter-panel-body"><div id="filter-region-chips" class="filter-chips"></div></div><button class="filter-panel-reset" data-target="region" onclick="resetServiceFilters('region')" title="Réinitialiser les régions"><i class="fas fa-undo"></i></button></section>
</div>
<div class="filter-entrepreneur-bar">
<div class="filter-bar-label"><i class="fas fa-hard-hat"></i><span>Entrepreneur</span></div>
<select id="filter-entrepreneur-select-1" onchange="applyServiceFilters()"><option value="">Entrepreneur 1...</option></select>
<select id="filter-entrepreneur-select-2" onchange="applyServiceFilters()"><option value="">Entrepreneur 2...</option></select>
<select id="filter-entrepreneur-select-3" onchange="applyServiceFilters()"><option value="">Entrepreneur 3...</option></select>
<button class="filter-entrepreneur-reset" onclick="resetEntrepreneurFilters()" title="Réinitialiser les entrepreneurs"><i class="fas fa-undo"></i></button>
</div>
<div class="table-container" style="margin-top:20px;">
<div class="table-header" style="background: linear-gradient(90deg, #047857, #10b981);"><h3><i class="fas fa-play-circle"></i> En Cours d'Exécution</h3><span class="badge" id="service-count">0 marchés</span></div>
<div class="table-wrapper"><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Montant Max</th><th>Période</th><th>Av. Phy</th><th>Av. Fin</th></tr></thead><tbody id="service-table"></tbody></table></div>
</div>
</section>
<!-- SLIDE 3: PILOTAGE PROACTIF -->
<section class="slide" id="slide-3" data-title="Pilotage Proactif">
<h2 class="section-title fade-in"><i class="fas fa-rocket" style="color:#6366F1"></i> Pilotage Proactif</h2>
<div class="kpi-grid" id="proactif-kpi-grid">
<div class="kpi-card proactif-normal fade-in"><div class="icon" style="color:#059669;"><i class="fas fa-check-double"></i></div><div class="value" id="proactif-kpi-normal" style="color:#059669;">--</div><div class="label">Normal</div><div class="sub">Projection entre Min et Max</div><div class="kpi-sub-impact impact-ok" id="proactif-kpi-normal-impact"></div></div>
<div class="kpi-card proactif-sous fade-in"><div class="icon" style="color:#DC2626;"><i class="fas fa-arrow-down"></i></div><div class="value" id="proactif-kpi-sous" style="color:#DC2626;">--</div><div class="label">Sous Montant Min</div><div class="sub">Risque non-atteinte seuil</div><div class="kpi-sub-impact impact-neg" id="proactif-kpi-sous-impact"></div></div>
<div class="kpi-card proactif-depasse fade-in"><div class="icon" style="color:#D97706;"><i class="fas fa-arrow-up"></i></div><div class="value" id="proactif-kpi-depasse" style="color:#D97706;">--</div><div class="label">Dépassement</div><div class="sub">Projection > Montant Max</div><div class="kpi-sub-impact impact-pos" id="proactif-kpi-depasse-impact"></div></div>
<div class="kpi-card proactif-none fade-in"><div class="icon" style="color:#64748B;"><i class="fas fa-question-circle"></i></div><div class="value" id="proactif-kpi-none" style="color:#64748B;">--</div><div class="label">Indéterminé</div><div class="sub">Données insuffisantes</div></div>
</div>
<div class="filters-panels">
<section class="filter-panel proactif-theme"><div class="filter-panel-header"><div class="filter-panel-title"><i class="fas fa-filter"></i><span>Filtres Projets</span></div><div class="filter-panel-sub">Clic pour (dé)sélectionner — aucune sélection = tous</div></div><div class="filter-panel-body"><div id="proactif-filter-projet-chips" class="filter-chips"></div></div><button class="filter-panel-reset" onclick="resetProactifFilters('projet')" title="Réinitialiser"><i class="fas fa-undo"></i></button></section>
<section class="filter-panel proactif-theme"><div class="filter-panel-header"><div class="filter-panel-title"><i class="fas fa-map-marker-alt"></i><span>Filtres Régions</span></div><div class="filter-panel-sub">Clic pour (dé)sélectionner — aucune sélection = toutes</div></div><div class="filter-panel-body"><div id="proactif-filter-region-chips" class="filter-chips"></div></div><button class="filter-panel-reset" onclick="resetProactifFilters('region')" title="Réinitialiser"><i class="fas fa-undo"></i></button></section>
</div>
<div class="filter-entrepreneur-bar proactif-theme">
<div class="filter-bar-label"><i class="fas fa-hard-hat"></i><span>Entrepreneur</span></div>
<select id="proactif-filter-entrepreneur-select-1" onchange="applyProactifFilters()"><option value="">Entrepreneur 1...</option></select>
<select id="proactif-filter-entrepreneur-select-2" onchange="applyProactifFilters()"><option value="">Entrepreneur 2...</option></select>
<select id="proactif-filter-entrepreneur-select-3" onchange="applyProactifFilters()"><option value="">Entrepreneur 3...</option></select>
<button class="filter-entrepreneur-reset" onclick="resetProactifEntrepreneurFilters()" title="Réinitialiser"><i class="fas fa-undo"></i></button>
</div>
<h2 class="section-title" style="margin-top:25px;"><i class="fas fa-file-alt" style="color:#6366F1"></i> Synthèse Opérationnelle</h2>
<div class="narrative-container" id="proactif-narrative-container"></div>
<h2 class="section-title" style="margin-top:25px;"><i class="fas fa-chart-gantt" style="color:#6366F1"></i> Trajectoire par Région</h2>
<div class="gantt-legend">
<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:#059669;"></div> Normal</div>
<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:#D97706;"></div> Sous Min</div>
<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:#DC2626;"></div> Critique</div>
<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:#94A3B8;"></div> Indéterminé</div>
<div class="gantt-legend-item"><div class="gantt-legend-diamond" style="background:#DC2626;"></div> Épuisement</div>
<div class="gantt-legend-item" style="border-left:2px dashed #6366F1;padding-left:8px;">Aujourd'hui</div>
</div>
<div class="gantt-container" id="proactif-gantt-container"></div>
<div class="gantt-tt" id="ganttTooltip"></div>
<h2 class="section-title" style="margin-top:25px;"><i class="fas fa-bullseye" style="color:#DC2626"></i> Actions Prioritaires</h2>
<div class="priority-container" id="proactif-actions-container"></div>
<h2 class="section-title" style="margin-top:30px;"><i class="fas fa-shield-alt" style="color:#0B2A55"></i> Matrice de Risque par Région</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #0B2A55, #1e40af);"><h3><i class="fas fa-th"></i> Évaluation Risque</h3></div>
<div class="table-wrapper"><table><thead><tr><th>Région</th><th>Marchés</th><th>Budget</th><th>Av. Phy</th><th>Av. Fin</th><th>Écart Phy-Fin</th><th>Projection</th><th>Tendance</th><th>Risque</th></tr></thead><tbody id="proactif-risque-table"></tbody></table></div>
</div>
</section>
<!-- SLIDE 4: PAR RÉGION -->
<section class="slide" id="slide-4" data-title="Détail par Région">
<h2 class="section-title"><i class="fas fa-map-marker-alt" style="color:var(--accent)"></i> Détail par Région</h2>
<div class="regions-grid" id="regions-grid"></div>
</section>
<!-- SLIDE 5: EN COURS -->
<section class="slide" id="slide-5" data-title="Marchés En Cours">
<h2 class="section-title"><i class="fas fa-clock" style="color:var(--warning)"></i> Marchés En Cours</h2>
<div class="table-container">
<div class="table-header" style="background: linear-gradient(90deg, #0369a1, #0ea5e9);"><h3><i class="fas fa-hourglass-half"></i> En Attente / Évaluation</h3><span class="badge" id="encours-count">0 marchés</span></div>
<div class="table-wrapper"><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Observation</th></tr></thead><tbody id="encours-table"></tbody></table></div>
</div>
</section>
</main>
<footer class="footer">
<img src="Nabil.Derouiche.jpg" alt="Nabil Derouiche">
<div><strong style="color:var(--accent);">Nabil Derouiche</strong></div>
<div>Responsable Achats Zone Sud — Tunisie Telecom</div>
<div style="margin-top:8px;font-size:0.8em;"><a href="mailto:Nabil.Derouiche@tunisietelecom.tn" style="color:var(--accent);text-decoration:none;"><i class="fas fa-envelope"></i> Nabil.Derouiche@tunisietelecom.tn</a></div>
</footer>
</div>
<script>
const CONFIG = {
API_URL: 'https://baserow.bolbol.tn/api/database/rows/table/856/',
API_URL_PIPELINE: 'https://baserow.bolbol.tn/api/database/rows/table/872/',
API_TOKEN: 'zJaDdkttN1gr6oPvd3cxfCXNwzvvwMMF',
REFRESH_INTERVAL: 60, SEUIL_STANDARD: 70, SEUIL_MODERNISATION: 50,
SEUIL_CRITIQUE: 90, DELAI_CRITIQUE: 45, DELAI_ATTENTION: 90, DEFAULT_THEME: 'light',
REGION_COLORS: { 'Gabes':'#17A2B8','Gafsa':'#22C55E','Kebili':'#9333EA','Medenine':'#0EA5E9','Sfax':'#002855','Tataouine':'#14B8A6','Tozeur':'#818CF8' }
};
const MONTH_MS = 30.44 * 86400000;
const LOGIN_LOG_KEY = 'rla_login_history';
function sanitizePPTX(text) { return String(text||'').replace(/&quot;/g,'"').replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&#039;/g,"'").replace(/\u2212/g,'-'); }
function formatMontantPPTX(val) { return sanitizePPTX(formatMontant(val)); }
let USERS = {
'Nabil.Derouiche':{password:'03167442',role:'superadmin',name:'Nabil Derouiche',region:null},
'admin':{password:'admin2025',role:'admin',name:'Administrateur',region:null},
'nabil':{password:'nabil2025',role:'admin',name:'Nabil Derouiche',region:null},
'ikram':{password:'ikram',role:'admin',name:'Ikram Abdmoulah',region:null},
'sfax':{password:'sfax2025',role:'user',name:'Agent Sfax',region:'Sfax'},
};
const ALL_REGIONS = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur'];
let currentUser = null, allData = [], filteredData = [], currentSlide = 0, pipelineData = [];
function showLoading(show) { document.getElementById('loadingOverlay').classList.toggle('active', !!show); }
function showError(msg) { const t = document.getElementById('errorToast'); t.textContent = msg||'Erreur'; t.classList.add('active'); setTimeout(() => t.classList.remove('active'), 5000); }
function setTheme(theme) { document.body.setAttribute('data-theme', theme); document.querySelectorAll('.theme-btn').forEach(b => b.classList.toggle('active', b.dataset.theme === theme)); localStorage.setItem('theme', theme); }
function loadTheme() { setTheme(localStorage.getItem('theme') || CONFIG.DEFAULT_THEME); }
function closeModal() { document.getElementById('regionModal').classList.remove('active'); }
function closePDFPreview() { document.getElementById('pdfPreviewModal').classList.remove('active'); }
function trackLogin(user) { const h = JSON.parse(localStorage.getItem(LOGIN_LOG_KEY)||'[]'); h.unshift({username:user.username,name:user.name,role:user.role,region:user.region||'Toutes',timestamp:new Date().toISOString()}); if(h.length>50)h.length=50; localStorage.setItem(LOGIN_LOG_KEY,JSON.stringify(h)); }
function getLoginHistory() { return JSON.parse(localStorage.getItem(LOGIN_LOG_KEY)||'[]'); }
function clearLoginHistory() { localStorage.removeItem(LOGIN_LOG_KEY); renderAdminPanel(); }
function handleLogin() {
const username=document.getElementById('username').value.trim(), password=document.getElementById('password').value, errorEl=document.getElementById('loginError');
errorEl.style.display='none'; const user=USERS[username];
if(user&&user.password===password) { currentUser={username,...user}; sessionStorage.setItem('currentUser',JSON.stringify(currentUser)); trackLogin(currentUser);
document.getElementById('loginPage').style.display='none'; document.getElementById('appContent').classList.add('active');
document.getElementById('currentUser').textContent=currentUser.name; const roleEl=document.getElementById('userRole');
if(currentUser.role==='superadmin'){roleEl.textContent='Super Admin';roleEl.classList.add('super-admin');}else if(currentUser.role==='admin'){roleEl.textContent='Admin';}else{roleEl.textContent=currentUser.region;}
document.getElementById('adminBtn').style.display=currentUser.role==='superadmin'?'inline-block':'none';
document.getElementById('btnExportPPTX').style.display=currentUser.role==='superadmin'?'inline-flex':'none'; loadData();
} else { errorEl.style.display='block'; setTimeout(()=>errorEl.style.display='none',3000); }
}
function handleLogout() { sessionStorage.removeItem('currentUser'); currentUser=null; document.getElementById('appContent').classList.remove('active'); document.getElementById('loginPage').style.display='flex'; document.getElementById('username').value=''; document.getElementById('password').value=''; }
function checkSession() { const s=sessionStorage.getItem('currentUser'); if(!s)return; try{currentUser=JSON.parse(s); document.getElementById('loginPage').style.display='none'; document.getElementById('appContent').classList.add('active'); document.getElementById('currentUser').textContent=currentUser.name; const r=document.getElementById('userRole'); if(currentUser.role==='superadmin'){r.textContent='Super Admin';r.classList.add('super-admin');}else if(currentUser.role==='admin'){r.textContent='Admin';}else{r.textContent=currentUser.region;} document.getElementById('adminBtn').style.display=currentUser.role==='superadmin'?'inline-block':'none'; document.getElementById('btnExportPPTX').style.display=currentUser.role==='superadmin'?'inline-flex':'none'; loadData();}catch(_){sessionStorage.removeItem('currentUser');} }
document.addEventListener('keydown',e=>{if(e.key==='Enter'&&document.getElementById('loginPage').style.display!=='none')handleLogin();if(e.key==='Escape'){closeModal();closeAdminModal();closePDFPreview();}});
function openAdminModal(){if(currentUser?.role!=='superadmin')return;renderAdminPanel();document.getElementById('adminModal').classList.add('active');}
function closeAdminModal(){document.getElementById('adminModal').classList.remove('active');}
function renderAdminPanel(){
let html=`<button class="add-user-btn" onclick="showAddUserForm()"><i class="fas fa-user-plus"></i> Ajouter</button><div id="addUserForm" style="display:none;margin-bottom:20px;padding:15px;background:var(--table-header);border-radius:10px;"><h4 style="margin-bottom:10px;">Nouvel utilisateur</h4><input type="text" id="newUsername" placeholder="Identifiant" style="width:100%;padding:8px;margin-bottom:8px;border-radius:6px;border:1px solid var(--border-color);"><input type="password" id="newPassword" placeholder="Mot de passe" style="width:100%;padding:8px;margin-bottom:8px;border-radius:6px;border:1px solid var(--border-color);"><input type="text" id="newName" placeholder="Nom complet" style="width:100%;padding:8px;margin-bottom:8px;border-radius:6px;border:1px solid var(--border-color);"><select id="newRole" style="width:100%;padding:8px;margin-bottom:8px;border-radius:6px;border:1px solid var(--border-color);"><option value="user">User</option><option value="admin">Admin</option></select><select id="newRegion" style="width:100%;padding:8px;margin-bottom:8px;border-radius:6px;border:1px solid var(--border-color);"><option value="">Toutes régions</option>${ALL_REGIONS.map(r=>`<option value="${r}">${r}</option>`).join('')}</select><button onclick="addUser()" style="padding:8px 15px;background:var(--success);color:white;border:none;border-radius:6px;cursor:pointer;">Créer</button> <button onclick="hideAddUserForm()" style="padding:8px 15px;background:var(--danger);color:white;border:none;border-radius:6px;cursor:pointer;">Annuler</button></div><table><thead><tr><th>ID</th><th>Nom</th><th>Rôle</th><th>Région</th><th>Actions</th></tr></thead><tbody>`;
for(const[u,user]of Object.entries(USERS)){const cd=u!=='Nabil.Derouiche';html+=`<tr><td><strong>${escapeHtml(u)}</strong></td><td>${escapeHtml(user.name)}</td><td><span class="status-badge ${user.role==='superadmin'?'attention':user.role==='admin'?'ok':''}">${user.role}</span></td><td>${user.region||'Toutes'}</td><td>${cd?`<button class="action-btn delete-btn" onclick="deleteUser('${u}')"><i class="fas fa-trash"></i></button>`:'<span style="color:var(--text-muted);font-size:0.8em;">Protégé</span>'}</td></tr>`;}
html+='</tbody></table>';
const history=getLoginHistory();
html+=`<div style="margin-top:30px;padding-top:20px;border-top:2px solid var(--border-color);"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;"><h3 style="display:flex;align-items:center;gap:10px;font-size:1.1em;"><i class="fas fa-history" style="color:var(--accent);"></i> Historique des Connexions</h3>${history.length>0?'<button onclick="clearLoginHistory()" style="padding:6px 14px;background:var(--danger);color:white;border:none;border-radius:6px;cursor:pointer;font-size:0.8em;"><i class="fas fa-trash-alt"></i> Vider</button>':''}</div>`;
if(!history.length){html+='<p style="color:var(--text-muted);text-align:center;padding:15px;">Aucune connexion enregistrée</p>';}else{html+='<table><thead><tr><th>Utilisateur</th><th>Rôle</th><th>Région</th><th>Date & Heure</th></tr></thead><tbody>';history.forEach(h=>{const dt=new Date(h.timestamp);html+=`<tr><td><strong>${escapeHtml(h.name||h.username)}</strong><br><span style="font-size:0.8em;color:var(--text-muted);">${escapeHtml(h.username)}</span></td><td><span class="status-badge ${h.role==='superadmin'?'attention':h.role==='admin'?'ok':''}">${h.role}</span></td><td>${escapeHtml(h.region)}</td><td><span style="font-weight:600;">${dt.toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit',year:'numeric'})}</span><br><span style="font-size:0.85em;color:var(--text-muted);">${dt.toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'})}</span></td></tr>`;});html+='</tbody></table>';}
html+='</div>'; document.getElementById('adminModalBody').innerHTML=html;
}
function showAddUserForm(){document.getElementById('addUserForm').style.display='block';}
function hideAddUserForm(){document.getElementById('addUserForm').style.display='none';}
function addUser(){const u=document.getElementById('newUsername').value.trim(),p=document.getElementById('newPassword').value,n=document.getElementById('newName').value.trim(),r=document.getElementById('newRole').value,rg=document.getElementById('newRegion').value||null;if(!u||!p||!n){alert('Champs requis');return;}if(USERS[u]){alert('ID existe');return;}USERS[u]={password:p,name:n,role:r,region:r==='user'?rg:null};hideAddUserForm();renderAdminPanel();}
function deleteUser(u){if(u==='Nabil.Derouiche')return;if(!confirm(`Supprimer "${u}" ?`))return;delete USERS[u];renderAdminPanel();}
function parseNum(v){if(v===null||v===undefined)return 0;const n=parseFloat(String(v).replace(/\s/g,'').replace(',','.'));return Number.isFinite(n)?n:0;}
function formatMontant(v){return parseNum(v).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,' ')+' DT';}
function formatDateFR(d){if(!d)return'-';const dt=new Date(d);if(isNaN(dt.getTime()))return String(d);return`${String(dt.getDate()).padStart(2,'0')}/${String(dt.getMonth()+1).padStart(2,'0')}/${dt.getFullYear()}`;}
function formatPeriode(debut,fin){const d=formatDateFR(debut),f=formatDateFR(fin);if(d==='-'&&f==='-')return'-';return`${d}${f}`;}
function calcPct(a,t){const av=parseNum(a),tt=parseNum(t);return tt>0?Math.round((av/tt)*100):0;}
function getProgressBar(pct,isMod=false){const s=isMod?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD;let c='green';if(pct>=CONFIG.SEUIL_CRITIQUE)c='red';else if(pct>=s)c='orange';return`<div class="progress-bar"><div class="progress-track"><div class="progress-fill ${c}" style="width:${Math.min(100,pct)}%"></div></div><span class="progress-value">${pct}%</span></div>`;}
function escapeHtml(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]));}
function isValidAO(r){if(!r)return false;return!/^AO\s*00\/0000/i.test(String(r).trim());}
function isLot00(l){if(!l)return false;return/^Lot\s*0?0$/i.test(String(l).trim());}
function shouldDisplayMarche(r){return isValidAO(r.ref||r.reference||'');}
function extractRegionsFromCSC(csc){if(!csc)return[];const regions=[],s=String(csc).toLowerCase(),parts=s.split(',').map(p=>p.trim());parts.forEach(p=>{if(p.includes('gabes')||p.includes('gabès')){if(!regions.includes('Gabes'))regions.push('Gabes');}if(p.includes('gafsa')||p.includes('metlaoui')){if(!regions.includes('Gafsa'))regions.push('Gafsa');}if(p.includes('kebili')||p.includes('kébili')){if(!regions.includes('Kebili'))regions.push('Kebili');}if(p.includes('medenine')||p.includes('médenine')||p.includes('jerba')||p.includes('zarzis')||p.includes('ben guerdene')){if(!regions.includes('Medenine'))regions.push('Medenine');}if(p.includes('sfax')){if(!regions.includes('Sfax'))regions.push('Sfax');}if(p.includes('tataouine')){if(!regions.includes('Tataouine'))regions.push('Tataouine');}if(p.includes('tozeur')){if(!regions.includes('Tozeur'))regions.push('Tozeur');}});return regions;}
function getRegion(r){const reg=String(r.region||r.csc||'').toLowerCase();if(reg.includes('gabes')||reg.includes('gabès'))return'Gabes';if(reg.includes('gafsa')||reg.includes('metlaoui'))return'Gafsa';if(reg.includes('kebili')||reg.includes('kébili'))return'Kebili';if(reg.includes('medenine')||reg.includes('médenine')||reg.includes('jerba'))return'Medenine';if(reg.includes('sfax'))return'Sfax';if(reg.includes('tataouine'))return'Tataouine';if(reg.includes('tozeur'))return'Tozeur';return'Zone Sud';}
function isZoneSud(r){return getRegion(r)==='Zone Sud';}
function getRegionsForMarcheV2(r){const c=String(r.csc||r.region||'').toLowerCase();if(c.includes('zone sud'))return[...ALL_REGIONS];const ex=extractRegionsFromCSC(r.csc||r.region||'');if(ex.length>1)return ex;const sr=getRegion(r);return sr==='Zone Sud'?[...ALL_REGIONS]:[sr];}
function isCloture(r){const o=String(r.observation||'').toLowerCase();return o.includes('clôtur')||o.includes('clotur')||!!r.date_cloture;}
function isInfructueux(r) {
const o = String(r.observation || '').toLowerCase();
return o.includes('infructueux');
}
function isEnService(r){if(isCloture(r)||isInfructueux(r))return false;const o=String(r.observation||'').toLowerCase(),s=String(r.statut||'').toLowerCase();return o.includes('en service')||s.includes('en service');}
function isModernisation(r){return String(r.projet||'').toLowerCase().includes('modern');}
function getDelaiRestant(r){if(r.delai_restant!=null)return parseInt(r.delai_restant,10);if(!r.date_fin)return null;const f=new Date(r.date_fin);if(isNaN(f.getTime()))return null;return Math.ceil((f-new Date())/86400000);}
function getObservation(r) {
return r.observation || '-';
}
function getMainReference(r){const ref=r.ref||r.reference||'',lots=r.lots||'';let m=ref;if(lots&&!isLot00(lots))m+=` : ${lots}`;return m.trim()||'-';}
function getReferenceWithCSC(r){const m=getMainReference(r),c=r.csc||'';return`<div><strong>${escapeHtml(m)}</strong>${c?`<span class="csc-tag">${escapeHtml(c)}</span>`:''}</div>`;}
function getFullReference(r){const m=getMainReference(r),c=r.csc||'';return c?`${m} ${c}`:m;}
function normalizeRow(r){const p=v=>(v&&typeof v==='object'&&'value' in v)?v.value:v;return{...r,ref:p(r.ref??r.reference),lots:p(r.lots),csc:p(r.csc??r.regioncsc??r.region_csc),projet:p(r.projet),entrepreneur:p(r.entrepreneur),region:p(r.region),nature:p(r.nature),observation:p(r.observation),tot_marche:p(r.tot_marche??r.totmarche),m_min:p(r.m_min??r.mmin??r.montant_min),avt_phy:p(r.avt_phy??r.avtphy),avt_fin:p(r.avt_fin??r.avtfin),date_debut:p(r.date_debut??r.datedebut),date_fin:p(r.date_fin??r.datefin),statut:p(r.statut)};}
function normalizePipelineRow(r) {
const p = v => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
const regStr = String(p(r['Regions']) || '');
return {
id: r.id,
projet: p(r['Description du projet']) || '',
regions: regStr.split(',').map(s => s.trim()).filter(Boolean),
estimation: p(r['Estimation']) || '',
duree: p(r['Duree']) || '',
statut_dca: p(r['Date_previsionnelle_de_la_communication_du_projet_a_la_DCA']) || ''
};
}
function getPipelineForRegion(region, projet) {
return pipelineData.filter(p => {
const matchReg = p.regions.includes(region);
const matchProj = projet ? p.projet.toLowerCase() === projet.toLowerCase() : true;
return matchReg && matchProj;
});
}
function findPipelineForInfructueux(r) {
const projet = String(r.projet || '').toLowerCase();
const regions = getRegionsForMarcheV2(r);
return pipelineData.find(p =>
p.projet.toLowerCase() === projet &&
p.regions.some(pr => regions.includes(pr))
);
}
function isModernisationDecision(pRow) {
return pRow.projet === 'Modernisation' &&
String(pRow.statut_dca).toLowerCase().includes('pas de march');
}
function getPipelineStatutBadge(statut) {
if (!statut) return '<span class="status-badge">-</span>';
const s = statut.toLowerCase();
if (s.includes('pas de march'))
return `<span class="status-badge" style="background:rgba(37,99,235,0.15);color:#2563eb;"><i class="fas fa-info-circle"></i> ${escapeHtml(statut)}</span>`;
if (s.includes('communiqu') || s.includes('transmis') || s.includes('déjà transmis'))
return `<span class="status-badge ok"><i class="fas fa-paper-plane"></i> ${escapeHtml(statut)}</span>`;
return `<span class="status-badge attention"><i class="fas fa-clock"></i> Prévu ${escapeHtml(statut)}</span>`;
}
async function loadData() {
showLoading(true);
try {
const headers = { 'Authorization': `Token ${CONFIG.API_TOKEN}` };
const [res1, res2] = await Promise.all([
fetch(`${CONFIG.API_URL}?user_field_names=true&size=200`, { headers }),
fetch(`${CONFIG.API_URL_PIPELINE}?user_field_names=true&size=200`, { headers })
]);
if (!res1.ok) throw new Error(`API 856: ${res1.status}`);
if (!res2.ok) throw new Error(`API 872: ${res2.status}`);
const [json1, json2] = await Promise.all([res1.json(), res2.json()]);
allData = (json1.results || []).map(normalizeRow).filter(r => shouldDisplayMarche(r));
pipelineData = (json2.results || []).map(normalizePipelineRow);
if (currentUser?.role === 'user' && currentUser.region) {
filteredData = allData.filter(r => getRegionsForMarcheV2(r).includes(currentUser.region));
} else {
filteredData = [...allData];
}
renderAll();
initMapInteractions();
document.getElementById('currentDate').textContent = new Date().toLocaleString('fr-FR');
} catch (e) {
showError('Erreur chargement données');
console.error(e);
} finally {
showLoading(false);
}
}
setInterval(()=>{if(currentUser&&document.getElementById('appContent').classList.contains('active'))loadData();},CONFIG.REFRESH_INTERVAL*60*1000);
function showSlide(index){
currentSlide=index;
document.querySelectorAll('.slide').forEach((s,i)=>s.classList.toggle('active',i===index));
document.querySelectorAll('.slide-nav button').forEach((b,i)=>{if(i<6)b.classList.toggle('active',i===index);});
if(index===0)initMapInteractions();
if(index===2)populateServiceFilters();
if(index===3)populateProactifFilters();
}
function buildByRegion(){const actifs=filteredData.filter(r=>!isCloture(r));const byR={};ALL_REGIONS.forEach(r=>byR[r]=[]);byR['Zone Sud']=[];actifs.forEach(r=>{const regions=getRegionsForMarcheV2(r);const isZS=isZoneSud(r);const isMulti=regions.length>1&&!isZS;if(isZS){byR['Zone Sud'].push({...r,isShared:true,isZoneSud:true});ALL_REGIONS.forEach(reg=>byR[reg].push({...r,isShared:true,isZoneSud:true}));}else if(isMulti){regions.forEach(reg=>{if(byR[reg])byR[reg].push({...r,isShared:true,isMultiRegion:true,coveredRegions:regions});});}else{const reg=regions[0];if(byR[reg])byR[reg].push(r);}});return byR;}
function countMarchesForRegion(rows){const own=rows.filter(x=>!x.isShared),zoneSud=rows.filter(x=>x.isZoneSud),multiRegion=rows.filter(x=>x.isMultiRegion&&!x.isZoneSud);return{own,zoneSud,multiRegion,total:rows.length};}
function getRegionTag(counts){const p=[];if(counts.zoneSud.length>0)p.push(`+${counts.zoneSud.length} ZS`);if(counts.multiRegion.length>0)p.push(`+${counts.multiRegion.length} Multi`);if(!p.length)return'';return`<span class="region-tag">${p.join(', ')}</span>`;}
function getRegionsSansModernisation(){const actifs=filteredData.filter(r=>!isCloture(r));const s=new Set();actifs.forEach(r=>{if(isModernisation(r))getRegionsForMarcheV2(r).forEach(reg=>s.add(reg));});return ALL_REGIONS.filter(r=>!s.has(r));}
function renderAll(){
renderKPIs();
renderAlertes();
renderModernisationAlertes();
renderInfructueux();
renderPipeline();
renderEnService();
renderEnCours();
renderRegions();
renderMapLegend();
renderBulletCharts();
renderPilotageProactif();
}
function renderKPIs(){const actifs=filteredData.filter(r=>!isCloture(r)),clotures=filteredData.filter(r=>isCloture(r)),capex=actifs.filter(r=>String(r.nature||'').toUpperCase()==='CAPEX'),opex=actifs.filter(r=>String(r.nature||'').toUpperCase()==='OPEX'),enService=actifs.filter(r=>isEnService(r)),alertes=enService.filter(r=>{const p=calcPct(r.avt_phy,r.tot_marche);return p>=(isModernisation(r)?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD);});document.getElementById('kpi-total').textContent=actifs.length;document.getElementById('kpi-budget').textContent=formatMontant(actifs.reduce((s,r)=>s+parseNum(r.tot_marche),0));document.getElementById('kpi-capex').textContent=capex.length;document.getElementById('kpi-capex-budget').textContent=formatMontant(capex.reduce((s,r)=>s+parseNum(r.tot_marche),0));document.getElementById('kpi-opex').textContent=opex.length;document.getElementById('kpi-opex-budget').textContent=formatMontant(opex.reduce((s,r)=>s+parseNum(r.tot_marche),0));document.getElementById('kpi-alertes').textContent=alertes.length;document.getElementById('kpi-clotures').textContent=clotures.length;}
function renderAlertes(){const enService=filteredData.filter(r=>!isCloture(r)&&isEnService(r)),alertes=enService.filter(r=>{const p=calcPct(r.avt_phy,r.tot_marche);return p>=(isModernisation(r)?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD);}).sort((a,b)=>calcPct(b.avt_phy,b.tot_marche)-calcPct(a.avt_phy,a.tot_marche));document.getElementById('alertes-count').textContent=`${alertes.length} alertes`;let html='';alertes.forEach(r=>{const pct=calcPct(r.avt_phy,r.tot_marche),delai=getDelaiRestant(r),isMod=isModernisation(r),status=pct>=CONFIG.SEUIL_CRITIQUE||(delai!==null&&delai<=CONFIG.DELAI_CRITIQUE)?'<span class="status-badge critique"><i class="fas fa-fire"></i> Critique</span>':'<span class="status-badge attention"><i class="fas fa-exclamation"></i> Attention</span>';html+=`<tr><td>${getReferenceWithCSC(r)}</td><td>${escapeHtml(r.entrepreneur||'-')}</td><td>${escapeHtml(r.projet||'-')}</td><td class="periode-tag">${formatPeriode(r.date_debut,r.date_fin)}</td><td>${getProgressBar(pct,isMod)}</td><td><strong style="color:${delai<=CONFIG.DELAI_CRITIQUE?'var(--danger)':'var(--warning)'};">${delai!==null?delai+' j':'-'}</strong></td><td>${status}</td></tr>`;});document.getElementById('alertes-table').innerHTML=html||'<tr><td colspan="7" style="text-align:center;color:var(--success);">Aucune alerte</td></tr>';}
function renderModernisationAlertes() {
const actifs = filteredData.filter(r => !isCloture(r) && isEnService(r) && isModernisation(r));
const byRegion = {};
ALL_REGIONS.forEach(reg => byRegion[reg] = []);
actifs.forEach(r => {
getRegionsForMarcheV2(r).forEach(reg => {
if (byRegion[reg]) byRegion[reg].push(r);
});
});
document.getElementById('modernisation-count').textContent = `${ALL_REGIONS.length} régions`;
let html = '';
ALL_REGIONS.forEach(reg => {
const color = CONFIG.REGION_COLORS[reg] || '#64748b';
const marches = byRegion[reg];
const pipeline = getPipelineForRegion(reg, 'Modernisation');
const pRow = pipeline[0];
// — Marché en vigueur —
let marcheRef = '-', avPhy = '-', avColor = '#64748b', isAlerte = false;
if (marches.length === 1) {
const m = marches[0], pct = calcPct(m.avt_phy, m.tot_marche);
marcheRef = `${escapeHtml(getMainReference(m))}<br><small>${escapeHtml(m.entrepreneur || '-')}</small>`;
avPhy = getProgressBar(pct, true);
isAlerte = pct >= CONFIG.SEUIL_MODERNISATION;
} else if (marches.length > 1) {
const maxPct = Math.max(...marches.map(m => calcPct(m.avt_phy, m.tot_marche)));
const refs = marches.map(m => escapeHtml(getMainReference(m))).join('<br>');
marcheRef = `<small>${refs}</small>`;
avPhy = getProgressBar(maxPct, true);
isAlerte = maxPct >= CONFIG.SEUIL_MODERNISATION;
}
// — Pipeline —
let pipelineEst = '-';
let pipelineStatut = '<span class="status-badge" style="background:rgba(239,68,68,0.15);color:#dc2626;"><i class="fas fa-exclamation-circle"></i> Non programmé</span>';
if (pRow) {
if (isModernisationDecision(pRow)) {
pipelineStatut = getPipelineStatutBadge(pRow.statut_dca);
} else {
pipelineEst = escapeHtml(pRow.estimation || '-');
pipelineStatut = getPipelineStatutBadge(pRow.statut_dca);
}
}
// — Style ligne —
const dot = `<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:8px;"></span>`;
const rowBg = isAlerte ? 'background:rgba(245,158,11,0.06);' : (!marches.length ? 'background:rgba(239,68,68,0.06);' : '');
html += `<tr style="${rowBg}">
<td>${dot}<strong>${escapeHtml(reg)}</strong></td>
<td>${marcheRef}</td>
<td>${avPhy}</td>
<td style="text-align:right;">${pipelineEst}</td>
<td>${pipelineStatut}</td>
</tr>`;
});
document.getElementById('modernisation-table').innerHTML = html;
}
function renderInfructueux() {
const inf = filteredData.filter(r => !isCloture(r) && isInfructueux(r));
document.getElementById('infructueux-count').textContent = `${inf.length} marchés`;
let html = '';
inf.forEach(r => {
const regions = getRegionsForMarcheV2(r);
const reg = regions.length > 1 ? regions.join(', ') : getRegion(r);
const obs = getObservation(r);
const pRow = findPipelineForInfructueux(r);
const relance = pRow
? getPipelineStatutBadge(pRow.statut_dca)
: '<span class="status-badge" style="background:rgba(100,116,139,0.15);color:#64748b;"><i class="fas fa-minus-circle"></i> -</span>';
html += `<tr class="infructueux-row">
<td><strong>${escapeHtml(reg)}</strong></td>
<td>${getReferenceWithCSC(r)}</td>
<td>${escapeHtml(r.projet || '-')}</td>
<td><span class="status-badge infructueux"><i class="fas fa-times-circle"></i> ${escapeHtml(obs)}</span></td>
<td>${relance}</td>
</tr>`;
});
document.getElementById('infructueux-table').innerHTML = html || '<tr><td colspan="5" style="text-align:center;color:var(--success);">Aucun marché infructueux</td></tr>';
}
function renderPipeline() {
document.getElementById('pipeline-count').textContent = `${pipelineData.length} projets`;
let html = '';
pipelineData.forEach(p => {
const isDec = isModernisationDecision(p);
const rowStyle = isDec ? 'background:rgba(37,99,235,0.05);' : '';
html += `<tr style="${rowStyle}">
<td><strong>${escapeHtml(p.projet)}</strong></td>
<td>${escapeHtml(p.regions.join(', '))}</td>
<td style="text-align:right;">${escapeHtml(p.estimation || '-')}</td>
<td>${escapeHtml(p.duree || '-')}</td>
<td>${getPipelineStatutBadge(p.statut_dca)}</td>
</tr>`;
});
document.getElementById('pipeline-table').innerHTML = html || '<tr><td colspan="5">Aucun projet pipeline</td></tr>';
}
function populateServiceFilters(){
const enService=filteredData.filter(r=>!isCloture(r)&&isEnService(r));
const projets=[...new Set(enService.map(r=>r.projet).filter(Boolean))].sort();
const pc=document.getElementById('filter-projet-chips');
pc.innerHTML=projets.map(p=>`<button type="button" class="filter-chip" data-type="projet" data-value="${escapeHtml(p)}"><span class="filter-chip-icon"><i class="far fa-circle"></i></span><span class="filter-chip-label">${escapeHtml(p)}</span></button>`).join('');
pc.querySelectorAll('.filter-chip').forEach(c=>c.addEventListener('click',()=>toggleChip(c)));
const regions=[...new Set(enService.flatMap(r=>{const rg=getRegionsForMarcheV2(r);return rg.length===ALL_REGIONS.length?[]:rg;}))].sort();
const rc=document.getElementById('filter-region-chips');
rc.innerHTML=regions.map(r=>`<button type="button" class="filter-chip" data-type="region" data-value="${escapeHtml(r)}"><span class="filter-chip-icon"><i class="far fa-circle"></i></span><span class="filter-chip-label">${escapeHtml(r)}</span></button>`).join('');
rc.querySelectorAll('.filter-chip').forEach(c=>c.addEventListener('click',()=>toggleChip(c)));
const entrepreneurs=[...new Set(enService.map(r=>r.entrepreneur).filter(Boolean))].sort();
[1,2,3].forEach(i=>{const sel=document.getElementById(`filter-entrepreneur-select-${i}`);const cv=sel.value;sel.innerHTML=`<option value="">Entrepreneur ${i}...</option>`+entrepreneurs.map(e=>`<option value="${escapeHtml(e)}"${e===cv?' selected':''}>${escapeHtml(e)}</option>`).join('');});
}
function toggleChip(chip){const icon=chip.querySelector('.filter-chip-icon i');const selected=chip.classList.toggle('selected');if(icon){if(selected){icon.classList.remove('far','fa-circle');icon.classList.add('fas','fa-dot-circle');}else{icon.classList.remove('fas','fa-dot-circle');icon.classList.add('far','fa-circle');}}applyServiceFilters();}
function getServiceFilteredData(){let data=filteredData.filter(r=>!isCloture(r)&&isEnService(r));const sp=Array.from(document.querySelectorAll('#filter-projet-chips .filter-chip.selected')).map(el=>el.dataset.value).filter(Boolean);if(sp.length)data=data.filter(r=>sp.includes(r.projet));const sr=Array.from(document.querySelectorAll('#filter-region-chips .filter-chip.selected')).map(el=>el.dataset.value).filter(Boolean);if(sr.length)data=data.filter(r=>getRegionsForMarcheV2(r).some(reg=>sr.includes(reg)));const se=[...new Set([1,2,3].map(i=>document.getElementById(`filter-entrepreneur-select-${i}`).value).filter(Boolean))];if(se.length)data=data.filter(r=>se.includes(r.entrepreneur));return data;}
function applyServiceFilters(){renderEnServiceFiltered(getServiceFilteredData());}
function resetServiceFilters(scope){let sel;if(scope==='projet')sel='#filter-projet-chips .filter-chip.selected';else if(scope==='region')sel='#filter-region-chips .filter-chip.selected';else sel='.slide#slide-2 .filter-chip.selected';document.querySelectorAll(sel).forEach(c=>{c.classList.remove('selected');const i=c.querySelector('.filter-chip-icon i');if(i){i.classList.remove('fas','fa-dot-circle');i.classList.add('far','fa-circle');}});applyServiceFilters();}
function resetEntrepreneurFilters(){[1,2,3].forEach(i=>document.getElementById(`filter-entrepreneur-select-${i}`).value='');applyServiceFilters();}
function renderEnService(){renderEnServiceFiltered(filteredData.filter(r=>!isCloture(r)&&isEnService(r)));populateServiceFilters();}
function renderEnServiceFiltered(enService){document.getElementById('service-count').textContent=`${enService.length} marchés`;let html='';enService.forEach(r=>{const isMod=isModernisation(r),regions=getRegionsForMarcheV2(r),isMulti=regions.length>1;html+=`<tr class="${isMulti?'multi-region-row':''}"><td>${getReferenceWithCSC(r)}${isMulti?'<span class="region-tag" style="margin-top:4px;display:inline-block;">'+regions.join(', ')+'</span>':''}</td><td>${escapeHtml(r.projet||'-')}</td><td>${escapeHtml(r.entrepreneur||'-')}</td><td style="text-align:right;">${formatMontant(r.tot_marche)}</td><td class="periode-tag">${formatPeriode(r.date_debut,r.date_fin)}</td><td>${getProgressBar(calcPct(r.avt_phy,r.tot_marche),isMod)}</td><td>${getProgressBar(calcPct(r.avt_fin,r.tot_marche),isMod)}</td></tr>`;});document.getElementById('service-table').innerHTML=html||'<tr><td colspan="7">Aucun marché</td></tr>';}
function renderEnCours(){const enCours=filteredData.filter(r=>!isCloture(r)&&!isEnService(r)&&!isInfructueux(r));document.getElementById('encours-count').textContent=`${enCours.length} marchés`;let html='';enCours.forEach(r=>{const regions=getRegionsForMarcheV2(r),isMulti=regions.length>1;html+=`<tr class="${isMulti?'multi-region-row':''}"><td>${getReferenceWithCSC(r)}${isMulti?'<span class="region-tag" style="margin-top:4px;display:inline-block;">'+regions.join(', ')+'</span>':''}</td><td>${escapeHtml(r.projet||'-')}</td><td>${escapeHtml(r.entrepreneur||'-')}</td><td>${escapeHtml(getObservation(r))}</td></tr>`;});document.getElementById('encours-table').innerHTML=html||'<tr><td colspan="4">Aucun marché</td></tr>';}
function renderRegions(){const byR=buildByRegion();let html='';const zs=byR['Zone Sud']||[];if(zs.length){html+=`<div class="region-card zone-sud-card"><div class="region-header"><span class="region-dot" style="background:var(--accent);"></span><strong>Zone Sud</strong><span class="status-badge multi-region" style="margin-left:auto;">Multi-régions</span></div><div class="region-stats"><div class="region-stat"><div class="value">${zs.length}</div><div class="label">Marchés</div></div><div class="region-stat"><div class="value">${zs.filter(r=>isEnService(r)).length}</div><div class="label">En Service</div></div></div><button class="details-btn" onclick="showRegionDetails('Zone Sud')"><i class="fas fa-search-plus"></i> Détails</button></div>`;}
ALL_REGIONS.sort().forEach(reg=>{const rows=byR[reg]||[];if(!rows.length)return;const counts=countMarchesForRegion(rows),budget=counts.own.reduce((s,r)=>s+parseNum(r.tot_marche),0),capex=rows.filter(r=>String(r.nature||'').toUpperCase()==='CAPEX').length,opex=rows.filter(r=>String(r.nature||'').toUpperCase()==='OPEX').length,enService=rows.filter(r=>isEnService(r)).length,avgPhy=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/rows.length),avgFin=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_fin,r.tot_marche),0)/rows.length),color=CONFIG.REGION_COLORS[reg];html+=`<div class="region-card"><div class="region-header"><span class="region-dot" style="background:${color};"></span><strong>${reg}</strong><span style="margin-left:auto;color:var(--text-muted);">${rows.length} marchés${getRegionTag(counts)}</span></div><div class="region-stats"><div class="region-stat"><div class="value">${capex}</div><div class="label">CAPEX</div></div><div class="region-stat"><div class="value">${opex}</div><div class="label">OPEX</div></div></div><div class="region-stats" style="margin-top:10px;"><div class="region-stat"><div class="value">${enService}</div><div class="label">En Service</div></div><div class="region-stat"><div class="value">${formatMontant(budget).replace(' DT','')}</div><div class="label">Budget (DT)</div></div></div><div style="margin-top:12px;"><div style="font-size:0.8em;color:var(--text-muted);">Av. Physique</div>${getProgressBar(avgPhy)}<div style="font-size:0.8em;color:var(--text-muted);margin-top:8px;">Av. Financier</div>${getProgressBar(avgFin)}</div><button class="details-btn" onclick="showRegionDetails('${reg}')"><i class="fas fa-search-plus"></i> Détails</button></div>`;});
document.getElementById('regions-grid').innerHTML=html||'<div>Aucune région</div>';
}
function showRegionDetails(region){const byR=buildByRegion();let actifs=region==='Zone Sud'?byR['Zone Sud']||[]:byR[region]||[];document.getElementById('modalRegionName').textContent=region;let html='<table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Av. Phy</th></tr></thead><tbody>';actifs.forEach(r=>{html+=`<tr><td>${getReferenceWithCSC(r)}</td><td>${escapeHtml(r.projet||'-')}</td><td>${escapeHtml(r.entrepreneur||'-')}</td><td>${getProgressBar(calcPct(r.avt_phy,r.tot_marche))}</td></tr>`;});html+='</tbody></table>';document.getElementById('modalBody').innerHTML=html;document.getElementById('regionModal').classList.add('active');}
function renderMapLegend(){const byR=buildByRegion();let html='';ALL_REGIONS.sort().forEach(reg=>{const rows=byR[reg]||[],counts=countMarchesForRegion(rows),budget=counts.own.reduce((s,r)=>s+parseNum(r.tot_marche),0);html+=`<div class="legend-item" onclick="showRegionDetails('${reg}')"><div class="legend-left"><span class="legend-dot" style="background:${CONFIG.REGION_COLORS[reg]};"></span><div><div class="legend-title">${reg}</div><div class="legend-sub">${rows.length} marchés</div></div></div><div class="legend-right"><div class="v">${formatMontant(budget)}</div></div></div>`;});document.getElementById('map-legend').innerHTML=html;}
function buildTooltipContent(reg,enService){const color=CONFIG.REGION_COLORS[reg]||'#64748b';let html=`<h4><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:${color};"></span>${reg} - ${enService.length} marchés en service</h4>`;if(!enService.length){html+='<div class="no-marche">Aucun marché en service</div>';return html;}const grouped={};enService.forEach(r=>{const p=r.projet||'Autres';if(!grouped[p])grouped[p]=[];grouped[p].push(r);});Object.keys(grouped).sort().forEach(projet=>{const marches=grouped[projet];html+=`<div class="projet-group"><div class="projet-title"><i class="fas fa-folder"></i> ${escapeHtml(projet)}<span class="count">${marches.length}</span></div>`;marches.forEach(r=>{const pct=calcPct(r.avt_phy,r.tot_marche),isMod=isModernisation(r),seuil=isMod?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD;let cc=pct>=CONFIG.SEUIL_CRITIQUE?'red':pct>=seuil?'orange':'green';html+=`<div class="marche-line"><span class="ref">${escapeHtml(getMainReference(r))}${r.csc?' - '+escapeHtml(r.csc):''}</span> | <span class="entrepreneur">${escapeHtml(r.entrepreneur||'-')}</span> | <span class="pct ${cc}">${pct}%</span></div>`;});html+='</div>';});return html;}
function initMapInteractions(){const tooltip=document.getElementById('regionTooltip'),wrapper=document.getElementById('mapWrapper');ALL_REGIONS.forEach(reg=>{const path=document.getElementById(`path-${reg}`);if(!path)return;path.replaceWith(path.cloneNode(true));const np=document.getElementById(`path-${reg}`);np.addEventListener('mouseenter',()=>{const byR=buildByRegion(),rows=byR[reg]||[],es=rows.filter(r=>isEnService(r));tooltip.innerHTML=buildTooltipContent(reg,es);tooltip.classList.add('visible');});np.addEventListener('mousemove',e=>{const rect=wrapper.getBoundingClientRect();let left=e.clientX-rect.left+15,top=e.clientY-rect.top-10;const tr=tooltip.getBoundingClientRect();if(left+tr.width>rect.width)left=e.clientX-rect.left-tr.width-15;if(top+tr.height>rect.height)top=rect.height-tr.height-10;if(top<0)top=10;tooltip.style.left=left+'px';tooltip.style.top=top+'px';});np.addEventListener('mouseleave',()=>tooltip.classList.remove('visible'));np.addEventListener('click',()=>showRegionDetails(reg));});}
function renderBulletCharts(){const byR=buildByRegion();let html='';ALL_REGIONS.sort().forEach(reg=>{const es=(byR[reg]||[]).filter(r=>isEnService(r));if(!es.length)return;const byP={};es.forEach(r=>{const p=r.projet||'Autres';if(!byP[p])byP[p]=[];byP[p].push(r);});html+=`<div class="bullet-region-group"><div class="bullet-region-title"><span class="region-dot" style="background:${CONFIG.REGION_COLORS[reg]};"></span>${reg}</div>`;Object.keys(byP).sort().forEach(projet=>{html+=`<div class="bullet-project-group"><div class="bullet-project-title"><i class="fas fa-folder"></i> ${escapeHtml(projet)}</div>`;byP[projet].forEach(r=>{const pct=calcPct(r.avt_phy,r.tot_marche),isMod=isModernisation(r),seuil=isMod?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD;let cc=pct>=CONFIG.SEUIL_CRITIQUE?'red':pct>=seuil?'orange':'green';const isM=r.isShared,mt=isM?`<span class="region-tag" style="font-size:0.7em;margin-left:5px;">${r.isZoneSud?'ZS':'Multi'}</span>`:'';html+=`<div class="bullet-item"><div class="ref-container"><div class="ref-main">${escapeHtml(getMainReference(r))}${mt}</div>${r.csc?`<div class="ref-csc">${escapeHtml(r.csc)}</div>`:''}</div><span class="entrepreneur">${escapeHtml(r.entrepreneur||'-')}</span><div class="bullet-chart"><div class="objectif-bar" style="width:100%;"></div><div class="avancement-bar ${cc}" style="width:${pct}%;"></div><div class="objectif-marker" style="left:calc(100% - 1.5px);"></div></div><span class="pct-value ${cc}">${pct}%</span></div>`;});html+='</div>';});html+='</div>';});document.getElementById('bulletChartsContent').innerHTML=html||'<p style="color:var(--text-muted);text-align:center;">Aucun marché en service</p>';}
// PROJECTION ENGINE
function computeProjection(r){const today=new Date(),dateDebut=r.date_debut?new Date(r.date_debut):null,dateFin=r.date_fin?new Date(r.date_fin):null,consomme=parseNum(r.avt_phy),tot=parseNum(r.tot_marche),mMin=parseNum(r.m_min),isMod=isModernisation(r);if(!dateDebut||isNaN(dateDebut.getTime())||consomme<=0)return{consomme,moisEcoules:0,tauxMois:0,projete:0,mMin,tot,verdict:'—',isMod};const moisEcoules=Math.max(0.5,(today-dateDebut)/MONTH_MS),tauxMois=consomme/moisEcoules,moisTotal=dateFin&&!isNaN(dateFin.getTime())?Math.max(1,(dateFin-dateDebut)/MONTH_MS):12,projete=tauxMois*moisTotal;let verdict='—';if(projete>0){if(mMin>0&&projete<mMin)verdict='Sous Min';else if(isMod||projete<=tot)verdict='Normal';else verdict='Dépassement';}return{consomme,moisEcoules:Math.round(moisEcoules*10)/10,tauxMois:Math.round(tauxMois),projete:Math.round(projete),mMin,tot,verdict,isMod};}
// PILOTAGE V2 UTILITIES
function formatKDT(val){const n=Math.abs(val);if(n>=1000)return(val<0?'':'+')+Math.round(n/1000)+' kDT';return(val<0?'':'+')+Math.round(n)+' DT';}
function formatKDTSigned(val){const n=Math.abs(val),prefix=val<0?'':'+';if(n>=1000000)return prefix+(n/1000000).toFixed(1)+' MDT';if(n>=1000)return prefix+Math.round(n/1000)+' kDT';return prefix+Math.round(n)+' DT';}
function formatMontantSigned(val){const n=Math.abs(Math.round(val)),prefix=val<0?'':'+';return prefix+n.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,' ')+' DT';}
function computeExhaustionDate(r){const p=computeProjection(r);if(p.tauxMois<=0||!r.date_debut)return null;const debut=new Date(r.date_debut);if(isNaN(debut.getTime()))return null;const tot=parseNum(r.tot_marche);if(tot<=0)return null;return new Date(debut.getTime()+(tot/p.tauxMois)*MONTH_MS);}
function getVerdictClass(v){if(v==='Normal')return'v-normal';if(v==='Sous Min')return'v-sous';if(v==='Dépassement')return'v-depasse';return'v-none';}
function getVerdictLabel(v){if(v==='Normal')return'Normal';if(v==='Sous Min')return'Sous Min';if(v==='Dépassement')return'Dépassement';return'Indéterminé';}
function truncateStr(s,m){if(!s)return'';return s.length>m?s.substring(0,m-1)+'…':s;}
// KPIs ENRICHIS
function renderEnrichedKPIs(projections){const normal=projections.filter(x=>x.proj.verdict==='Normal'),sous=projections.filter(x=>x.proj.verdict==='Sous Min'),depasse=projections.filter(x=>x.proj.verdict==='Dépassement'),none=projections.filter(x=>x.proj.verdict==='—');document.getElementById('proactif-kpi-normal').textContent=normal.length;document.getElementById('proactif-kpi-sous').textContent=sous.length;document.getElementById('proactif-kpi-depasse').textContent=depasse.length;document.getElementById('proactif-kpi-none').textContent=none.length;const exposSous=sous.reduce((s,x)=>s+(x.proj.projete-x.proj.mMin),0);document.getElementById('proactif-kpi-sous-impact').textContent=sous.length>0?`${formatKDTSigned(exposSous)} d'exposition`:'';const surpDep=depasse.reduce((s,x)=>s+(x.proj.projete-x.proj.tot),0);document.getElementById('proactif-kpi-depasse-impact').textContent=depasse.length>0?`${formatKDTSigned(surpDep)} de surplus`:'';const pctN=projections.length>0?Math.round((normal.length/projections.length)*100):0;document.getElementById('proactif-kpi-normal-impact').textContent=normal.length>0?`${pctN}% du portefeuille`:'';}
// SYNTHÈSE NARRATIVE
function renderNarrativeSynthesis(projections){const byReg={};projections.forEach(x=>{const reg=getRegion(x);if(!byReg[reg])byReg[reg]=[];byReg[reg].push(x);});let html='';Object.keys(byReg).sort().forEach(reg=>{const items=byReg[reg],sous=items.filter(x=>x.proj.verdict==='Sous Min').sort((a,b)=>(a.proj.projete-a.proj.mMin)-(b.proj.projete-b.proj.mMin)),dep=items.filter(x=>x.proj.verdict==='Dépassement').sort((a,b)=>(b.proj.projete-b.proj.tot)-(a.proj.projete-a.proj.tot)),none=items.filter(x=>x.proj.verdict==='—');let riskClass='risk-low';if(sous.length>=3||(sous.length>=1&&dep.length>=1))riskClass='risk-high';else if(sous.length>=1||dep.length>=1)riskClass='risk-moderate';const color=CONFIG.REGION_COLORS[reg]||'#64748b';html+=`<div class="narrative-region-block ${riskClass}"><div class="narrative-header"><span class="region-dot" style="background:${color}"></span><span>${escapeHtml(reg)} (${items.length} marchés)</span></div><div class="narrative-body">`;
if(sous.length>0){const totalExpo=sous.reduce((s,x)=>s+(x.proj.projete-x.proj.mMin),0);html+=`<div class="narr-intro">⚠ ${sous.length} marché(s) présentent un risque de non-atteinte du seuil minimum (exposition : <span class="narr-neg">${formatMontantSigned(totalExpo)}</span>).</div>`;sous.forEach(x=>{const ref=`${getMainReference(x)} ${x.csc||''}`.trim(),ecartMin=x.proj.projete-x.proj.mMin,ecartMax=x.proj.projete-x.proj.tot;html+=`<div class="narr-line"><span class="narr-ref">${escapeHtml(ref)}</span> (${escapeHtml(x.entrepreneur||'-')}, ${escapeHtml(x.projet||'-')}) : projeté ${formatMontant(x.proj.projete)} vs minimum ${formatMontant(x.proj.mMin)}, écart <span class="narr-neg">${formatMontantSigned(ecartMin)}</span> (Vs Max ${formatMontant(x.proj.tot)}, écart <span class="narr-neg">${formatMontantSigned(ecartMax)}</span>).</div>`;});}
if(dep.length>0){dep.forEach((x,idx)=>{const ref=`${getMainReference(x)} ${x.csc||''}`.trim(),surplus=x.proj.projete-x.proj.tot,intro=(idx===0&&sous.length>0)?'Par ailleurs, ':'';html+=`<div class="narr-line">${intro}<span class="narr-ref">${escapeHtml(ref)}</span> (${escapeHtml(x.entrepreneur||'-')}, ${escapeHtml(x.projet||'-')}) dépasse le montant max de <span class="narr-pos">${formatMontantSigned(surplus)}</span>.</div>`;});}
if(none.length>0){const byP={};none.forEach(x=>{const p=x.projet||'Autres';byP[p]=(byP[p]||0)+1;});const parts=Object.entries(byP).map(([p,c])=>`${c} marché(s) ${p}`);html+=`<div class="narr-line"><span class="narr-warn">${parts.join(', ')} sans consommation (données insuffisantes).</span></div>`;}
if(!sous.length&&!dep.length&&!none.length)html+='<div class="narr-line" style="color:#059669;font-weight:600;">✓ Tous les marchés projettent une consommation normale.</div>';
if(sous.length>0){const pire=sous[0],refP=getMainReference(pire),ecart=pire.proj.projete-pire.proj.mMin;html+=`<div style="margin-top:6px;padding-top:6px;border-top:1px dashed var(--border-color);font-size:0.85em;"><strong style="color:var(--accent);">Action prioritaire :</strong> accélérer <span class="narr-ref">${escapeHtml(refP)}</span> (écart <span class="narr-neg">${formatMontantSigned(ecart)}</span>).</div>`;}
html+='</div></div>';});document.getElementById('proactif-narrative-container').innerHTML=html||'<div style="text-align:center;color:var(--text-muted);padding:20px;">Aucune donnée</div>';}
// GANTT PAR RÉGION
function generateAxisLabels(minTs,maxTs){const labels=[],start=new Date(minTs),end=new Date(maxTs),totalMonths=(end.getFullYear()-start.getFullYear())*12+(end.getMonth()-start.getMonth()),step=Math.max(1,Math.ceil(totalMonths/6)),mN=['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'];let cur=new Date(start.getFullYear(),start.getMonth(),1);while(cur.getTime()<=maxTs){labels.push({label:`${mN[cur.getMonth()]} ${String(cur.getFullYear()).slice(-2)}`,pct:Math.max(0,Math.min(100,((cur.getTime()-minTs)/(maxTs-minTs))*100))});cur.setMonth(cur.getMonth()+step);}return labels;}
function renderGanttCharts(projections){const byReg={};projections.forEach(x=>{const reg=getRegion(x);if(!byReg[reg])byReg[reg]=[];byReg[reg].push(x);});const today=new Date();let html='';Object.keys(byReg).sort().forEach(reg=>{const items=byReg[reg],color=CONFIG.REGION_COLORS[reg]||'#64748b';let minDate=Infinity,maxDate=-Infinity;items.forEach(x=>{const d0=x.date_debut?new Date(x.date_debut).getTime():NaN,d1=x.date_fin?new Date(x.date_fin).getTime():NaN;if(!isNaN(d0))minDate=Math.min(minDate,d0);if(!isNaN(d1))maxDate=Math.max(maxDate,d1);});if(!isFinite(minDate)||!isFinite(maxDate))return;const timeRange=maxDate-minDate;if(timeRange<=0)return;
const sortO={'Sous Min':0,'Dépassement':1,'—':2,'Normal':3};items.sort((a,b)=>{const oa=sortO[a.proj.verdict]??9,ob=sortO[b.proj.verdict]??9;if(oa!==ob)return oa-ob;if(a.proj.verdict==='Sous Min')return(a.proj.projete-a.proj.mMin)-(b.proj.projete-b.proj.mMin);if(a.proj.verdict==='Dépassement')return(b.proj.projete-b.proj.tot)-(a.proj.projete-a.proj.tot);return 0;});
const nbS=items.filter(x=>x.proj.verdict==='Sous Min').length,nbD=items.filter(x=>x.proj.verdict==='Dépassement').length,nbN=items.filter(x=>x.proj.verdict==='Normal').length,nbI=items.filter(x=>x.proj.verdict==='—').length;const sumP=[];if(nbS)sumP.push(`<span style="color:#DC2626">${nbS} sous min</span>`);if(nbD)sumP.push(`<span style="color:#D97706">${nbD} dép.</span>`);if(nbN)sumP.push(`<span style="color:#059669">${nbN} normal</span>`);if(nbI)sumP.push(`<span style="color:#64748B">${nbI} indét.</span>`);
const todayPct=Math.max(0,Math.min(100,((today.getTime()-minDate)/timeRange)*100));const axisLabels=generateAxisLabels(minDate,maxDate);
html+=`<div class="gantt-region-group"><div class="gantt-region-header"><span class="region-dot" style="background:${color}"></span>${escapeHtml(reg)} (${items.length} marchés)<span class="gantt-summary">${sumP.join(' · ')}</span></div>`;
html+=`<div style="display:flex;align-items:center;padding:2px 0 6px;border-bottom:1px solid var(--border-color);"><div style="width:245px;min-width:245px;font-size:0.68em;color:var(--text-muted);text-align:right;padding-right:12px;">Période →</div><div style="flex:1;position:relative;height:18px;font-size:0.66em;color:var(--text-muted);">`;
axisLabels.forEach(a=>{html+=`<span style="position:absolute;left:${a.pct}%;transform:translateX(-50%);white-space:nowrap;">${a.label}</span>`;});
html+=`<div style="position:absolute;left:${todayPct}%;top:14px;width:0;height:6px;border-left:2px solid #7C3AED;"></div></div><div style="width:175px;min-width:175px;font-size:0.68em;color:var(--text-muted);padding-left:10px;">Verdict</div></div>`;
items.forEach(x=>{const d0=x.date_debut?new Date(x.date_debut).getTime():NaN,d1=x.date_fin?new Date(x.date_fin).getTime():NaN;if(isNaN(d0)||isNaN(d1))return;const barLeft=((d0-minDate)/timeRange)*100,barWidth=Math.max(1,((d1-d0)/timeRange)*100),elapsed=Math.max(0,Math.min(1,(today.getTime()-d0)/(d1-d0))),elapsedPct=elapsed*100,vClass=getVerdictClass(x.proj.verdict);
const exhaustDate=computeExhaustionDate(x);let exhaustPct=null;if(exhaustDate){const et=exhaustDate.getTime();if(et>d0)exhaustPct=Math.min(120,((et-d0)/(d1-d0))*100);}
const refTxt=truncateStr(`${getMainReference(x)}${x.csc?' '+x.csc:''}`,35),entrTxt=truncateStr(x.entrepreneur||'-',22);
let ecartTxt='';if(x.proj.verdict==='Sous Min')ecartTxt=formatKDTSigned(x.proj.projete-x.proj.mMin);else if(x.proj.verdict==='Dépassement')ecartTxt=formatKDTSigned(x.proj.projete-x.proj.tot);else if(x.proj.verdict==='Normal'&&x.proj.projete>0)ecartTxt=formatMontant(x.proj.projete);
const ttData=JSON.stringify({ref:getMainReference(x)+(x.csc?' '+x.csc:''),entr:x.entrepreneur||'-',proj:x.projet||'-',projete:x.proj.projete,tot:x.proj.tot,mMin:x.proj.mMin,cons:x.proj.consomme,verdict:x.proj.verdict,pctCons:x.proj.tot>0?Math.round((x.proj.consomme/x.proj.tot)*100):0}).replace(/"/g,'&quot;');
html+=`<div class="gantt-row" data-gantt-tt="${ttData}"><div class="gantt-label-left"><div class="g-ref" title="${escapeHtml(getMainReference(x)+(x.csc?' '+x.csc:''))}">${escapeHtml(refTxt)}</div><div class="g-entr">${escapeHtml(entrTxt)}</div></div><div class="gantt-bar-area"><div class="gantt-bar-track" style="left:${barLeft.toFixed(2)}%;width:${barWidth.toFixed(2)}%;"><div class="gantt-bar-elapsed ${vClass}" style="width:${Math.min(100,elapsedPct).toFixed(1)}%"></div>`;
if(elapsedPct<100)html+=`<div class="gantt-bar-remaining" style="left:${elapsedPct.toFixed(1)}%;width:${(100-elapsedPct).toFixed(1)}%"></div>`;
if(elapsed>0.01&&elapsed<0.99)html+=`<div class="gantt-today-marker" style="left:${elapsedPct.toFixed(1)}%"></div>`;
if(exhaustPct!==null&&exhaustPct>5)html+=`<div class="gantt-exhaustion-marker" style="left:${exhaustPct.toFixed(1)}%" title="Épuisement budget estimé"></div>`;
html+=`</div></div><div class="gantt-label-right"><span class="g-verdict ${vClass}">${getVerdictLabel(x.proj.verdict)}</span>${ecartTxt?`<span class="g-amount">${ecartTxt}</span>`:''}</div></div>`;});
html+='</div>';});document.getElementById('proactif-gantt-container').innerHTML=html||'<div style="text-align:center;color:var(--text-muted);padding:20px;">Aucun marché en service</div>';setTimeout(()=>initGanttTooltips(),200);}
function initGanttTooltips(){const tt=document.getElementById('ganttTooltip');if(!tt)return;document.querySelectorAll('.gantt-row[data-gantt-tt]').forEach(row=>{row.addEventListener('mouseenter',()=>{try{const d=JSON.parse(row.dataset.ganttTt),ecartMax=d.projete-d.tot,ecartMin=d.mMin>0?d.projete-d.mMin:null;let h=`<h5>${escapeHtml(d.ref)}</h5><div class="ttr"><span class="ttl">Entrepreneur</span><span class="ttv">${escapeHtml(d.entr)}</span></div><div class="ttr"><span class="ttl">Projet</span><span class="ttv">${escapeHtml(d.proj)}</span></div><div style="border-top:1px solid var(--border-color);margin:6px 0;"></div><div class="ttr"><span class="ttl">Montant projeté</span><span class="ttv">${formatMontant(d.projete)}</span></div><div class="ttr"><span class="ttl">Montant max</span><span class="ttv">${formatMontant(d.tot)}</span></div>`;if(d.mMin>0)h+=`<div class="ttr"><span class="ttl">Montant min</span><span class="ttv">${formatMontant(d.mMin)}</span></div>`;h+=`<div style="border-top:1px solid var(--border-color);margin:6px 0;"></div><div class="ttr"><span class="ttl">Écart vs Max</span><span class="ttv ${ecartMax>=0?'pos':'neg'}">${formatMontantSigned(ecartMax)}</span></div>`;if(ecartMin!==null)h+=`<div class="ttr"><span class="ttl">Écart vs Min</span><span class="ttv ${ecartMin>=0?'pos':'neg'}">${formatMontantSigned(ecartMin)}</span></div>`;const vc=d.verdict==='Normal'?'#059669':d.verdict==='Sous Min'?'#DC2626':d.verdict==='Dépassement'?'#D97706':'#64748B';h+=`<div class="ttr"><span class="ttl">% consommé</span><span class="ttv">${d.pctCons}%</span></div><div style="border-top:1px solid var(--border-color);margin:6px 0;"></div><div class="ttr"><span class="ttl">Verdict</span><span class="ttv" style="color:${vc}">${d.verdict}</span></div>`;tt.innerHTML=h;tt.classList.add('visible');}catch(e){}});row.addEventListener('mousemove',e=>{tt.style.left=(e.clientX+15)+'px';tt.style.top=(e.clientY-10)+'px';requestAnimationFrame(()=>{const r=tt.getBoundingClientRect();if(r.right>window.innerWidth-5)tt.style.left=(e.clientX-r.width-15)+'px';if(r.bottom>window.innerHeight-5)tt.style.top=(window.innerHeight-r.height-10)+'px';});});row.addEventListener('mouseleave',()=>tt.classList.remove('visible'));});}
// ACTIONS PRIORITAIRES
function renderPriorityActions(projections){const actionable=projections.filter(x=>x.proj.verdict==='Sous Min'||x.proj.verdict==='Dépassement').map(x=>{const isSous=x.proj.verdict==='Sous Min',ecart=isSous?x.proj.projete-x.proj.mMin:x.proj.projete-x.proj.tot;return{...x,ecart,absEcart:Math.abs(ecart),action:isSous?'Accélérer consommation':'Ralentir / planifier transition'};}).sort((a,b)=>b.absEcart-a.absEcart);const container=document.getElementById('proactif-actions-container');if(!actionable.length){container.innerHTML='<div style="text-align:center;color:var(--success);padding:20px;"><i class="fas fa-check-circle"></i> Aucune action prioritaire requise</div>';return;}let html='<div class="table-container"><div class="table-header" style="background:linear-gradient(90deg,#DC2626,#EF4444);"><h3><i class="fas fa-bullseye"></i> Actions par Impact Financier</h3><span class="badge">'+actionable.length+' actions</span></div><div class="table-wrapper"><table><thead><tr><th>#</th><th>Localité</th><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Verdict</th><th>Écart (DT)</th><th>Action</th></tr></thead><tbody>';actionable.forEach((x,idx)=>{const isSous=x.proj.verdict==='Sous Min',rkC=isSous?'rk-danger':'rk-warning',vB=isSous?'<span class="status-badge verdict-sous"><i class="fas fa-arrow-down"></i> Sous Min</span>':'<span class="status-badge verdict-depasse"><i class="fas fa-arrow-up"></i> Dépassement</span>',ecC=x.ecart<0?'#DC2626':'#D97706';html+=`<tr><td><span class="priority-rank ${rkC}">${idx+1}</span></td><td><strong>${escapeHtml(x.csc||getRegion(x))}</strong></td><td><strong>${escapeHtml(getMainReference(x))}</strong></td><td>${escapeHtml(x.projet||'-')}</td><td>${escapeHtml(x.entrepreneur||'-')}</td><td>${vB}</td><td style="text-align:right;font-weight:700;color:${ecC};">${formatMontantSigned(x.ecart)}</td><td><span class="action-recommandee">${escapeHtml(x.action)}</span></td></tr>`;});html+='</tbody></table></div></div>';container.innerHTML=html;}
// MATRICE DE RISQUE
function renderMatriceRisque(enService){const byReg={};enService.forEach(r=>{const reg=getRegion(r);if(!byReg[reg])byReg[reg]=[];byReg[reg].push(r);});let html='';ALL_REGIONS.sort().forEach(reg=>{const rows=byReg[reg]||[];if(!rows.length)return;const color=CONFIG.REGION_COLORS[reg]||'#64748b',budget=rows.reduce((s,r)=>s+parseNum(r.tot_marche),0),avgPhy=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/rows.length),avgFin=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_fin,r.tot_marche),0)/rows.length),ecart=avgPhy-avgFin,projs=rows.map(r=>computeProjection(r)),nbS=projs.filter(p=>p.verdict==='Sous Min').length,nbD=projs.filter(p=>p.verdict==='Dépassement').length,nbO=projs.filter(p=>p.verdict==='Normal').length;const projAll=[nbS>0?`<span style="color:#DC2626;">${nbS} sous</span>`:'',nbD>0?`<span style="color:#D97706;">${nbD} dép.</span>`:'',nbO>0?`<span style="color:#059669;">${nbO} ok</span>`:''].filter(Boolean).join(' · ')||'-';const tendance=avgPhy>30?'↗':avgPhy>=15?'→':'↘',tendC=tendance==='↗'?'#059669':tendance==='→'?'#D97706':'#DC2626';let risque,rC;if(avgPhy>=30){risque='Faible';rC='risque-faible';}else if(avgPhy<=22){risque='Élevé';rC='risque-eleve';}else{risque='Modéré';rC='risque-modere';}const ecartAbs=Math.abs(ecart),ecC=ecartAbs>=8?'#DC2626':ecartAbs>=4?'#D97706':'#059669';html+=`<tr><td><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:8px;"></span><strong>${reg}</strong></td><td>${rows.length}</td><td style="text-align:right;">${formatMontant(budget)}</td><td>${getProgressBar(avgPhy)}</td><td>${getProgressBar(avgFin)}</td><td style="text-align:center;font-weight:700;color:${ecC};">${ecart>0?'+':''}${ecart} pts</td><td>${projAll}</td><td style="text-align:center;font-size:1.3em;color:${tendC};">${tendance}</td><td><span class="status-badge ${rC}">${risque}</span></td></tr>`;});document.getElementById('proactif-risque-table').innerHTML=html||'<tr><td colspan="9" style="text-align:center;">Aucune donnée</td></tr>';}
// PILOTAGE PROACTIF — FILTRES
function populateProactifFilters(){const enService=filteredData.filter(r=>!isCloture(r)&&isEnService(r));const projets=[...new Set(enService.map(r=>r.projet).filter(Boolean))].sort();const pc=document.getElementById('proactif-filter-projet-chips');pc.innerHTML=projets.map(p=>`<button type="button" class="filter-chip proactif-theme" data-type="proactif-projet" data-value="${escapeHtml(p)}"><span class="filter-chip-icon"><i class="far fa-circle"></i></span><span class="filter-chip-label">${escapeHtml(p)}</span></button>`).join('');pc.querySelectorAll('.filter-chip').forEach(c=>c.addEventListener('click',()=>toggleProactifChip(c)));
const regions=[...new Set(enService.flatMap(r=>{const rg=getRegionsForMarcheV2(r);return rg.length===ALL_REGIONS.length?[]:rg;}))].sort();const rc=document.getElementById('proactif-filter-region-chips');rc.innerHTML=regions.map(r=>`<button type="button" class="filter-chip proactif-theme" data-type="proactif-region" data-value="${escapeHtml(r)}"><span class="filter-chip-icon"><i class="far fa-circle"></i></span><span class="filter-chip-label">${escapeHtml(r)}</span></button>`).join('');rc.querySelectorAll('.filter-chip').forEach(c=>c.addEventListener('click',()=>toggleProactifChip(c)));
const entrepreneurs=[...new Set(enService.map(r=>r.entrepreneur).filter(Boolean))].sort();[1,2,3].forEach(i=>{const sel=document.getElementById(`proactif-filter-entrepreneur-select-${i}`);const cv=sel.value;sel.innerHTML=`<option value="">Entrepreneur ${i}...</option>`+entrepreneurs.map(e=>`<option value="${escapeHtml(e)}"${e===cv?' selected':''}>${escapeHtml(e)}</option>`).join('');});}
function toggleProactifChip(chip){const icon=chip.querySelector('.filter-chip-icon i');const selected=chip.classList.toggle('selected');if(icon){if(selected){icon.classList.remove('far','fa-circle');icon.classList.add('fas','fa-dot-circle');}else{icon.classList.remove('fas','fa-dot-circle');icon.classList.add('far','fa-circle');}}applyProactifFilters();}
function resetProactifFilters(scope){const sel=scope==='projet'?'#proactif-filter-projet-chips .filter-chip.selected':'#proactif-filter-region-chips .filter-chip.selected';document.querySelectorAll(sel).forEach(c=>{c.classList.remove('selected');const i=c.querySelector('.filter-chip-icon i');if(i){i.classList.remove('fas','fa-dot-circle');i.classList.add('far','fa-circle');}});applyProactifFilters();}
function resetProactifEntrepreneurFilters(){[1,2,3].forEach(i=>document.getElementById(`proactif-filter-entrepreneur-select-${i}`).value='');applyProactifFilters();}
function getProactifFilteredData(){let data=filteredData.filter(r=>!isCloture(r)&&isEnService(r));const sp=Array.from(document.querySelectorAll('#proactif-filter-projet-chips .filter-chip.selected')).map(el=>el.dataset.value).filter(Boolean);if(sp.length)data=data.filter(r=>sp.includes(r.projet));const sr=Array.from(document.querySelectorAll('#proactif-filter-region-chips .filter-chip.selected')).map(el=>el.dataset.value).filter(Boolean);if(sr.length)data=data.filter(r=>getRegionsForMarcheV2(r).some(reg=>sr.includes(reg)));const se=[...new Set([1,2,3].map(i=>document.getElementById(`proactif-filter-entrepreneur-select-${i}`).value).filter(Boolean))];if(se.length)data=data.filter(r=>se.includes(r.entrepreneur));return data;}
function applyProactifFilters(){renderPilotageProactifFiltered(getProactifFilteredData());}
function renderPilotageProactif(){const enService=filteredData.filter(r=>!isCloture(r)&&isEnService(r));renderPilotageProactifFiltered(enService);populateProactifFilters();}
function renderPilotageProactifFiltered(enService){const projections=enService.map(r=>({...r,proj:computeProjection(r)}));renderEnrichedKPIs(projections);renderNarrativeSynthesis(projections);renderGanttCharts(projections);renderPriorityActions(projections);renderMatriceRisque(enService);}
// ACTIVE FILTERS LABEL
function getActiveFiltersLabel(slideIndex){const parts=[];
if(slideIndex===2){const sp=Array.from(document.querySelectorAll('#filter-projet-chips .filter-chip.selected')).map(el=>el.dataset.value);const sr=Array.from(document.querySelectorAll('#filter-region-chips .filter-chip.selected')).map(el=>el.dataset.value);const se=[...new Set([1,2,3].map(i=>document.getElementById(`filter-entrepreneur-select-${i}`).value).filter(Boolean))];if(sp.length)parts.push(sp.join(', '));if(sr.length)parts.push(sr.join(', '));if(se.length)parts.push(se.join(', '));}
else if(slideIndex===3){const sp=Array.from(document.querySelectorAll('#proactif-filter-projet-chips .filter-chip.selected')).map(el=>el.dataset.value);const sr=Array.from(document.querySelectorAll('#proactif-filter-region-chips .filter-chip.selected')).map(el=>el.dataset.value);const se=[...new Set([1,2,3].map(i=>document.getElementById(`proactif-filter-entrepreneur-select-${i}`).value).filter(Boolean))];if(sp.length)parts.push(sp.join(', '));if(sr.length)parts.push(sr.join(', '));if(se.length)parts.push(se.join(', '));}
return parts.length?parts.join(' | '):'Tous les marchés';}
// EXPORT PDF
function exportPDF(){const slideTitle=document.getElementById(`slide-${currentSlide}`)?.dataset.title||'Rapport';const filterLabel=getActiveFiltersLabel(currentSlide);const fullTitle=filterLabel!=='Tous les marchés'?`${slideTitle}${filterLabel}`:slideTitle;showPDFPreview({slideIndex:currentSlide,title:fullTitle});}
function showPDFPreview(pdfData){document.getElementById('pdfPreviewBody').innerHTML=generatePDFPreviewHTML(pdfData);document.getElementById('pdfPreviewModal').classList.add('active');window.currentPDFData=pdfData;}
function generatePDFPreviewHTML(pdfData){const{slideIndex,title}=pdfData;let contentHTML='';
switch(slideIndex){case 0:contentHTML=generatePDFCartographie();break;case 1:contentHTML=generatePDFAlertes();break;case 2:contentHTML=generatePDFEnService();break;case 3:contentHTML=generatePDFPilotageProactif();break;case 4:contentHTML=generatePDFParRegion();break;case 5:contentHTML=generatePDFEnCours();break;default:contentHTML='<p>Contenu non disponible</p>';}
return`<div class="pdf-page-preview"><div class="pdf-header"><div><strong style="color:#002855;font-size:1.2em;">TUNISIE TELECOM</strong><br><span style="color:#6b7280;font-size:0.9em;">Direction Centrale Zone Sud</span></div><div class="pdf-title"><h1>${title}</h1><p>${new Date().toLocaleDateString('fr-FR')}</p></div></div>${contentHTML}<div class="pdf-footer"><strong>Division Achats - Direction Centrale Zone Sud</strong></div></div>`;}
// PDF CARTOGRAPHIE (enrichi)
function generatePDFCartographie(){const byR=buildByRegion(),actifs=filteredData.filter(r=>!isCloture(r)),enService=actifs.filter(r=>isEnService(r)),capex=actifs.filter(r=>String(r.nature||'').toUpperCase()==='CAPEX'),opex=actifs.filter(r=>String(r.nature||'').toUpperCase()==='OPEX'),budgetTotal=actifs.reduce((s,r)=>s+parseNum(r.tot_marche),0),alertes=enService.filter(r=>{const p=calcPct(r.avt_phy,r.tot_marche);return p>=(isModernisation(r)?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD);});
let html=`<div class="pdf-summary"><strong>Résumé exécutif :</strong> La Zone Sud gère actuellement <strong>${actifs.length} marchés actifs</strong> pour un budget total de <strong>${formatMontant(budgetTotal)}</strong>, répartis entre <strong>${capex.length} CAPEX</strong> (${formatMontant(capex.reduce((s,r)=>s+parseNum(r.tot_marche),0))}) et <strong>${opex.length} OPEX</strong> (${formatMontant(opex.reduce((s,r)=>s+parseNum(r.tot_marche),0))}). <strong>${enService.length} marchés</strong> sont en service dont <strong style="color:#dc2626;">${alertes.length} en alerte</strong> de consommation.</div>`;
html+='<div class="pdf-section"><div class="pdf-section-title">Répartition par Région</div><table><thead><tr><th>Région</th><th>Marchés</th><th>En Service</th><th>CAPEX</th><th>OPEX</th><th>Budget</th><th>Av. Phy Moy.</th><th>Av. Fin Moy.</th></tr></thead><tbody>';
ALL_REGIONS.forEach(reg=>{const rows=byR[reg]||[];if(!rows.length)return;const es=rows.filter(r=>isEnService(r)).length,cx=rows.filter(r=>String(r.nature||'').toUpperCase()==='CAPEX').length,ox=rows.filter(r=>String(r.nature||'').toUpperCase()==='OPEX').length,budget=rows.filter(x=>!x.isShared).reduce((s,r)=>s+parseNum(r.tot_marche),0),avgP=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/rows.length),avgF=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_fin,r.tot_marche),0)/rows.length);const pC=avgP>=70?'#dc2626':avgP>=50?'#f59e0b':'#059669';html+=`<tr><td><strong>${reg}</strong></td><td style="text-align:center;">${rows.length}</td><td style="text-align:center;">${es}</td><td style="text-align:center;">${cx}</td><td style="text-align:center;">${ox}</td><td style="text-align:right;">${formatMontant(budget)}</td><td style="text-align:center;color:${pC};font-weight:700;">${avgP}%</td><td style="text-align:center;">${avgF}%</td></tr>`;});
html+='</tbody></table></div>';
html+='<div class="pdf-note"><strong>Légende :</strong> Av. Phy en <span style="color:#059669;">vert</span> (&lt;50%), <span style="color:#f59e0b;">orange</span> (50-69%), <span style="color:#dc2626;">rouge</span> (≥70%). Les marchés Zone Sud et multi-régions sont comptabilisés dans chaque région concernée.</div>';
return html;}
// PDF ALERTES (enrichi)
function generatePDFAlertes(){const enService=filteredData.filter(r=>!isCloture(r)&&isEnService(r)),alertes=enService.filter(r=>{const p=calcPct(r.avt_phy,r.tot_marche);return p>=(isModernisation(r)?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD);}).sort((a,b)=>calcPct(b.avt_phy,b.tot_marche)-calcPct(a.avt_phy,a.tot_marche)),regionsSM=getRegionsSansModernisation(),infructueux=filteredData.filter(r=>!isCloture(r)&&isInfructueux(r)),critiques=alertes.filter(r=>calcPct(r.avt_phy,r.tot_marche)>=CONFIG.SEUIL_CRITIQUE),attention=alertes.filter(r=>calcPct(r.avt_phy,r.tot_marche)<CONFIG.SEUIL_CRITIQUE),budgetExpose=alertes.reduce((s,r)=>s+parseNum(r.tot_marche),0);
let html=`<div class="pdf-summary"><strong>Résumé exécutif :</strong> <strong style="color:#dc2626;">${alertes.length} marchés en alerte</strong> de consommation identifiés, dont <strong style="color:#dc2626;">${critiques.length} critiques</strong> (≥${CONFIG.SEUIL_CRITIQUE}%) et <strong style="color:#f59e0b;">${attention.length} en attention</strong>. Budget exposé : <strong>${formatMontant(budgetExpose)}</strong>. ${regionsSM.length} région(s) sans marché Modernisation. ${infructueux.length} marché(s) infructueux à relancer.</div>`;
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#dc2626;">Alertes Consommation (${alertes.length})</div><table><thead><tr><th>Référence</th><th>Entrepreneur</th><th>Projet</th><th>Région</th><th>Avancement</th><th>Délai</th><th>Statut</th></tr></thead><tbody>`;
alertes.forEach(r=>{const pct=calcPct(r.avt_phy,r.tot_marche),delai=getDelaiRestant(r),isCrit=pct>=CONFIG.SEUIL_CRITIQUE;html+=`<tr><td>${getMainReference(r)}<br><small>${r.csc||''}</small></td><td>${r.entrepreneur||'-'}</td><td>${r.projet||'-'}</td><td>${getRegion(r)}</td><td style="text-align:center;color:${isCrit?'#dc2626':'#f59e0b'};font-weight:700;">${pct}%</td><td style="text-align:center;font-weight:700;color:${delai!==null&&delai<=CONFIG.DELAI_CRITIQUE?'#dc2626':'#f59e0b'};">${delai!==null?delai+'j':'-'}</td><td style="font-weight:700;color:${isCrit?'#dc2626':'#f59e0b'};">${isCrit?'Critique':'Attention'}</td></tr>`;});
html+='</tbody></table></div>';
// SUIVI MODERNISATION PAR RÉGION (toujours affiché)
const modActifsP=filteredData.filter(r=>!isCloture(r)&&isEnService(r)&&isModernisation(r));
const modByRegP={};ALL_REGIONS.forEach(rg=>modByRegP[rg]=[]);
modActifsP.forEach(r=>{getRegionsForMarcheV2(r).forEach(rg=>{if(modByRegP[rg])modByRegP[rg].push(r);});});
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#7c3aed;">Suivi Modernisation par Région (${ALL_REGIONS.length})</div><table><thead><tr><th>Région</th><th>Marché en vigueur</th><th>Av. Phy</th><th>Estimation</th><th>Statut Pipeline</th></tr></thead><tbody>`;
ALL_REGIONS.forEach(rg=>{
const mm=modByRegP[rg],pipe=getPipelineForRegion(rg,'Modernisation'),pR=pipe[0];
let mRef='-',avP='-',pC='#64748b';
if(mm.length===1){const m=mm[0],pct=calcPct(m.avt_phy,m.tot_marche);mRef=`${getMainReference(m)}<br><small>${m.entrepreneur||'-'}</small>`;avP=`${pct}%`;pC=pct>=90?'#dc2626':pct>=50?'#f59e0b':'#059669';}
else if(mm.length>1){const mx=Math.max(...mm.map(m=>calcPct(m.avt_phy,m.tot_marche)));mRef=`${mm.length} lots`;avP=`${mx}%`;pC=mx>=90?'#dc2626':mx>=50?'#f59e0b':'#059669';}
let pEst='-',pStat='-';
if(pR){if(isModernisationDecision(pR)){pStat=`<em style="color:#2563eb;">${pR.statut_dca}</em>`;}else{pEst=pR.estimation||'-';pStat=`<strong style="color:#059669;">${pR.statut_dca}</strong>`;}}
else{pStat='<span style="color:#dc2626;">Non programmé</span>';}
html+=`<tr><td><strong>${rg}</strong></td><td>${mRef}</td><td style="text-align:center;color:${pC};font-weight:700;">${avP}</td><td style="text-align:right;">${pEst}</td><td>${pStat}</td></tr>`;
});
html+='</tbody></table></div>';
if(infructueux.length){
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#dc2626;">Alerte Lancement (${infructueux.length})</div><table><thead><tr><th>Région</th><th>Référence</th><th>Projet</th><th>Observation</th><th>Relance</th></tr></thead><tbody>`;
infructueux.forEach(r=>{
const pRow=findPipelineForInfructueux(r);
const relance=pRow?`<strong style="color:#059669;">${pRow.statut_dca}</strong>`:'<span style="color:#64748b;">-</span>';
html+=`<tr><td>${getRegion(r)}</td><td>${getMainReference(r)}</td><td>${r.projet||'-'}</td><td style="color:#dc2626;font-weight:700;">${getObservation(r)}</td><td>${relance}</td></tr>`;
});
html+='</tbody></table></div>';
}
html+='<div class="pdf-note"><strong>Seuils :</strong> Standard ≥70%, Modernisation ≥50%, Critique ≥90%. Délai critique ≤45 jours.</div>';
// PIPELINE DE LANCEMENT
if(pipelineData.length){
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#4F46E5;">Pipeline de Lancement (${pipelineData.length} projets)</div><table><thead><tr><th>Projet</th><th>Régions</th><th>Estimation</th><th>Durée</th><th>Statut DCA</th></tr></thead><tbody>`;
pipelineData.forEach(p=>{
const isDec=isModernisationDecision(p);
const sC=isDec?'#2563eb':'#059669';
html+=`<tr${isDec?' style="background:#eff6ff;"':''}><td><strong>${p.projet}</strong></td><td>${p.regions.join(', ')}</td><td style="text-align:right;">${p.estimation||'-'}</td><td>${p.duree||'-'}</td><td style="color:${sC};font-weight:700;">${p.statut_dca||'-'}</td></tr>`;
});
html+='</tbody></table></div>';
}
return html;}
// PDF EN SERVICE (enrichi)
function generatePDFEnService(){const enService=getServiceFilteredData(),budgetT=enService.reduce((s,r)=>s+parseNum(r.tot_marche),0),avgP=enService.length?Math.round(enService.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/enService.length):0,avgF=enService.length?Math.round(enService.reduce((s,r)=>s+calcPct(r.avt_fin,r.tot_marche),0)/enService.length):0,ecart=avgP-avgF;
let html=`<div class="pdf-summary"><strong>Résumé exécutif :</strong> <strong>${enService.length} marchés en service</strong> pour un budget total de <strong>${formatMontant(budgetT)}</strong>. Avancement physique moyen : <strong>${avgP}%</strong>, financier moyen : <strong>${avgF}%</strong>. Écart Phy-Fin : <strong style="color:${Math.abs(ecart)>=8?'#dc2626':Math.abs(ecart)>=4?'#f59e0b':'#059669'};">${ecart>0?'+':''}${ecart} pts</strong>.</div>`;
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#047857;">Marchés En Service (${enService.length})</div><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Montant Max</th><th>Période</th><th>Av. Phy</th><th>Av. Fin</th></tr></thead><tbody>`;
enService.forEach(r=>{const pP=calcPct(r.avt_phy,r.tot_marche),pF=calcPct(r.avt_fin,r.tot_marche),isMod=isModernisation(r),seuil=isMod?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD,cP=pP>=CONFIG.SEUIL_CRITIQUE?'#dc2626':pP>=seuil?'#f59e0b':'#059669';html+=`<tr><td>${getMainReference(r)}<br><small>${r.csc||''}</small></td><td>${r.projet||'-'}</td><td>${r.entrepreneur||'-'}</td><td style="text-align:right;">${formatMontant(r.tot_marche)}</td><td style="font-size:0.85em;">${formatPeriode(r.date_debut,r.date_fin)}</td><td style="text-align:center;color:${cP};font-weight:700;">${pP}%</td><td style="text-align:center;">${pF}%</td></tr>`;});
html+='</tbody></table></div>';
if(Math.abs(ecart)>=5)html+=`<div class="pdf-note"><strong>Point d'attention :</strong> L'écart moyen entre avancement physique et financier (${ecart>0?'+':''}${ecart} pts) ${ecart>0?'indique une consommation physique plus rapide que la facturation':'suggère un retard de consommation par rapport à la facturation'}. À surveiller.</div>`;
return html;}
// PDF EN COURS (enrichi)
function generatePDFEnCours(){const enCours=filteredData.filter(r=>!isCloture(r)&&!isEnService(r)&&!isInfructueux(r)),budgetBloque=enCours.reduce((s,r)=>s+parseNum(r.tot_marche),0);
let html=`<div class="pdf-summary"><strong>Résumé exécutif :</strong> <strong>${enCours.length} marchés en cours</strong> de traitement (évaluation, attribution, notification) pour un budget potentiel de <strong>${formatMontant(budgetBloque)}</strong>. Ces marchés ne sont pas encore en service et nécessitent un suivi de leur avancement administratif.</div>`;
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#0369a1;">Marchés En Cours (${enCours.length})</div><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Région</th><th>Observation</th></tr></thead><tbody>`;
enCours.forEach(r=>{html+=`<tr><td>${getMainReference(r)}<br><small>${r.csc||''}</small></td><td>${r.projet||'-'}</td><td>${r.entrepreneur||'-'}</td><td>${getRegion(r)}</td><td>${getObservation(r)}</td></tr>`;});
html+='</tbody></table></div>';return html;}
// PDF PAR RÉGION (enrichi)
function generatePDFParRegion(){const byR=buildByRegion();let html='<div class="pdf-summary"><strong>Résumé exécutif :</strong> Vue détaillée par région. Chaque section présente les marchés actifs, leur répartition par nature (CAPEX/OPEX), et l\'état d\'avancement. Les marchés Zone Sud et multi-régions apparaissent dans chaque région concernée.</div>';
ALL_REGIONS.forEach(reg=>{const rows=byR[reg]||[];if(!rows.length)return;const color=CONFIG.REGION_COLORS[reg],es=rows.filter(r=>isEnService(r)),avgP=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/rows.length);
html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:${color};">${reg} (${rows.length} marchés — Av. Phy moy. ${avgP}%)</div><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Nature</th><th>Av. Phy</th></tr></thead><tbody>`;
rows.slice(0,12).forEach(r=>{const pct=calcPct(r.avt_phy,r.tot_marche),cP=pct>=70?'#dc2626':pct>=50?'#f59e0b':'#059669';html+=`<tr><td>${getMainReference(r)}<br><small>${r.csc||''}</small></td><td>${r.projet||'-'}</td><td>${r.entrepreneur||'-'}</td><td>${String(r.nature||'-').toUpperCase()}</td><td style="text-align:center;color:${cP};font-weight:700;">${pct}%</td></tr>`;});
if(rows.length>12)html+=`<tr><td colspan="5" style="text-align:center;font-style:italic;">... et ${rows.length-12} autres marchés</td></tr>`;
html+='</tbody></table></div>';});return html;}
// PDF PILOTAGE PROACTIF (enrichi)
function generatePDFPilotageProactif(){const data=getProactifFilteredData(),projections=data.map(r=>({...r,proj:computeProjection(r)})),normal=projections.filter(x=>x.proj.verdict==='Normal'),sous=projections.filter(x=>x.proj.verdict==='Sous Min'),dep=projections.filter(x=>x.proj.verdict==='Dépassement'),none=projections.filter(x=>x.proj.verdict==='—'),exposSous=sous.reduce((s,x)=>s+(x.proj.projete-x.proj.mMin),0),surplusDep=dep.reduce((s,x)=>s+(x.proj.projete-x.proj.tot),0);
let html=`<div class="pdf-summary"><strong>Résumé exécutif :</strong> Sur <strong>${data.length} marchés en service</strong>, la projection financière identifie <strong style="color:#059669;">${normal.length} normaux</strong>, <strong style="color:#dc2626;">${sous.length} sous le montant minimum</strong> (exposition : ${formatKDTSigned(exposSous)}), <strong style="color:#d97706;">${dep.length} en dépassement</strong> (surplus : ${formatKDTSigned(surplusDep)}) et <strong>${none.length} indéterminés</strong>. ${sous.length>0?'Une action immédiate est requise sur les marchés sous-minimum.':'Aucune action critique requise.'}</div>`;
// KPI Table
html+='<div class="pdf-section"><div class="pdf-section-title" style="background:#6366F1;">Indicateurs de Projection</div><table><tbody><tr>';
html+=`<td style="text-align:center;"><strong style="color:#059669;font-size:1.3em;">${normal.length}</strong><br>Normal<br><small>${projections.length>0?Math.round((normal.length/projections.length)*100):0}%</small></td>`;
html+=`<td style="text-align:center;"><strong style="color:#DC2626;font-size:1.3em;">${sous.length}</strong><br>Sous Min<br><small style="color:#DC2626;">${formatKDTSigned(exposSous)}</small></td>`;
html+=`<td style="text-align:center;"><strong style="color:#D97706;font-size:1.3em;">${dep.length}</strong><br>Dépassement<br><small style="color:#D97706;">${formatKDTSigned(surplusDep)}</small></td>`;
html+=`<td style="text-align:center;"><strong style="color:#64748B;font-size:1.3em;">${none.length}</strong><br>Indéterminé</td></tr></tbody></table></div>`;
// Synthèse narrative
const byReg={};projections.forEach(x=>{const reg=getRegion(x);if(!byReg[reg])byReg[reg]=[];byReg[reg].push(x);});
html+='<div class="pdf-section"><div class="pdf-section-title" style="background:#4F46E5;">Synthèse Opérationnelle</div>';
Object.keys(byReg).sort().forEach(reg=>{const items=byReg[reg],sR=items.filter(x=>x.proj.verdict==='Sous Min'),dR=items.filter(x=>x.proj.verdict==='Dépassement'),nR=items.filter(x=>x.proj.verdict==='—');html+=`<p style="margin:8px 0 4px;"><strong>${reg} (${items.length} marchés)</strong></p>`;if(sR.length)html+=`<p style="margin:2px 0;color:#DC2626;font-size:0.9em;">⚠ ${sR.length} marché(s) sous le seuil minimum.</p>`;if(dR.length)html+=`<p style="margin:2px 0;color:#D97706;font-size:0.9em;">${dR.length} marché(s) en dépassement.</p>`;if(nR.length)html+=`<p style="margin:2px 0;color:#64748B;font-size:0.9em;font-style:italic;">${nR.length} marché(s) sans données.</p>`;if(!sR.length&&!dR.length&&!nR.length)html+=`<p style="margin:2px 0;color:#059669;font-size:0.9em;">✓ Tous normaux.</p>`;});
html+='</div>';
// Actions prioritaires
const actionable=projections.filter(x=>x.proj.verdict==='Sous Min'||x.proj.verdict==='Dépassement').map(x=>{const isSous=x.proj.verdict==='Sous Min',ecart=isSous?x.proj.projete-x.proj.mMin:x.proj.projete-x.proj.tot;return{...x,ecart,absEcart:Math.abs(ecart),action:isSous?'Accélérer':'Ralentir'};}).sort((a,b)=>b.absEcart-a.absEcart);
if(actionable.length){html+=`<div class="pdf-section"><div class="pdf-section-title" style="background:#DC2626;">Actions Prioritaires (${actionable.length})</div><table><thead><tr><th>#</th><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Projeté</th><th>Écart</th><th>Verdict</th><th>Action</th></tr></thead><tbody>`;actionable.forEach((x,idx)=>{const vc=x.ecart<0?'#DC2626':'#D97706';html+=`<tr><td>${idx+1}</td><td>${getMainReference(x)}<br><small>${x.csc||''}</small></td><td>${x.projet||'-'}</td><td>${x.entrepreneur||'-'}</td><td style="text-align:right;">${formatMontant(x.proj.projete)}</td><td style="text-align:right;color:${vc};font-weight:700;">${formatMontantSigned(x.ecart)}</td><td style="font-weight:700;color:${vc};">${x.proj.verdict}</td><td>${x.action}</td></tr>`;});html+='</tbody></table></div>';}
// Trajectoire par région
const sorted=[...sous,...dep,...none,...normal].sort((a,b)=>{const ra=getRegion(a),rb=getRegion(b);return ra!==rb?ra.localeCompare(rb):0;});
html+='<div class="pdf-section"><div class="pdf-section-title" style="background:#4F46E5;">Trajectoire par Région</div><table><thead><tr><th>Référence</th><th>Projet</th><th>Entrepreneur</th><th>Montant</th><th>Consommé</th><th>Projeté</th><th>Verdict</th></tr></thead><tbody>';
let lastReg='';sorted.forEach(x=>{const reg=getRegion(x);if(reg!==lastReg){html+=`<tr style="background:#e5e7eb;"><td colspan="7"><strong>${reg}</strong></td></tr>`;lastReg=reg;}const p=x.proj,vc=p.verdict==='Normal'?'#059669':p.verdict==='Sous Min'?'#DC2626':p.verdict==='Dépassement'?'#D97706':'#64748B';html+=`<tr><td>${getMainReference(x)}<br><small>${x.csc||''}</small></td><td>${x.projet||'-'}</td><td>${x.entrepreneur||'-'}</td><td style="text-align:right;">${formatMontant(p.tot)}</td><td style="text-align:right;">${formatMontant(p.consomme)}</td><td style="text-align:right;font-weight:700;">${p.projete>0?formatMontant(p.projete):'-'}</td><td style="color:${vc};font-weight:700;">${p.verdict}</td></tr>`;});
html+='</tbody></table></div>';
// Matrice de Risque
html+='<div class="pdf-section"><div class="pdf-section-title" style="background:#0B2A55;">Matrice de Risque par Région</div><table><thead><tr><th>Région</th><th>Marchés</th><th>Av. Phy</th><th>Projection</th><th>Risque</th></tr></thead><tbody>';
const byRegRisk={};data.forEach(r=>{const reg=getRegion(r);if(!byRegRisk[reg])byRegRisk[reg]=[];byRegRisk[reg].push(r);});
ALL_REGIONS.sort().forEach(reg=>{const rows=byRegRisk[reg]||[];if(!rows.length)return;const avgPhy=Math.round(rows.reduce((s,r)=>s+calcPct(r.avt_phy,r.tot_marche),0)/rows.length),projs=rows.map(r=>computeProjection(r)),nS=projs.filter(p=>p.verdict==='Sous Min').length,nD=projs.filter(p=>p.verdict==='Dépassement').length,nO=projs.filter(p=>p.verdict==='Normal').length,projTxt=[nO>0?`${nO} ok`:'',nS>0?`${nS} sous`:'',nD>0?`${nD} dép.`:''].filter(Boolean).join(' · ')||'-',risque=avgPhy>=30?'Faible':avgPhy<=22?'Élevé':'Modéré',rc=risque==='Faible'?'#059669':risque==='Élevé'?'#DC2626':'#D97706';html+=`<tr><td><strong>${reg}</strong></td><td>${rows.length}</td><td style="text-align:center;">${avgPhy}%</td><td>${projTxt}</td><td style="color:${rc};font-weight:700;">${risque}</td></tr>`;});
html+='</tbody></table></div>';
html+='<div class="pdf-note"><strong>Méthode de projection :</strong> Basée sur le taux de consommation mensuel moyen observé, extrapolé sur la durée contractuelle restante. Verdict "Sous Min" = projection < montant minimum contractuel. "Dépassement" = projection > montant maximum.</div>';
return html;}
// DOWNLOAD PDF (jsPDF)
function downloadPDF(){const pdfData=window.currentPDFData;if(!pdfData)return;const{jsPDF}=window.jspdf;const doc=new jsPDF('p','mm','a4');const pw=doc.internal.pageSize.getWidth(),ph=doc.internal.pageSize.getHeight(),m=15;
doc.setFillColor(0,40,85);doc.rect(0,0,pw,22,'F');doc.setFillColor(0,181,226);doc.rect(0,0,pw,3,'F');doc.setTextColor(255,255,255);doc.setFontSize(16);doc.setFont('helvetica','bold');doc.text('TUNISIE TELECOM',m,12);doc.setFontSize(9);doc.setFont('helvetica','normal');doc.text('Direction Centrale Zone Sud',m,17);doc.setFontSize(12);doc.text(pdfData.title,pw-m,14,{align:'right'});
let yPos=30;
const generators={0:()=>generatePDFTableForDoc(doc,'Cartographie',getCartographieData(),yPos),1:()=>generatePDFTableForDoc(doc,'Alertes',getAlertesData(),yPos),2:()=>generatePDFTableForDoc(doc,'En Service',getEnServiceData(),yPos),3:()=>generatePDFTableForDoc(doc,'Pilotage Proactif',getPilotageProactifData(),yPos),4:()=>generatePDFTableForDoc(doc,'Par Région',getCartographieData(),yPos),5:()=>generatePDFTableForDoc(doc,'En Cours',getEnCoursData(),yPos)};
if(generators[pdfData.slideIndex])generators[pdfData.slideIndex]();
const tp=doc.internal.getNumberOfPages();for(let i=1;i<=tp;i++){doc.setPage(i);doc.setFillColor(243,244,246);doc.rect(0,ph-12,pw,12,'F');doc.setTextColor(107,114,128);doc.setFontSize(8);doc.text('Division Achats - Direction Centrale Zone Sud',m,ph-5);doc.text(`Page ${i}/${tp}`,pw-m,ph-5,{align:'right'});}
doc.save(`Marches_RLA_${pdfData.title.replace(/\s+/g,'_')}_${new Date().toISOString().split('T')[0]}.pdf`);closePDFPreview();}
// EXPORT PDF
async function downloadPDF(){
const pdfData=window.currentPDFData;
if(!pdfData)return;
showLoading(true);
try{
const previewBody=document.getElementById('pdfPreviewBody');
const container=previewBody.querySelector('.pdf-page-preview');
if(!container){showError('Aperçu non disponible');return;}
// Forcer les styles pour un rendu propre
const origBg=previewBody.style.background;
previewBody.style.background='#ffffff';
const canvas=await html2canvas(container,{
scale:2,
useCORS:true,
backgroundColor:'#ffffff',
logging:false,
windowWidth:900,
onclone:function(clonedDoc){
const el=clonedDoc.querySelector('.pdf-page-preview');
if(el){el.style.boxShadow='none';el.style.margin='0';el.style.maxWidth='none';}
}
});
previewBody.style.background=origBg;
const{jsPDF}=window.jspdf;
const doc=new jsPDF('p','mm','a4');
const pw=doc.internal.pageSize.getWidth();
const ph=doc.internal.pageSize.getHeight();
const margin=8;
const usableW=pw-2*margin;
const usableH=ph-2*margin;
// Ratio image
const imgRatio=canvas.width/canvas.height;
const pageRatio=usableW/usableH;
const totalImgH=(canvas.height*usableW)/canvas.width;
// Découpage en pages
const srcPageH=Math.floor((usableH/totalImgH)*canvas.height);
let srcY=0;
let pageNum=0;
while(srcY<canvas.height){
if(pageNum>0)doc.addPage();
const sliceH=Math.min(srcPageH,canvas.height-srcY);
// Canvas temporaire pour cette tranche
const pageCanvas=document.createElement('canvas');
pageCanvas.width=canvas.width;
pageCanvas.height=sliceH;
const ctx=pageCanvas.getContext('2d');
ctx.fillStyle='#ffffff';
ctx.fillRect(0,0,pageCanvas.width,pageCanvas.height);
ctx.drawImage(canvas,0,srcY,canvas.width,sliceH,0,0,canvas.width,sliceH);
const imgData=pageCanvas.toDataURL('image/jpeg',0.92);
const renderH=(sliceH*usableW)/canvas.width;
doc.addImage(imgData,'JPEG',margin,margin,usableW,renderH);
// Numéro de page
doc.setFontSize(7);
doc.setTextColor(160,160,160);
doc.text('Page '+(pageNum+1),pw-margin,ph-4,{align:'right'});
srcY+=sliceH;
pageNum++;
}
const fileName=`Marches_RLA_${pdfData.title.replace(/[^a-zA-Z0-9àâäéèêëïîôùûüç_\-]/g,'_')}_${new Date().toISOString().split('T')[0]}.pdf`;
doc.save(fileName);
closePDFPreview();
}catch(e){
console.error('Erreur PDF:',e);
showError('Erreur génération PDF: '+e.message);
}finally{showLoading(false);}
}
// EXPORT PPTX
async function exportPPTX(){if(currentUser?.role!=='superadmin'){showError('Accès réservé au Super Admin');return;}showLoading(true);try{
const today=new Date(),lastDay=new Date(today.getFullYear(),today.getMonth(),0),dateRapport=lastDay.toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit',year:'numeric'}),moisAvt=lastDay.toLocaleDateString('fr-FR',{month:'long',year:'numeric'}).replace(/^./,c=>c.toUpperCase());
const actifs=filteredData.filter(r=>!isCloture(r)),dataES=actifs.filter(r=>isEnService(r)),capexD=dataES.filter(r=>(r.nature||'').toUpperCase()==='CAPEX'),opexD=dataES.filter(r=>(r.nature||'').toUpperCase()==='OPEX');
const alertes=dataES.filter(r=>{const p=calcPct(r.avt_phy,r.tot_marche);return p>=(isModernisation(r)?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD);}).sort((a,b)=>calcPct(b.avt_phy,b.tot_marche)-calcPct(a.avt_phy,a.tot_marche));
const pptx=new PptxGenJS();pptx.author='Nabil Derouiche';pptx.title=`Marchés RLA Zone Sud - ${moisAvt}`;pptx.company='Tunisie Telecom';pptx.layout='LAYOUT_16x9';
const pBar=(pct)=>{const b=10,f=Math.round((pct/100)*b);return'█'.repeat(Math.min(f,b))+'░'.repeat(Math.max(0,b-f))+' '+pct+'%';};
const pColor=(pct,isMod)=>{const s=isMod?CONFIG.SEUIL_MODERNISATION:CONFIG.SEUIL_STANDARD;if(pct>=CONFIG.SEUIL_CRITIQUE)return'B91C1C';if(pct>=s)return'CC6600';return'0D7C3D';};
const pDesig=(r)=>{const ref=r.ref||'',lots=r.lots||'',csc=r.csc||'';let m=ref;if(lots&&!isLot00(lots))m+=` : ${lots}`;return sanitizePPTX(csc?m+' - '+csc:m);};
// PAGE DE GARDE
let slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:'100%',fill:{color:'002855'}});slide.addShape('rect',{x:8.0,y:0,w:2.0,h:'100%',fill:{color:'001E46'}});slide.addShape('rect',{x:8.0,y:1.8,w:2.0,h:2.0,fill:{color:'0077B3'}});slide.addShape('rect',{x:0,y:0,w:'100%',h:0.06,fill:{color:'00B5E2'}});slide.addShape('rect',{x:0,y:0.06,w:0.15,h:5.57,fill:{color:'00B5E2'}});
slide.addText('TUNISIE TELECOM',{x:0.7,y:0.5,w:7,h:0.3,fontSize:11,color:'00B5E2',bold:true,charSpacing:4});slide.addShape('rect',{x:0.7,y:0.95,w:1.3,h:0.03,fill:{color:'00B5E2'}});slide.addText('AXE ACHATS',{x:0.7,y:1.3,w:7,h:0.85,fontSize:42,bold:true,color:'FFFFFF'});slide.addText('Marchés RLA — Zone Sud',{x:0.7,y:2.15,w:7,h:0.45,fontSize:21,color:'80D0F0'});
slide.addShape('roundRect',{x:0.7,y:3.1,w:2.6,h:0.5,fill:{color:'00B5E2'},rectRadius:0.06});slide.addText(dateRapport,{x:0.7,y:3.1,w:2.6,h:0.5,fontSize:18,bold:true,color:'002855',align:'center',valign:'middle'});slide.addText(moisAvt,{x:3.5,y:3.15,w:4,h:0.4,fontSize:12,color:'6B9CC5',valign:'middle'});
slide.addShape('rect',{x:0,y:5.13,w:'100%',h:0.5,fill:{color:'001533'}});slide.addText('Direction Centrale Zone Sud • Division Achats',{x:0.7,y:5.13,w:5,h:0.5,fontSize:9.5,color:'5A8DB5',valign:'middle'});slide.addText('Nabil Derouiche',{x:6,y:5.13,w:3.5,h:0.5,fontSize:9.5,color:'5A8DB5',valign:'middle',align:'right'});
// SLIDE ALERTES
if(alertes.length){slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'FEE2E2'}});slide.addText('Alertes Consommation',{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'C41E3A'});const hdr=[{text:'Désignation',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10}},{text:'Entrepreneur',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10}},{text:'Projet',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10,align:'center'}},{text:'Avt Phy',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10,align:'center'}},{text:'Délai',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10,align:'center'}},{text:'Alerte',options:{bold:true,fill:{color:'C41E3A'},color:'FFFFFF',fontSize:10,align:'center'}}];const rows=alertes.slice(0,12).map((r,idx)=>{const pct=calcPct(r.avt_phy,r.tot_marche),isMod=isModernisation(r),delai=getDelaiRestant(r),isCrit=pct>=CONFIG.SEUIL_CRITIQUE,bg=idx%2===0?'FFFFFF':'FEF2F2';return[{text:pDesig(r),options:{fontSize:9,fill:{color:bg}}},{text:r.entrepreneur||'-',options:{fontSize:9,fill:{color:bg}}},{text:r.projet||'-',options:{fontSize:9,fill:{color:bg},align:'center'}},{text:pBar(pct),options:{fontSize:10,fill:{color:bg},color:pColor(pct,isMod),fontFace:'Consolas'}},{text:delai!==null?delai+'j':'-',options:{fontSize:9,fill:{color:bg},align:'center',bold:true,color:delai<=CONFIG.DELAI_CRITIQUE?'B91C1C':'CC6600'}},{text:isCrit?'Critique':'Attention',options:{fontSize:9,fill:{color:bg},align:'center',bold:true,color:isCrit?'B91C1C':'CC6600'}}];});slide.addTable([hdr,...rows],{x:0.2,y:0.77,w:9.6,colW:[3.0,1.8,1.4,2.0,0.7,0.7],border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.36});}
// SLIDES CAPEX/OPEX
const buildSlides=(data,type,headerColor)=>{if(!data.length)return;const perPage=10;for(let p=0;p<Math.ceil(data.length/perPage);p++){slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:type==='capex'?'DBEAFE':'E0F2FE'}});slide.addText(`Marchés RLA en vigueur - ${type.toUpperCase()}`,{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:headerColor});const hdr=[{text:'Projet',options:{bold:true,fill:{color:headerColor},color:'FFFFFF',fontSize:10,align:'center'}},{text:'Désignation',options:{bold:true,fill:{color:headerColor},color:'FFFFFF',fontSize:10}},{text:'Entrepreneur',options:{bold:true,fill:{color:headerColor},color:'FFFFFF',fontSize:10}},{text:'Avt Fin',options:{bold:true,fill:{color:headerColor},color:'FFFFFF',fontSize:10,align:'center'}},{text:'Avt Phy',options:{bold:true,fill:{color:headerColor},color:'FFFFFF',fontSize:10,align:'center'}}];const chunk=data.slice(p*perPage,(p+1)*perPage);const rows=chunk.map((r,idx)=>{const isMod=isModernisation(r),pF=calcPct(r.avt_fin,r.tot_marche),pP=calcPct(r.avt_phy,r.tot_marche),bg=idx%2===0?'FFFFFF':(type==='capex'?'EFF6FF':'F0F9FF');return[{text:r.projet||'-',options:{fontSize:9,fill:{color:bg},align:'center'}},{text:pDesig(r),options:{fontSize:9,fill:{color:bg}}},{text:r.entrepreneur||'-',options:{fontSize:9,fill:{color:bg}}},{text:pBar(pF),options:{fontSize:10,fill:{color:bg},color:pColor(pF,isMod),fontFace:'Consolas'}},{text:pBar(pP),options:{fontSize:10,fill:{color:bg},color:pColor(pP,isMod),fontFace:'Consolas'}}];});slide.addTable([hdr,...rows],{x:0.2,y:0.77,w:9.6,colW:[1.0,3.4,1.8,1.7,1.7],border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.42});}};
buildSlides(capexD,'capex','1A365D');buildSlides(opexD,'opex','0D4A6F');
// PILOTAGE PROACTIF PPTX
buildProactifSlides(pptx,dataES);
await pptx.writeFile({fileName:`Marches_RLA_Zone_Sud_${moisAvt.replace(' ','_')}.pptx`});
}catch(e){console.error('Erreur PPTX:',e);showError('Erreur: '+e.message);}finally{showLoading(false);}}
function buildProactifSlides(pptx,enServiceData){const projections=enServiceData.map(r=>({...r,proj:computeProjection(r)})),normal=projections.filter(x=>x.proj.verdict==='Normal'),sous=projections.filter(x=>x.proj.verdict==='Sous Min'),dep=projections.filter(x=>x.proj.verdict==='Dépassement'),none=projections.filter(x=>x.proj.verdict==='—');
const fmtK=v=>{const abs=Math.abs(v);if(abs>=1e6)return sanitizePPTX((v/1e6).toFixed(1)+' MDT');if(abs>=1e3)return sanitizePPTX(Math.round(v/1e3)+' kDT');return formatMontantPPTX(v);};
const verdictColor=v=>v==='Normal'?'059669':v==='Sous Min'?'DC2626':v==='Dépassement'?'D97706':'64748B';
const pDesig=r=>{const ref=r.ref||'',lots=r.lots||'',csc=r.csc||'';let m=ref;if(lots&&!isLot00(lots))m+=' : '+lots;return sanitizePPTX(csc?m+' - '+csc:m);};
const tHdr=[{text:'Désignation',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}},{text:'Entrepreneur',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}},{text:'Projet',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Montant',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Mnt Min',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Projeté',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Verdict',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}}];
const tColW=[2.6,1.3,1.0,1.1,1.0,1.1,1.5];
const tRow=(x,idx)=>{const p=x.proj,bg=idx%2===0?'FFFFFF':'F5F3FF';return[{text:pDesig(x),options:{fontSize:8,fill:{color:bg}}},{text:sanitizePPTX(x.entrepreneur||'-'),options:{fontSize:8,fill:{color:bg}}},{text:sanitizePPTX(x.projet||'-'),options:{fontSize:8,fill:{color:bg},align:'center'}},{text:formatMontantPPTX(p.tot),options:{fontSize:8,fill:{color:bg},align:'center'}},{text:p.mMin>0?formatMontantPPTX(p.mMin):'-',options:{fontSize:8,fill:{color:bg},align:'center'}},{text:p.projete>0?formatMontantPPTX(p.projete):'-',options:{fontSize:8,fill:{color:bg},align:'center',bold:true}},{text:p.verdict,options:{fontSize:8,fill:{color:bg},align:'center',bold:true,color:verdictColor(p.verdict)}}];};
const sorted=[...sous,...dep,...none,...normal],P1=9,PN=11,totalPg=sorted.length<=P1?1:1+Math.ceil((sorted.length-P1)/PN);
// Page 1: KPI + lignes
let slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'EEF2FF'}});slide.addText('Pilotage Proactif — Verdicts',{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'4F46E5'});
const exposTotal=sous.reduce((s,x)=>s+(x.proj.projete-x.proj.mMin),0),surpTotal=dep.reduce((s,x)=>s+(x.proj.projete-x.proj.tot),0);
const kpis=[{label:'Normal',value:normal.length,sub:Math.round(normal.length/projections.length*100)+'%',color:'059669',bg:'ECFDF5'},{label:'Sous Min',value:sous.length,sub:fmtK(exposTotal),color:'DC2626',bg:'FEF2F2'},{label:'Dépassement',value:dep.length,sub:'+'+fmtK(surpTotal),color:'D97706',bg:'FFFBEB'},{label:'Indéterminé',value:none.length,sub:'Données insuff.',color:'64748B',bg:'F1F5F9'}];
kpis.forEach((k,i)=>{const bx=0.3+i*2.4;slide.addShape('roundRect',{x:bx,y:0.75,w:2.1,h:0.78,fill:{color:k.bg},rectRadius:0.08,line:{color:k.color,width:1.5}});slide.addText(String(k.value),{x:bx,y:0.75,w:2.1,h:0.38,fontSize:22,bold:true,color:k.color,align:'center',valign:'middle'});slide.addText(k.label,{x:bx,y:1.1,w:2.1,h:0.2,fontSize:9,color:k.color,align:'center',valign:'middle'});slide.addText(sanitizePPTX(k.sub),{x:bx,y:1.28,w:2.1,h:0.2,fontSize:7,color:'64748B',align:'center',valign:'middle'});});
slide.addTable([tHdr,...sorted.slice(0,P1).map((x,i)=>tRow(x,i))],{x:0.2,y:1.65,w:9.6,colW:tColW,border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.38});
for(let pg=1;pg<totalPg;pg++){slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'EEF2FF'}});slide.addText(sanitizePPTX('Verdicts ('+(pg+1)+'/'+totalPg+')'),{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'4F46E5'});const start=P1+(pg-1)*PN;slide.addTable([tHdr,...sorted.slice(start,start+PN).map((x,i)=>tRow(x,i))],{x:0.2,y:0.77,w:9.6,colW:tColW,border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.40});}
// Alertes par région
const byReg={};enServiceData.forEach(r=>{const rg=getRegion(r);if(!byReg[rg])byReg[rg]=[];byReg[rg].push(r);});
slide=pptx.addSlide();slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'FEE2E2'}});slide.addText('Alertes & Mise en Garde par Région',{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'DC2626'});
const regSansMod=getRegionsSansModernisation(),infructueux=filteredData.filter(r=>!isCloture(r)&&isInfructueux(r));
const aHdr=[{text:'Région',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9}},{text:'Sous Min',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Exposition',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Dépass.',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Surplus',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9,align:'center'}},{text:'Alerte spécifique',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9}},{text:'Action prioritaire',options:{bold:true,fill:{color:'B91C1C'},color:'FFFFFF',fontSize:9}}];
// ═══ SLIDE: SUIVI MODERNISATION PAR RÉGION ═══
const modActifs=enServiceData.filter(r=>isModernisation(r));
const modByReg={};ALL_REGIONS.forEach(rg=>modByReg[rg]=[]);
modActifs.forEach(r=>{getRegionsForMarcheV2(r).forEach(rg=>{if(modByReg[rg])modByReg[rg].push(r);});});
slide=pptx.addSlide();
slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'EDE9FE'}});
slide.addText('Suivi Modernisation par Région',{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'7C3AED'});
const mHdr=[
{text:'Région',options:{bold:true,fill:{color:'7C3AED'},color:'FFFFFF',fontSize:9}},
{text:'Marché en vigueur',options:{bold:true,fill:{color:'7C3AED'},color:'FFFFFF',fontSize:9}},
{text:'Av. Phy',options:{bold:true,fill:{color:'7C3AED'},color:'FFFFFF',fontSize:9,align:'center'}},
{text:'Estimation',options:{bold:true,fill:{color:'7C3AED'},color:'FFFFFF',fontSize:9,align:'center'}},
{text:'Statut Pipeline',options:{bold:true,fill:{color:'7C3AED'},color:'FFFFFF',fontSize:9}}
];
const mRows=ALL_REGIONS.map((rg,idx)=>{
const mm=modByReg[rg],pipe=getPipelineForRegion(rg,'Modernisation'),pR=pipe[0];
const bg=idx%2===0?'FFFFFF':'F5F3FF';
let mRef='-',avP='-',avC='374151';
if(mm.length===1){
const m=mm[0],pct=calcPct(m.avt_phy,m.tot_marche);
mRef=sanitizePPTX(`${getMainReference(m)} (${m.entrepreneur||'-'})`);
avP=`${pct}%`;avC=pct>=90?'B91C1C':pct>=50?'CC6600':'0D7C3D';
} else if(mm.length>1){
const mx=Math.max(...mm.map(m=>calcPct(m.avt_phy,m.tot_marche)));
mRef=sanitizePPTX(`${mm.length} lots (max ${mx}%)`);
avP=`${mx}%`;avC=mx>=90?'B91C1C':mx>=50?'CC6600':'0D7C3D';
}
let pEst='-',pStat='Non programmé',pCol='DC2626';
if(pR){
if(isModernisationDecision(pR)){pStat=sanitizePPTX(pR.statut_dca);pCol='2563EB';}
else{pEst=sanitizePPTX(pR.estimation||'-');pStat=sanitizePPTX(pR.statut_dca);
const sl=pR.statut_dca.toLowerCase();
pCol=(sl.includes('communiqu')||sl.includes('transmis')||sl.includes('déjà'))?'059669':'CC6600';
}
}
return[
{text:rg,options:{fontSize:9,fill:{color:bg},bold:true}},
{text:mRef,options:{fontSize:8,fill:{color:bg}}},
{text:avP,options:{fontSize:9,fill:{color:bg},align:'center',bold:true,color:avC}},
{text:pEst,options:{fontSize:8,fill:{color:bg},align:'center'}},
{text:pStat,options:{fontSize:8,fill:{color:bg},color:pCol,bold:true}}
];
});
slide.addTable([mHdr,...mRows],{x:0.2,y:0.77,w:9.6,colW:[1.2,3.0,0.8,1.3,3.3],border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.50});
// ═══ SLIDE: PIPELINE DE LANCEMENT ═══
slide=pptx.addSlide();
slide.addShape('rect',{x:0,y:0,w:'100%',h:0.65,fill:{color:'EEF2FF'}});
slide.addText(sanitizePPTX('Pipeline de Lancement (Table 872)'),{x:0.3,y:0.12,w:9.4,h:0.45,fontSize:20,bold:true,color:'4F46E5'});
const plHdr=[
{text:'Projet',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}},
{text:'Régions',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}},
{text:'Estimation',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9,align:'center'}},
{text:'Durée',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}},
{text:'Statut DCA',options:{bold:true,fill:{color:'4F46E5'},color:'FFFFFF',fontSize:9}}
];
const plRows=pipelineData.map((p,idx)=>{
const bg=idx%2===0?'FFFFFF':'F5F3FF';
const isDec=isModernisationDecision(p);
const sl=(p.statut_dca||'').toLowerCase();
const sC=isDec?'2563EB':(sl.includes('communiqu')||sl.includes('transmis')||sl.includes('déjà'))?'059669':sl.includes('pas de')?'64748B':'CC6600';
return[
{text:sanitizePPTX(p.projet),options:{fontSize:9,fill:{color:bg},bold:true}},
{text:sanitizePPTX(p.regions.join(', ')),options:{fontSize:8,fill:{color:bg}}},
{text:sanitizePPTX(p.estimation||'-'),options:{fontSize:8,fill:{color:bg},align:'center'}},
{text:sanitizePPTX(p.duree||'-'),options:{fontSize:8,fill:{color:bg}}},
{text:sanitizePPTX(p.statut_dca||'-'),options:{fontSize:8,fill:{color:bg},color:sC,bold:true}}
];
});
slide.addTable([plHdr,...plRows],{x:0.2,y:0.77,w:9.6,colW:[1.5,2.5,1.3,1.8,2.5],border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.42});
const aRows=ALL_REGIONS.sort().map((reg,idx)=>{const rows=byReg[reg]||[],projs=rows.map(r=>({...r,proj:computeProjection(r)})),rS=projs.filter(x=>x.proj.verdict==='Sous Min'),rD=projs.filter(x=>x.proj.verdict==='Dépassement'),expo=rS.reduce((s,x)=>s+(x.proj.projete-x.proj.mMin),0),surp=rD.reduce((s,x)=>s+(x.proj.projete-x.proj.tot),0);
let alertSpec=[];const rI=infructueux.filter(r=>getRegionsForMarcheV2(r).includes(reg));if(rI.length)alertSpec.push(`${rI.length} infructueux`);if(regSansMod.includes(reg)){
const regPipe=getPipelineForRegion(reg,'Modernisation');
if(regPipe.length>0){
const pR=regPipe[0];
alertSpec.push(isModernisationDecision(pR)?sanitizePPTX(pR.statut_dca):sanitizePPTX('Pipeline: '+pR.statut_dca));
} else {
alertSpec.push('Modernisation a lancer');
}
}
const noC=projs.filter(x=>x.proj.verdict==='—');if(noC.length)alertSpec.push(`${noC.length} sans consomm.`);
let action='-';if(rS.length){const worst=rS.sort((a,b)=>(a.proj.projete-a.proj.mMin)-(b.proj.projete-b.proj.mMin))[0];action=sanitizePPTX(`Accelerer ${getMainReference(worst)}`);}else if(rD.length)action=sanitizePPTX(`Ralentir ${getMainReference(rD[0])}`);
const bg=idx%2===0?'FFFFFF':'FEF2F2',hasRisk=rS.length>0||rD.length>0;
return[{text:reg,options:{fontSize:9,fill:{color:bg},bold:true,color:hasRisk?'DC2626':'059669'}},{text:rS.length>0?String(rS.length):'-',options:{fontSize:9,fill:{color:bg},align:'center',color:rS.length>0?'DC2626':'374151'}},{text:expo<0?sanitizePPTX(formatMontant(expo)):'-',options:{fontSize:8,fill:{color:bg},align:'center',color:expo<0?'DC2626':'374151'}},{text:rD.length>0?String(rD.length):'-',options:{fontSize:9,fill:{color:bg},align:'center',color:rD.length>0?'D97706':'374151'}},{text:surp>0?sanitizePPTX('+'+formatMontant(surp)):'-',options:{fontSize:8,fill:{color:bg},align:'center',color:surp>0?'D97706':'374151'}},{text:alertSpec.length?alertSpec.join(' | '):'RAS',options:{fontSize:8,fill:{color:bg},color:alertSpec.length?'7C3AED':'059669'}},{text:action,options:{fontSize:8,fill:{color:bg},color:'374151'}}];});
slide.addTable([aHdr,...aRows],{x:0.15,y:0.77,w:9.7,colW:[1.1,0.8,1.3,0.8,1.3,2.4,2.0],border:{pt:0.5,color:'E5E7EB'},valign:'middle',rowH:0.42});
}
// EVENT LISTENERS
document.getElementById('regionModal').addEventListener('click',e=>{if(e.target.id==='regionModal')closeModal();});
document.getElementById('adminModal').addEventListener('click',e=>{if(e.target.id==='adminModal')closeAdminModal();});
document.getElementById('pdfPreviewModal').addEventListener('click',e=>{if(e.target.id==='pdfPreviewModal')closePDFPreview();});
// INIT
document.addEventListener('DOMContentLoaded',()=>{loadTheme();checkSession();});
</script>
</body>
</html>