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 }})
+
+ | Veto | Sévérité | Localisation | Message |
+
+ {% for issue in vr.issues %}
+
+ {{ issue.veto }} |
+
+ {% if issue.severity == 'HARD' %}HARD
+ {% elif issue.severity == 'MEDIUM' %}MEDIUM
+ {% else %}LOW{% endif %}
+ |
+ {{ issue.where }} |
+ {{ issue.message }} |
+
+ {% endfor %}
+
+
+
+ {% 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 @@
| Texte | CIM-10 | Confiance | CMA | Source | Justification |
{% 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 @@
| Test | Valeur | Anomalie | Source |
|
{% 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 }})
+
+ | Test | Valeur | Raison |
+
+ {% for b in dossier.biologie_discarded %}
+ | {{ b.test or '' }} | {{ b.valeur or '' }} | {{ b.discard_reason or '—' }} |
+ {% endfor %}
+
+
+
+ {% endif %}
{% endif %}