1360 lines
167 KiB
HTML
1360 lines
167 KiB
HTML
<!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(/"/g,'"').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/'/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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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,'"');
|
||
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> (<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> |