From 1e79b7cc522cdef788a97ba2c51a25fb297961af Mon Sep 17 00:00:00 2001 From: dom Date: Mon, 23 Feb 2026 10:56:15 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20viewer=20=E2=80=94=20affichage=20qualit?= =?UTF-8?q?=C3=A9=20CPAM,=20tra=C3=A7abilit=C3=A9=20d=C3=A9cisions=20DP/DA?= =?UTF-8?q?S,=20VetoReport=20et=20bio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CPAM : badge quality_tier (A/B/C), bandeau requires_review, warnings catégorisés, force probante dossier - DP/DAS : code suggestion barré → code final si modifié, ligne grisée si ruled_out, badges décision + règles - DAS : badge needs_info avec détails, raison ruled_out sous la ligne - VetoReport : section contestabilité avec verdict, barre score/100, tableau issues HARD/MEDIUM/LOW - Biologie : badge Suspect avec tooltip, valeurs écartées en details pliable - Nouveau filtre Jinja2 decision_badge, import _assess_dossier_strength (pas de duplication) Co-Authored-By: Claude Opus 4.6 --- src/viewer/app.py | 23 ++++ src/viewer/templates/detail.html | 190 +++++++++++++++++++++++++++++-- 2 files changed, 205 insertions(+), 8 deletions(-) diff --git a/src/viewer/app.py b/src/viewer/app.py index f7aa818..c012e8e 100644 --- a/src/viewer/app.py +++ b/src/viewer/app.py @@ -22,6 +22,7 @@ from ..config import ( CIM10_PDF, GUIDE_METHODO_PDF, CCAM_PDF, CIM10_DICT_PATH, CIM10_SUPPLEMENTS_PATH, ) from .. import config as cfg +from ..control.cpam_context import _assess_dossier_strength from .referentiels import ReferentielManager from .validation import ValidationManager @@ -374,6 +375,24 @@ def format_doc_name(name: str) -> str: return name +def decision_badge(decision) -> Markup: + """Badge HTML pour une CodeDecision (action != KEEP).""" + if not decision: + return Markup("") + action = decision.get("action", "KEEP") if isinstance(decision, dict) else getattr(decision, "action", "KEEP") + if action == "KEEP": + return Markup("") + labels = { + "DOWNGRADE": ("Rétrogradé", "#fef3c7", "#92400e"), + "REMOVE": ("Supprimé", "#fee2e2", "#dc2626"), + "RULED_OUT": ("Écarté", "#f1f5f9", "#64748b"), + "NEED_INFO": ("Info requise", "#fff7ed", "#c2410c"), + "PROMOTE_DP": ("Promu DP", "#dbeafe", "#1d4ed8"), + } + label, bg, fg = labels.get(action, (action, "#f1f5f9", "#64748b")) + return Markup(f'{label}') + + def format_cpam_text(text: str | None) -> Markup: """Convertit un texte CPAM (section) en HTML avec puces et paragraphes.""" if not text: @@ -420,6 +439,7 @@ def create_app() -> Flask: app.jinja_env.filters["format_dossier_name"] = format_dossier_name app.jinja_env.filters["format_doc_name"] = format_doc_name app.jinja_env.filters["format_cpam_text"] = format_cpam_text + app.jinja_env.filters["decision_badge"] = decision_badge ccam_dict = load_ccam_dict() @@ -440,6 +460,8 @@ def create_app() -> Flask: if len(rel_parts) > 1: current_group = str(Path(*rel_parts[:-1])) siblings = groups.get(current_group, []) + # Force probante (pour section CPAM) + dossier_strength = _assess_dossier_strength(dossier) if dossier.controles_cpam else None return render_template( "detail.html", dossier=dossier, @@ -447,6 +469,7 @@ def create_app() -> Flask: ccam_dict=ccam_dict, siblings=siblings, current_group=current_group, + dossier_strength=dossier_strength, ) @app.route("/dashboard") diff --git a/src/viewer/templates/detail.html b/src/viewer/templates/detail.html index 53e77a8..7c7f6d4 100644 --- a/src/viewer/templates/detail.html +++ b/src/viewer/templates/detail.html @@ -131,9 +131,14 @@ {% if dossier.controles_cpam %}

Contrôle CPAM ({{ dossier.controles_cpam|length }})

