feat: viewer — affichage qualité CPAM, traçabilité décisions DP/DAS, VetoReport et bio
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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'<span class="badge" style="background:{bg};color:{fg};font-size:0.7rem;">{label}</span>')
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@@ -131,9 +131,14 @@
|
||||
{% if dossier.controles_cpam %}
|
||||
<div class="card section" style="border-left:4px solid #f59e0b;">
|
||||
<h3 style="color:#b45309;">Contrôle CPAM ({{ dossier.controles_cpam|length }})</h3>
|
||||
{% if dossier_strength and dossier_strength.is_weak %}
|
||||
<div style="background:#fff7ed;border:1px solid #fed7aa;padding:0.5rem 0.75rem;border-radius:4px;margin-bottom:0.75rem;font-size:0.85rem;color:#9a3412;">
|
||||
Dossier à preuves limitées (score {{ dossier_strength.score }}/10) — manque : {{ dossier_strength.missing|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for ctrl in dossier.controles_cpam %}
|
||||
<div style="margin-bottom:1.5rem;{% if not loop.last %}border-bottom:1px solid #e2e8f0;padding-bottom:1rem;{% endif %}">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;flex-wrap:wrap;">
|
||||
<strong>OGC {{ ctrl.numero_ogc }} — {{ ctrl.titre }}</strong>
|
||||
{% if 'retient' in ctrl.decision_ucr|lower %}
|
||||
<span class="badge" style="background:#d1fae5;color:#065f46;">{{ ctrl.decision_ucr }}</span>
|
||||
@@ -142,6 +147,13 @@
|
||||
{% else %}
|
||||
<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ ctrl.decision_ucr }}</span>
|
||||
{% endif %}
|
||||
{% if ctrl.quality_tier == 'A' %}
|
||||
<span class="badge" style="background:#2ecc71;color:#fff;font-weight:700;">Qualité A</span>
|
||||
{% elif ctrl.quality_tier == 'B' %}
|
||||
<span class="badge" style="background:#f39c12;color:#fff;font-weight:700;">Qualité B</span>
|
||||
{% elif ctrl.quality_tier == 'C' %}
|
||||
<span class="badge" style="background:#e74c3c;color:#fff;font-weight:700;">Qualité C</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Argument CPAM #}
|
||||
@@ -165,6 +177,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Bandeau revue manuelle si Tier C #}
|
||||
{% if ctrl.requires_review %}
|
||||
<div style="background:#fee2e2;border:1px solid #fca5a5;padding:0.5rem 0.75rem;border-radius:4px;margin-bottom:0.75rem;font-size:0.85rem;color:#991b1b;">
|
||||
⚠ Revue manuelle requise — la contre-argumentation contient des incohérences détectées
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Contre-argumentation structurée ou fallback texte brut #}
|
||||
{% if ctrl.response_data %}
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
@@ -261,6 +280,22 @@
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{# Avertissements qualité #}
|
||||
{% if ctrl.quality_warnings %}
|
||||
<details style="margin-top:0.5rem;">
|
||||
<summary style="font-size:0.8rem;color:#9333ea;">Avertissements qualité ({{ ctrl.quality_warnings|length }})</summary>
|
||||
<ul style="margin:0.25rem 0;padding-left:1.2rem;">
|
||||
{% for w in ctrl.quality_warnings %}
|
||||
{% if w.startswith('[CRITIQUE]') %}
|
||||
<li style="color:#dc2626;font-size:0.8rem;">{{ w }}</li>
|
||||
{% else %}
|
||||
<li style="color:#d97706;font-size:0.8rem;">{{ w }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -282,17 +317,75 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
<div class="card section" style="border-left:4px solid {{ vr_color }};">
|
||||
<h3>Contestabilité du dossier</h3>
|
||||
<div style="display:flex;align-items:center;gap:1rem;">
|
||||
{% if vr.verdict == 'PASS' %}
|
||||
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;">PASS</span>
|
||||
{% elif vr.verdict == 'NEED_INFO' %}
|
||||
<span class="badge" style="background:#fef3c7;color:#92400e;font-weight:700;">NEED_INFO</span>
|
||||
{% else %}
|
||||
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;">FAIL</span>
|
||||
{% endif %}
|
||||
<div style="flex:1;height:8px;background:#e2e8f0;border-radius:4px;">
|
||||
<div style="width:{{ vr.score_contestabilite }}%;height:100%;background:{{ vr_color }};border-radius:4px;"></div>
|
||||
</div>
|
||||
<span style="font-weight:600;">{{ vr.score_contestabilite }}/100</span>
|
||||
</div>
|
||||
{% if vr.issues %}
|
||||
<details style="margin-top:0.5rem;">
|
||||
<summary style="font-size:0.8rem;color:#64748b;">Problèmes détectés ({{ vr.issues|length }})</summary>
|
||||
<table style="margin-top:0.25rem;">
|
||||
<thead><tr><th>Veto</th><th>Sévérité</th><th>Localisation</th><th>Message</th></tr></thead>
|
||||
<tbody>
|
||||
{% for issue in vr.issues %}
|
||||
<tr>
|
||||
<td><code style="font-size:0.75rem;">{{ issue.veto }}</code></td>
|
||||
<td>
|
||||
{% if issue.severity == 'HARD' %}<span class="badge" style="background:#fee2e2;color:#dc2626;">HARD</span>
|
||||
{% elif issue.severity == 'MEDIUM' %}<span class="badge" style="background:#fef3c7;color:#92400e;">MEDIUM</span>
|
||||
{% else %}<span class="badge" style="background:#f0fdf4;color:#166534;">LOW</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.75rem;color:#64748b;">{{ issue.where }}</td>
|
||||
<td style="font-size:0.8rem;">{{ issue.message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ---- Diagnostic principal ---- #}
|
||||
{% if dossier.diagnostic_principal %}
|
||||
{% set dp = dossier.diagnostic_principal %}
|
||||
<div class="card section">
|
||||
<div class="card section"{% if dp.status == 'ruled_out' %} style="opacity:0.5;"{% endif %}>
|
||||
<h3>Diagnostic principal</h3>
|
||||
<div style="font-size:0.95rem;margin-bottom:0.5rem;">
|
||||
{{ dp.texte }}
|
||||
{% if dp.status == 'ruled_out' %}<span style="text-decoration:line-through;">{{ dp.texte }}</span>{% else %}{{ dp.texte }}{% endif %}
|
||||
{% if dp.source_page %}<button class="src-btn" data-texte="{{ dp.texte|e }}" data-excerpt="{{ dp.source_excerpt|default('',true)|e }}" data-page="{{ dp.source_page }}">p.{{ dp.source_page }}</button>{% endif %}
|
||||
</div>
|
||||
{% if dp.cim10_suggestion %}
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
{% if dp.cim10_final and dp.cim10_final != dp.cim10_suggestion %}
|
||||
<span style="text-decoration:line-through;color:#94a3b8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
<span style="color:#64748b;">→</span>
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-size:0.85rem;">{{ dp.cim10_final }}</span>
|
||||
{% elif dp.status == 'ruled_out' %}
|
||||
<span style="text-decoration:line-through;color:#94a3b8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
{% else %}
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-size:0.85rem;">{{ dp.cim10_suggestion }}</span>
|
||||
{% 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 @@
|
||||
<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.75rem;">CMA</span>
|
||||
{% 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 %}
|
||||
<span class="badge" style="background:#f1f5f9;color:#64748b;font-size:0.65rem;">{{ rule }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if dp.status == 'ruled_out' and dp.ruled_out_reason %}
|
||||
<div style="font-size:0.75rem;color:#dc2626;margin-top:0.25rem;">{{ dp.ruled_out_reason }}</div>
|
||||
{% endif %}
|
||||
{% if dp.cim10_decision and dp.cim10_decision.action != 'KEEP' and dp.cim10_decision.reason %}
|
||||
<div style="font-size:0.75rem;color:#64748b;margin-top:0.25rem;">{{ dp.cim10_decision.reason }}</div>
|
||||
{% endif %}
|
||||
{% if dp.justification %}
|
||||
<div style="margin-top:0.5rem;font-size:0.8rem;color:#475569;">{{ dp.justification }}</div>
|
||||
@@ -340,9 +445,43 @@
|
||||
<thead><tr><th>Texte</th><th>CIM-10</th><th>Confiance</th><th>CMA</th><th>Source</th><th>Justification</th></tr></thead>
|
||||
<tbody>
|
||||
{% for das in dossier.diagnostics_associes %}
|
||||
<tr>
|
||||
<tr{% if das.status == 'ruled_out' %} style="opacity:0.5;text-decoration:line-through;"{% endif %}>
|
||||
<td>{{ das.texte }}</td>
|
||||
<td>{% if das.cim10_suggestion %}<span class="badge" style="background:#dbeafe;color:#1d4ed8;">{{ das.cim10_suggestion }}</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if das.cim10_suggestion %}
|
||||
{% if das.cim10_final and das.cim10_final != das.cim10_suggestion %}
|
||||
<span style="text-decoration:line-through;color:#94a3b8;font-size:0.8rem;">{{ das.cim10_suggestion }}</span>
|
||||
<span style="color:#64748b;">→</span>
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;">{{ das.cim10_final }}</span>
|
||||
{% elif das.status == 'ruled_out' %}
|
||||
<span style="color:#94a3b8;">{{ das.cim10_suggestion }}</span>
|
||||
{% else %}
|
||||
<span class="badge" style="background:#dbeafe;color:#1d4ed8;">{{ das.cim10_suggestion }}</span>
|
||||
{% endif %}
|
||||
{% if das.cim10_decision and das.cim10_decision.action != 'KEEP' %}
|
||||
<div style="margin-top:0.2rem;">
|
||||
{{ das.cim10_decision | decision_badge }}
|
||||
{% for rule in das.cim10_decision.applied_rules %}
|
||||
<span class="badge" style="background:#f1f5f9;color:#64748b;font-size:0.65rem;">{{ rule }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if das.status == 'needs_info' %}
|
||||
<div style="margin-top:0.2rem;">
|
||||
<span class="badge" style="background:#fff7ed;color:#c2410c;font-size:0.7rem;">Info requise</span>
|
||||
{% if das.cim10_decision and das.cim10_decision.needs_info %}
|
||||
<details style="margin-top:0.15rem;"><summary style="font-size:0.7rem;color:#c2410c;cursor:pointer;">détails</summary>
|
||||
<ul style="margin:0.1rem 0;padding-left:1rem;font-size:0.7rem;color:#9a3412;">
|
||||
{% for ni in das.cim10_decision.needs_info %}
|
||||
<li>{{ ni }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ das.cim10_confidence | confidence_badge }}</td>
|
||||
<td>
|
||||
{% if das.niveau_cma and das.niveau_cma > 1 %}
|
||||
@@ -374,6 +513,20 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if das.status == 'ruled_out' and das.ruled_out_reason %}
|
||||
<tr>
|
||||
<td colspan="6" style="padding:0 0.75rem 0.3rem;">
|
||||
<div style="font-size:0.75rem;color:#dc2626;">{{ das.ruled_out_reason }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if das.cim10_decision and das.cim10_decision.action != 'KEEP' and das.cim10_decision.reason and das.status != 'ruled_out' %}
|
||||
<tr>
|
||||
<td colspan="6" style="padding:0 0.75rem 0.3rem;">
|
||||
<div style="font-size:0.75rem;color:#64748b;">{{ das.cim10_decision.reason }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if das.raisonnement %}
|
||||
<tr>
|
||||
<td colspan="6" style="padding:0 0.75rem 0.5rem;">
|
||||
@@ -446,15 +599,36 @@
|
||||
<thead><tr><th>Test</th><th>Valeur</th><th>Anomalie</th><th>Source</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in dossier.biologie_cle %}
|
||||
<tr{% if b.anomalie %} class="anomalie"{% endif %}>
|
||||
<tr{% if b.quality == 'suspect' %} style="background:#fffbeb;"{% elif b.anomalie %} class="anomalie"{% endif %}>
|
||||
<td>{{ b.test }}</td>
|
||||
<td>{{ b.valeur or '' }}</td>
|
||||
<td>{% if b.anomalie %}<span class="badge" style="background:#fee2e2;color:#dc2626;">Oui</span>{% else %}—{% endif %}</td>
|
||||
<td>
|
||||
{% if b.quality == 'suspect' %}
|
||||
<span class="badge" style="background:#fef3c7;color:#92400e;" title="{{ b.discard_reason or '' }}">⚠ Suspect</span>
|
||||
{% elif b.anomalie %}
|
||||
<span class="badge" style="background:#fee2e2;color:#dc2626;">Oui</span>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if b.source_page %}<button class="src-btn" data-texte="{{ b.test|e }}" data-excerpt="{{ b.source_excerpt|default('',true)|e }}" data-page="{{ b.source_page }}">p.{{ b.source_page }}</button>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if dossier.biologie_discarded %}
|
||||
<details style="margin-top:0.5rem;">
|
||||
<summary style="font-size:0.8rem;color:#d97706;">Valeurs écartées ({{ dossier.biologie_discarded|length }})</summary>
|
||||
<table style="margin-top:0.25rem;">
|
||||
<thead><tr><th>Test</th><th>Valeur</th><th>Raison</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in dossier.biologie_discarded %}
|
||||
<tr style="opacity:0.6;"><td>{{ b.test or '' }}</td><td>{{ b.valeur or '' }}</td><td>{{ b.discard_reason or '—' }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user