From ee661dae1d7a2fefc75d204abe78604e2802b17f Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 13 Feb 2026 18:11:21 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20dashboard=20m=C3=A9triques=20+=20vue=20?= =?UTF-8?q?CPAM=20agr=C3=A9g=C3=A9e=20dans=20le=20viewer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout d'un dashboard global (distribution confiance DP, top 15 codes CIM-10, types GHM, sévérité) et d'une page listant tous les contrôles CPAM agrégés. Co-Authored-By: Claude Opus 4.6 --- src/viewer/app.py | 112 +++++++++++++++++++++ src/viewer/templates/base.html | 8 ++ src/viewer/templates/cpam.html | 88 +++++++++++++++++ src/viewer/templates/dashboard.html | 145 ++++++++++++++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 src/viewer/templates/cpam.html create mode 100644 src/viewer/templates/dashboard.html diff --git a/src/viewer/app.py b/src/viewer/app.py index 5723685..4d91c2d 100644 --- a/src/viewer/app.py +++ b/src/viewer/app.py @@ -13,6 +13,8 @@ from markupsafe import Markup from werkzeug.utils import secure_filename +from collections import Counter + from ..config import STRUCTURED_DIR, OLLAMA_URL, CCAM_DICT_PATH, DossierMedical, ALLOWED_EXTENSIONS, UPLOAD_MAX_SIZE_MB from .. import config as cfg from .referentiels import ReferentielManager @@ -54,6 +56,104 @@ def compute_group_stats(items: list[dict]) -> dict: } +def compute_dashboard_stats(groups: dict[str, list[dict]]) -> dict: + """Calcule les statistiques globales du pipeline pour le dashboard.""" + total_dossiers = len(groups) + total_fichiers = 0 + total_das = 0 + total_actes = 0 + total_alertes = 0 + total_cma = 0 + total_cpam = 0 + dp_confidence: Counter = Counter() + dp_validity: Counter = Counter() + code_counter: Counter = Counter() + ghm_types: Counter = Counter() + severity_dist: Counter = Counter() + processing_times: list[float] = [] + + for items in groups.values(): + total_fichiers += len(items) + for item in items: + d = item["dossier"] + total_das += len(d.diagnostics_associes) + total_actes += len(d.actes_ccam) + total_alertes += len(d.alertes_codage) + total_cpam += len(d.controles_cpam) + + if d.processing_time_s is not None: + processing_times.append(d.processing_time_s) + + # DP confidence & validity + dp = d.diagnostic_principal + if dp: + conf = dp.cim10_confidence or "none" + dp_confidence[conf] += 1 + if dp.cim10_suggestion: + dp_validity["valide"] += 1 + code_counter[dp.cim10_suggestion] += 1 + else: + dp_validity["absent"] += 1 + else: + dp_confidence["none"] += 1 + dp_validity["absent"] += 1 + + # DAS codes + CMA + for das in d.diagnostics_associes: + if das.cim10_suggestion: + code_counter[das.cim10_suggestion] += 1 + if das.est_cma: + total_cma += 1 + if dp and dp.est_cma: + total_cma += 1 + + # GHM + ghm = d.ghm_estimation + if ghm: + if ghm.type_ghm: + ghm_types[ghm.type_ghm] += 1 + severity_dist[ghm.severite] += 1 + + top_codes = code_counter.most_common(15) + top_max = top_codes[0][1] if top_codes else 1 + + return { + "total_dossiers": total_dossiers, + "total_fichiers": total_fichiers, + "total_das": total_das, + "total_actes": total_actes, + "total_alertes": total_alertes, + "total_cma": total_cma, + "total_cpam": total_cpam, + "dp_confidence": dict(dp_confidence), + "dp_validity": dict(dp_validity), + "top_codes": top_codes, + "top_max": top_max, + "ghm_types": dict(ghm_types), + "severity_dist": dict(severity_dist), + "processing_time_total": sum(processing_times), + "processing_time_avg": sum(processing_times) / len(processing_times) if processing_times else 0, + } + + +def collect_cpam_controls(groups: dict[str, list[dict]]) -> list[dict]: + """Collecte tous les contrôles CPAM de tous les dossiers.""" + controls = [] + for group_name, items in groups.items(): + for item in items: + d = item["dossier"] + dp_code = d.diagnostic_principal.cim10_suggestion if d.diagnostic_principal else None + for ctrl in d.controles_cpam: + controls.append({ + "group_name": group_name, + "filepath": item["path_rel"], + "ctrl": ctrl, + "dp_code": dp_code, + }) + controls.sort(key=lambda c: c["ctrl"].numero_ogc) + return controls + + def load_ccam_dict() -> dict[str, dict]: """Charge le dictionnaire CCAM pour les regroupements.""" if CCAM_DICT_PATH.exists(): @@ -255,6 +355,18 @@ def create_app() -> Flask: current_group=current_group, ) + @app.route("/dashboard") + def dashboard(): + groups = scan_dossiers() + stats = compute_dashboard_stats(groups) + return render_template("dashboard.html", stats=stats, groups=groups) + + @app.route("/cpam") + def cpam_list(): + groups = scan_dossiers() + controls = collect_cpam_controls(groups) + return render_template("cpam.html", controls=controls, total=len(controls), groups=groups) + @app.route("/admin/models", methods=["GET"]) def list_models(): models = fetch_ollama_models() diff --git a/src/viewer/templates/base.html b/src/viewer/templates/base.html index 070dbab..5ec6d65 100644 --- a/src/viewer/templates/base.html +++ b/src/viewer/templates/base.html @@ -250,6 +250,14 @@ {% block sidebar %}{% endblock %}