+ {% if dossier_strength and dossier_strength.is_weak %} +
+ Dossier à preuves limitées (score {{ dossier_strength.score }}/10) — manque : {{ dossier_strength.missing|join(', ') }} +
+ {% endif %} {% for ctrl in dossier.controles_cpam %}
-
+
OGC {{ ctrl.numero_ogc }} — {{ ctrl.titre }} {% if 'retient' in ctrl.decision_ucr|lower %} {{ ctrl.decision_ucr }} @@ -142,6 +147,13 @@ {% else %} {{ ctrl.decision_ucr }} {% endif %} + {% if ctrl.quality_tier == 'A' %} + Qualité A + {% elif ctrl.quality_tier == 'B' %} + Qualité B + {% elif ctrl.quality_tier == 'C' %} + Qualité C + {% endif %}
{# Argument CPAM #} @@ -165,6 +177,13 @@
{% endif %} + {# Bandeau revue manuelle si Tier C #} + {% if ctrl.requires_review %} +
+ ⚠ Revue manuelle requise — la contre-argumentation contient des incohérences détectées +
+ {% endif %} + {# Contre-argumentation structurée ou fallback texte brut #} {% if ctrl.response_data %}
@@ -261,6 +280,22 @@ {% endfor %} {% endif %} + + {# Avertissements qualité #} + {% if ctrl.quality_warnings %} +
+ Avertissements qualité ({{ ctrl.quality_warnings|length }}) +
    + {% for w in ctrl.quality_warnings %} + {% if w.startswith('[CRITIQUE]') %} +
  • {{ w }}
  • + {% else %} +
  • {{ w }}
  • + {% endif %} + {% endfor %} +
+
+ {% endif %}
{% endfor %}
@@ -282,17 +317,75 @@
{% endif %} +{# ---- Contestabilité (VetoReport) ---- #} +{% if dossier.veto_report %} +{% set vr = dossier.veto_report %} +{% if vr.verdict == 'PASS' %} + {% set vr_color = '#22c55e' %} +{% elif vr.verdict == 'NEED_INFO' %} + {% set vr_color = '#f59e0b' %} +{% else %} + {% set vr_color = '#ef4444' %} +{% endif %} +
+

Contestabilité du dossier

+
+ {% if vr.verdict == 'PASS' %} + PASS + {% elif vr.verdict == 'NEED_INFO' %} + NEED_INFO + {% else %} + FAIL + {% endif %} +
+
+
+ {{ vr.score_contestabilite }}/100 +
+ {% if vr.issues %} +
+ Problèmes détectés ({{ vr.issues|length }}) + + + + {% for issue in vr.issues %} + + + + + + + {% endfor %} + +
VetoSévéritéLocalisationMessage
{{ issue.veto }} + {% if issue.severity == 'HARD' %}HARD + {% elif issue.severity == 'MEDIUM' %}MEDIUM + {% else %}LOW{% endif %} + {{ issue.where }}{{ issue.message }}
+
+ {% endif %} +
+{% endif %} + {# ---- Diagnostic principal ---- #} {% if dossier.diagnostic_principal %} {% set dp = dossier.diagnostic_principal %} -
+

Diagnostic principal

- {{ dp.texte }} + {% if dp.status == 'ruled_out' %}{{ dp.texte }}{% else %}{{ dp.texte }}{% endif %} {% if dp.source_page %}{% endif %}
{% if dp.cim10_suggestion %} - {{ dp.cim10_suggestion }} + {% if dp.cim10_final and dp.cim10_final != dp.cim10_suggestion %} + {{ dp.cim10_suggestion }} + + {{ dp.cim10_final }} + {% elif dp.status == 'ruled_out' %} + {{ dp.cim10_suggestion }} + {% else %} + {{ dp.cim10_suggestion }} + {% endif %} {{ dp.cim10_confidence | confidence_badge }} {% if dp.niveau_cma and dp.niveau_cma > 1 %} {{ dp.niveau_cma | cma_level_badge }} @@ -300,6 +393,18 @@ CMA {% endif %} {{ dp.niveau_severite | severity_badge }} + {% if dp.cim10_decision and dp.cim10_decision.action != 'KEEP' %} + {{ dp.cim10_decision | decision_badge }} + {% for rule in dp.cim10_decision.applied_rules %} + {{ rule }} + {% endfor %} + {% endif %} + {% endif %} + {% if dp.status == 'ruled_out' and dp.ruled_out_reason %} +
{{ dp.ruled_out_reason }}
+ {% endif %} + {% if dp.cim10_decision and dp.cim10_decision.action != 'KEEP' and dp.cim10_decision.reason %} +
{{ dp.cim10_decision.reason }}
{% endif %} {% if dp.justification %}
{{ dp.justification }}
@@ -340,9 +445,43 @@ TexteCIM-10ConfianceCMASourceJustification {% for das in dossier.diagnostics_associes %} - + {{ das.texte }} - {% if das.cim10_suggestion %}{{ das.cim10_suggestion }}{% endif %} + + {% if das.cim10_suggestion %} + {% if das.cim10_final and das.cim10_final != das.cim10_suggestion %} + {{ das.cim10_suggestion }} + + {{ das.cim10_final }} + {% elif das.status == 'ruled_out' %} + {{ das.cim10_suggestion }} + {% else %} + {{ das.cim10_suggestion }} + {% endif %} + {% if das.cim10_decision and das.cim10_decision.action != 'KEEP' %} +
+ {{ das.cim10_decision | decision_badge }} + {% for rule in das.cim10_decision.applied_rules %} + {{ rule }} + {% endfor %} +
+ {% endif %} + {% if das.status == 'needs_info' %} +
+ Info requise + {% if das.cim10_decision and das.cim10_decision.needs_info %} +
détails +
    + {% for ni in das.cim10_decision.needs_info %} +
  • {{ ni }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% endif %} + {% endif %} + {{ das.cim10_confidence | confidence_badge }} {% if das.niveau_cma and das.niveau_cma > 1 %} @@ -374,6 +513,20 @@ {% endif %} + {% if das.status == 'ruled_out' and das.ruled_out_reason %} + + +
{{ das.ruled_out_reason }}
+ + + {% endif %} + {% if das.cim10_decision and das.cim10_decision.action != 'KEEP' and das.cim10_decision.reason and das.status != 'ruled_out' %} + + +
{{ das.cim10_decision.reason }}
+ + + {% endif %} {% if das.raisonnement %} @@ -446,15 +599,36 @@ TestValeurAnomalieSource {% for b in dossier.biologie_cle %} - + {{ b.test }} {{ b.valeur or '' }} - {% if b.anomalie %}Oui{% else %}—{% endif %} + + {% if b.quality == 'suspect' %} + ⚠ Suspect + {% elif b.anomalie %} + Oui + {% else %} + — + {% endif %} + {% if b.source_page %}{% endif %} {% endfor %} + {% if dossier.biologie_discarded %} +
+ Valeurs écartées ({{ dossier.biologie_discarded|length }}) + + + + {% for b in dossier.biologie_discarded %} + + {% endfor %} + +
TestValeurRaison
{{ b.test or '' }}{{ b.valeur or '' }}{{ b.discard_reason or '—' }}
+
+ {% endif %}
{% endif %}