Files
t2a_v2/src/viewer/templates/detail.html
dom 0c38bc261b chore: sauvegarde état courant avant merge des branches teammates
Modifications en cours : pipeline médical (cim10_extractor, dp_finalizer,
dp_selector, fusion, rag_search), viewer (helpers, detail.html),
cache ollama et référentiels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:36:54 +01:00

1194 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ dossier.source_file or filepath }}{% endblock %}
{% block content %}
<a class="back" href="/">&larr; Retour à la liste</a>
{# ==================================================================== #}
{# 1. BANDEAU PATIENT — Identité + Séjour + Codage DP + GHM + Score #}
{# ==================================================================== #}
{% set s = dossier.sejour %}
{% set dp = dossier.diagnostic_principal %}
{% set ghm = dossier.ghm_estimation %}
{% set vr = dossier.veto_report %}
<div class="card" style="margin-top:1rem;padding:1.25rem 1.5rem;">
{# Titre patient + identifiants #}
<div style="display:flex;align-items:baseline;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap;">
<h2 style="margin:0;">{{ current_group | format_dossier_name if current_group else (dossier.source_file or filepath) }}</h2>
<span class="badge" style="background:#e0e7ff;color:#3730a3;font-size:0.75rem;font-weight:600;" title="Identifiant du dossier dans le système">N° {{ current_group | format_dossier_name if current_group else filepath }}</span>
{% if dossier.controles_cpam %}
{% for ctrl in dossier.controles_cpam %}
<span class="badge" style="background:#fef3c7;color:#92400e;font-weight:700;font-size:0.8rem;" title="Numéro OGC (Ordonnance de Gestion de Caisse) du contrôle CPAM">OGC {{ ctrl.numero_ogc }}</span>
{% endfor %}
{% endif %}
{% if dossier.document_type %}
<span class="badge" style="background:#e0e7ff;color:#3730a3;" title="Type de document source (CRH, lettre de sortie, compte-rendu opératoire, etc.)">{{ dossier.document_type }}</span>
{% endif %}
{% if dossier.source_files %}
<span class="badge" style="background:#ede9fe;color:#5b21b6;" title="Ce dossier est le résultat de la fusion de plusieurs documents sources">fusionné</span>
{% endif %}
{% if dossier.processing_time_s is not none %}
<span style="font-size:0.75rem;color:#94a3b8;">{{ dossier.processing_time_s|format_duration }}</span>
{% endif %}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;">
{# Colonne gauche — Identité et séjour #}
<div>
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:0.5rem;">Patient & Séjour</div>
<div class="info-grid" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem;">
{% if s.sexe %}<div class="info-item"><label>Sexe</label><span>{{ s.sexe }}</span></div>{% endif %}
{% if s.age is not none %}<div class="info-item"><label>Âge</label><span>{{ s.age }} ans</span></div>{% endif %}
{% if s.date_entree %}<div class="info-item"><label>Entrée</label><span>{{ s.date_entree }}</span></div>{% endif %}
{% if s.date_sortie %}<div class="info-item"><label>Sortie</label><span>{{ s.date_sortie }}</span></div>{% endif %}
{% if s.duree_sejour is not none %}<div class="info-item"><label>Durée</label><span>{{ s.duree_sejour }} j</span></div>{% endif %}
{% if s.mode_entree %}<div class="info-item"><label>Mode entrée</label><span>{{ s.mode_entree }}</span></div>{% endif %}
{% if s.mode_sortie %}<div class="info-item"><label>Mode sortie</label><span>{{ s.mode_sortie }}</span></div>{% endif %}
{% if s.poids %}<div class="info-item"><label>Poids</label><span>{{ s.poids }} kg</span></div>{% endif %}
{% if s.taille %}<div class="info-item"><label>Taille</label><span>{{ s.taille }} cm</span></div>{% endif %}
{% if s.imc %}<div class="info-item"><label>IMC</label><span>{{ s.imc }}</span></div>{% endif %}
</div>
</div>
{# Colonne droite — Codage #}
<div>
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:0.5rem;">Codage</div>
{# DP en gros #}
{% if dp %}
<div style="margin-bottom:0.75rem;">
<div style="font-size:0.7rem;color:#94a3b8;font-weight:600;">DIAGNOSTIC PRINCIPAL</div>
<div style="margin-top:0.2rem;display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
{% if dp.cim10_suggestion %}
{% if dp.cim10_final and dp.cim10_final != dp.cim10_suggestion %}
<span style="text-decoration:line-through;color:#94a3b8;font-size:1rem;">{{ dp.cim10_suggestion }}</span>
<span style="color:#64748b;"></span>
<span style="font-size:1.3rem;font-weight:700;color:#1d4ed8;">{{ dp.cim10_final }}</span>
{% else %}
<span style="font-size:1.3rem;font-weight:700;color:#1d4ed8;">{{ dp.cim10_suggestion }}</span>
{% endif %}
{{ dp.cim10_confidence | confidence_badge }}
{% endif %}
{% if dp.niveau_cma and dp.niveau_cma > 1 %}{{ dp.niveau_cma | cma_level_badge }}{% endif %}
{{ dp.niveau_severite | severity_badge }}
</div>
<div style="font-size:0.85rem;color:#334155;margin-top:0.15rem;">
{% 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>
</div>
{% endif %}
{# GHM estimé #}
{% if ghm %}
<div style="margin-bottom:0.75rem;">
<div style="font-size:0.7rem;color:#94a3b8;font-weight:600;">GHM ESTIMÉ</div>
<div style="margin-top:0.2rem;display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
{% if ghm.ghm_approx %}
<code style="font-size:1.1rem;font-weight:700;letter-spacing:0.05em;">{{ ghm.ghm_approx }}</code>
{% endif %}
{% if ghm.type_ghm == 'C' %}
<span class="badge" style="background:#fee2e2;color:#dc2626;" title="GHM Chirurgical — séjour avec acte opératoire classant">C — Chirurgical</span>
{% elif ghm.type_ghm == 'K' %}
<span class="badge" style="background:#fef3c7;color:#92400e;" title="GHM Interventionnel — séjour avec acte non chirurgical classant (endoscopie, cathétérisme, etc.)">K — Interventionnel</span>
{% elif ghm.type_ghm == 'M' %}
<span class="badge" style="background:#dbeafe;color:#1d4ed8;" title="GHM Médical — séjour sans acte classant">M — Médical</span>
{% endif %}
{% if ghm.severite <= 1 %}
<span class="badge" style="background:#d1fae5;color:#065f46;" title="Sévérité 1 — sans complication ni morbidité associée significative">Niv. {{ ghm.severite }}</span>
{% elif ghm.severite == 2 %}
<span class="badge" style="background:#fef3c7;color:#92400e;" title="Sévérité 2 — complication ou morbidité associée mineure">Niv. {{ ghm.severite }}</span>
{% elif ghm.severite == 3 %}
<span class="badge" style="background:#fed7aa;color:#9a3412;" title="Sévérité 3 — complication ou morbidité associée majeure">Niv. {{ ghm.severite }}</span>
{% else %}
<span class="badge" style="background:#fee2e2;color:#dc2626;" title="Sévérité 4 — complication ou morbidité associée très sévère (réanimation, décès, etc.)">Niv. {{ ghm.severite }}</span>
{% endif %}
<span style="font-size:0.75rem;color:#64748b;" title="CMA = Comorbidités/Morbidités Associées (augmentent la sévérité), CMS = Comorbidités/Morbidités Sans effet sur la sévérité">{{ ghm.cma_count }} CMA, {{ ghm.cms_count }} CMS</span>
</div>
</div>
{% endif %}
{# Score de contestabilité #}
{% if vr %}
<div>
<div style="font-size:0.7rem;color:#94a3b8;font-weight:600;">CONTESTABILITÉ</div>
{% if vr.verdict == 'PASS' %}{% set vr_color = '#22c55e' %}
{% elif vr.verdict == 'NEED_INFO' %}{% set vr_color = '#f59e0b' %}
{% else %}{% set vr_color = '#ef4444' %}{% endif %}
<div style="margin-top:0.2rem;display:flex;align-items:center;gap:0.5rem;">
{% if vr.verdict == 'PASS' %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;" title="Le codage respecte les règles ATIH — défendable en cas de contrôle externe">CONFORME</span>
{% elif vr.verdict == 'NEED_INFO' %}
<span class="badge" style="background:#fef3c7;color:#92400e;font-weight:700;" title="Le codage nécessite des informations complémentaires pour être pleinement validé">À COMPLÉTER</span>
{% else %}
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;" title="Le codage enfreint au moins une règle ATIH — risque de rejet ou pénalité en cas de contrôle">NON CONFORME</span>
{% endif %}
<div style="flex:1;height:6px;background:#e2e8f0;border-radius:3px;max-width:120px;" title="Barre de score de défendabilité (0=indéfendable, 100=parfaitement défendable)">
<div style="width:{{ vr.score_contestabilite }}%;height:100%;background:{{ vr_color }};border-radius:3px;"></div>
</div>
<span style="font-size:0.8rem;font-weight:600;" title="Score de défendabilité du codage face à un contrôle externe (CPAM, ARS)">{{ vr.score_contestabilite }}/100</span>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{# Actions (compact, sous le bandeau) #}
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;margin-top:0.5rem;padding:0 0.25rem;">
<button id="btn-reprocess" onclick="reprocessDossier()" style="padding:0.3rem 0.75rem;border-radius:6px;border:1px solid #cbd5e1;background:#fff;color:#475569;font-size:0.75rem;font-weight:600;cursor:pointer;transition:all 0.15s;">&#8635; Retraiter</button>
<span id="reprocess-status" style="font-size:0.7rem;color:#64748b;"></span>
<label style="display:inline-flex;align-items:center;gap:0.35rem;font-size:0.75rem;color:#475569;cursor:pointer;padding:0.3rem 0.75rem;border-radius:6px;border:1px solid #cbd5e1;background:#fff;">
&#128206; Ajouter un PDF
<input type="file" id="upload-file" accept=".pdf" style="display:none;" onchange="uploadDocument()">
</label>
<span id="upload-status" style="font-size:0.7rem;color:#64748b;"></span>
</div>
{# ==================================================================== #}
{# 1b. COMPLÉTUDE DOCUMENTAIRE DIM #}
{# ==================================================================== #}
{% set compl = dossier.completude %}
{% if compl and compl.checks %}
<div class="card section" style="margin-top:1rem;">
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem;flex-wrap:wrap;">
<h3 style="margin:0;">Complétude Documentaire DIM</h3>
{% if compl.verdict_global == 'defendable' %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;" title="Dossier complet — toutes les pièces justificatives sont présentes pour étayer le codage en cas de contrôle">DÉFENDABLE</span>
{% set compl_color = '#22c55e' %}
{% elif compl.verdict_global == 'fragile' %}
<span class="badge" style="background:#fef3c7;color:#92400e;font-weight:700;" title="Dossier incomplet — certaines pièces justificatives manquent, le codage pourrait être contesté">FRAGILE</span>
{% set compl_color = '#f59e0b' %}
{% else %}
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;" title="Dossier très incomplet — pièces essentielles absentes, le codage est indéfendable en cas de contrôle CPAM">INDÉFENDABLE</span>
{% set compl_color = '#ef4444' %}
{% endif %}
<div style="display:flex;align-items:center;gap:0.5rem;">
<div style="flex:none;width:100px;height:6px;background:#e2e8f0;border-radius:3px;">
<div style="width:{{ compl.score_global }}%;height:100%;background:{{ compl_color }};border-radius:3px;"></div>
</div>
<span style="font-size:0.8rem;font-weight:600;">{{ compl.score_global }}/100</span>
</div>
</div>
{% if compl.documents_manquants %}
<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:0.5rem 0.75rem;margin-bottom:1rem;font-size:0.8rem;">
<strong style="color:#dc2626;">Documents manquants :</strong> {{ compl.documents_manquants | join(', ') }}
</div>
{% endif %}
{% for check in compl.checks %}
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:0.75rem 1rem;margin-bottom:0.5rem;{% if check.verdict == 'indefendable' %}background:#fef2f2;border-color:#fecaca;{% elif check.verdict == 'fragile' %}background:#fffbeb;border-color:#fed7aa;{% else %}background:#f0fdf4;border-color:#bbf7d0;{% endif %}">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;flex-wrap:wrap;">
<span class="badge" style="background:#dbeafe;color:#1d4ed8;font-weight:700;">{{ check.code }}</span>
<span style="font-size:0.85rem;font-weight:600;color:#334155;">{{ check.libelle | truncate(60) }}</span>
<span class="badge" style="background:#f1f5f9;color:#64748b;font-size:0.65rem;">{{ check.type_diag }}</span>
{% if check.verdict == 'defendable' %}
<span style="font-size:0.75rem;color:#16a34a;font-weight:600;">&#10003; {{ check.resume }}</span>
{% elif check.verdict == 'fragile' %}
<span style="font-size:0.75rem;color:#d97706;font-weight:600;">&#9888; {{ check.resume }}</span>
{% else %}
<span style="font-size:0.75rem;color:#dc2626;font-weight:600;">&#10007; {{ check.resume }}</span>
{% endif %}
</div>
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;">
{% for item in check.items %}
{% if item.statut == 'present_confirme' %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#065f46;color:#d1fae5;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.confirmation_detail or item.impact_cpam }}">
&#10003; {{ item.element }}{% if item.valeur %} <span style="color:#bbf7d0;font-weight:600;">({{ item.valeur | truncate(20) }})</span>{% endif %}
{% if item.confirmation_detail %}<span style="font-size:0.6rem;opacity:0.85;"> — confirmé</span>{% endif %}
</span>
{% elif item.statut == 'present_non_confirme' %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#fff7ed;color:#c2410c;border:1px solid #fed7aa;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.confirmation_detail or item.impact_cpam }}">
&#9888; {{ item.element }}{% if item.valeur %} <span style="font-weight:600;">({{ item.valeur | truncate(20) }})</span>{% endif %}
{% if item.confirmation_detail %}<span style="font-size:0.6rem;"> — seuil non atteint</span>{% endif %}
</span>
{% elif item.statut == 'present_indirect' %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#dbeafe;color:#1e40af;border:1px solid #93c5fd;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.confirmation_detail or item.impact_cpam }}">
&#126; {{ item.element }}{% if item.valeur %} <span style="font-weight:600;">({{ item.valeur | truncate(25) }})</span>{% endif %}
<span style="font-size:0.6rem;"> — preuve clinique</span>
</span>
{% elif item.statut == 'present' %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#d1fae5;color:#065f46;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.impact_cpam }}">
&#10003; {{ item.element }}{% if item.valeur %} <span style="color:#047857;font-weight:600;">({{ item.valeur | truncate(20) }})</span>{% endif %}
</span>
{% elif item.importance == 'obligatoire' %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#fee2e2;color:#dc2626;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.impact_cpam }}">
&#10007; {{ item.element }} <span style="font-size:0.65rem;font-weight:600;">(obligatoire)</span>
</span>
{% else %}
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:#fef3c7;color:#92400e;border-radius:4px;padding:0.15rem 0.5rem;font-size:0.75rem;" title="{{ item.impact_cpam }}">
&#8212; {{ item.element }} <span style="font-size:0.65rem;">(recommandé)</span>
</span>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
{% if compl.documents_presents %}
<div style="margin-top:0.5rem;font-size:0.75rem;color:#64748b;">
Documents présents : {{ compl.documents_presents | join(', ') }}
</div>
{% endif %}
</div>
{% endif %}
{# ==================================================================== #}
{# 2. DAS + ACTES CCAM #}
{# ==================================================================== #}
{# ---- Diagnostic principal (détail) ---- #}
{% set dp_has_detail = dp and (dp.justification or dp.preuves_cliniques or dp.raisonnement or dp.sources_rag or (dp.cim10_decision and dp.cim10_decision.action != 'KEEP') or dp.status == 'ruled_out') %}
{% if dp_has_detail %}
<div class="card section" style="margin-top:1rem;{% if dp.status == 'ruled_out' %}opacity:0.5;{% endif %}">
<h3>Diagnostic principal — Détail</h3>
{% if dp.status == 'ruled_out' and dp.ruled_out_reason %}
<div style="font-size:0.75rem;color:#dc2626;margin-bottom:0.25rem;">{{ dp.ruled_out_reason }}</div>
{% endif %}
{% if dp.cim10_decision and dp.cim10_decision.action != 'KEEP' %}
<div style="margin-bottom:0.25rem;">
{{ 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 %}
{% if dp.cim10_decision.reason %}
<div style="font-size:0.75rem;color:#64748b;margin-top:0.15rem;">{{ dp.cim10_decision.reason }}</div>
{% endif %}
</div>
{% endif %}
{% if dp.justification %}
<div style="font-size:0.8rem;color:#475569;margin-bottom:0.5rem;">{{ dp.justification }}</div>
{% endif %}
{% if dp.preuves_cliniques %}
<details>
<summary style="font-size:0.8rem;color:#0369a1;cursor:pointer;font-weight:600;">Preuves cliniques ({{ dp.preuves_cliniques|length }})</summary>
<ul style="margin:0.25rem 0 0 0;padding-left:1.2rem;font-size:0.8rem;">
{% for p in dp.preuves_cliniques %}
<li style="margin-bottom:0.15rem;"><span class="badge" style="background:#e0f2fe;color:#0369a1;font-size:0.7rem;">{{ p.type }}</span> {{ p.element }} <span style="color:#64748b;">&rarr; {{ p.interpretation }}</span></li>
{% endfor %}
</ul>
</details>
{% endif %}
{% if dp.raisonnement %}
<details style="margin-top:0.5rem;">
<summary>Raisonnement LLM</summary>
<pre>{{ dp.raisonnement }}</pre>
</details>
{% endif %}
{% if dp.sources_rag %}
<details>
<summary>Sources RAG ({{ dp.sources_rag|length }})</summary>
{% for src in dp.sources_rag %}
<pre>{{ src.document }}{% if src.code %} — {{ src.code }}{% endif %}{% if src.page %} [p.{{ src.page }}]{% endif %}
{{ src.extrait or '' }}</pre>
{% endfor %}
</details>
{% endif %}
</div>
{% elif dp and not dp_has_detail and dp.source == 'trackare' %}
<div class="card section" style="margin-top:1rem;">
<h3>Diagnostic principal — Détail</h3>
<div style="font-size:0.8rem;color:#64748b;font-style:italic;">Codage issu de Trackare — pas de détail IA disponible.</div>
</div>
{% endif %}
{# ---- Diagnostics associés ---- #}
{% if dossier.diagnostics_associes %}
<details class="card section">
<summary><h3 style="display:inline;">Diagnostics associés ({{ dossier.diagnostics_associes|length }})</h3></summary>
<table>
<thead><tr><th title="Code CIM-10 attribué au diagnostic associé">Code CIM-10</th><th>Libellé</th><th title="Comorbidité/Morbidité Associée — indique si ce diagnostic augmente la sévérité GHM">CMA</th><th title="Niveau de confiance du pipeline IA sur ce code CIM-10">Confiance</th><th title="Source du diagnostic dans le document (page)">Source</th></tr></thead>
<tbody>
{% for das in dossier.diagnostics_associes %}
<tr{% if das.status == 'ruled_out' %} style="opacity:0.5;text-decoration:line-through;"{% endif %}>
<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;" title="Ce diagnostic nécessite des informations complémentaires pour être pleinement validé">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.texte }}
{% if das.justification %}
<div style="font-size:0.75rem;color:#64748b;margin-top:0.15rem;">{{ das.justification }}</div>
{% endif %}
{% if das.preuves_cliniques %}
<details style="margin-top:0.3rem;"><summary style="font-size:0.7rem;color:#0369a1;cursor:pointer;">preuves ({{ das.preuves_cliniques|length }})</summary>
<ul style="margin:0.15rem 0 0 0;padding-left:1rem;font-size:0.75rem;">
{% for p in das.preuves_cliniques %}
<li><span style="font-weight:600;{% if p.type == 'biologie' %}color:#0891b2;{% else %}color:#0369a1;{% endif %}">[{{ p.type }}]</span> {{ p.element }} <span style="color:#64748b;">&rarr; {{ p.interpretation }}</span></li>
{% endfor %}
</ul>
</details>
{% endif %}
</td>
<td>
{% if das.niveau_cma and das.niveau_cma > 1 %}
{{ das.niveau_cma | cma_level_badge }}
{% elif das.est_cma %}
<span class="badge" style="background:#fee2e2;color:#dc2626;font-size:0.7rem;" title="Comorbidité/Morbidité Associée — ce diagnostic augmente le niveau de sévérité du GHM">CMA</span>
{% else %}
{% endif %}
</td>
<td>{{ das.cim10_confidence | confidence_badge }}</td>
<td>
{% if das.source %}
<span class="badge" style="background:#e0e7ff;color:#3730a3;font-size:0.7rem;">{{ das.source }}</span>
{% endif %}
{% if das.source_page %}
<button class="src-btn" data-texte="{{ das.texte|e }}" data-excerpt="{{ das.source_excerpt|default('',true)|e }}" data-page="{{ das.source_page }}">p.{{ das.source_page }}</button>
{% endif %}
</td>
</tr>
{% if das.status == 'ruled_out' and das.ruled_out_reason %}
<tr><td colspan="5" 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="5" 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="5" style="padding:0 0.75rem 0.5rem;"><details><summary>Raisonnement LLM</summary><pre>{{ das.raisonnement }}</pre></details></td></tr>
{% endif %}
{% if das.sources_rag %}
<tr><td colspan="5" style="padding:0 0.75rem 0.5rem;">
<details><summary>Sources RAG ({{ das.sources_rag|length }})</summary>
{% for src in das.sources_rag %}<pre>{{ src.document }}{% if src.code %} — {{ src.code }}{% endif %}{% if src.page %} [p.{{ src.page }}]{% endif %}
{{ src.extrait or '' }}</pre>{% endfor %}
</details>
</td></tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{# ---- Actes CCAM ---- #}
{% if dossier.actes_ccam %}
<div class="card section">
<h3>Actes CCAM ({{ dossier.actes_ccam|length }})</h3>
<table>
<thead><tr><th title="Code de la Classification Commune des Actes Médicaux">Code CCAM</th><th>Libellé</th><th title="Code de regroupement GHM — détermine le classement du séjour en GHM chirurgical ou interventionnel">Regroupement</th><th>Date</th><th title="Validité du code CCAM dans la nomenclature en vigueur">Validité</th><th>Source</th></tr></thead>
<tbody>
{% for a in dossier.actes_ccam %}
<tr>
<td>{% if a.code_ccam_suggestion %}<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ a.code_ccam_suggestion }}</span>{% endif %}</td>
<td>{{ a.texte }}</td>
<td>
{% if a.code_ccam_suggestion and ccam_dict.get(a.code_ccam_suggestion, {}).get('regroupement') %}
<span class="badge badge-regroup">{{ ccam_dict[a.code_ccam_suggestion]['regroupement'] }}</span>
{% else %}—{% endif %}
</td>
<td>{{ a.date or '' }}</td>
<td>
{% if a.validite == 'valide' %}<span class="badge" style="background:#d1fae5;color:#065f46;" title="Code CCAM en vigueur dans la nomenclature actuelle">Valide</span>
{% elif a.validite == 'obsolete' %}<span class="badge" style="background:#fee2e2;color:#dc2626;" title="Code CCAM obsolète — à remplacer par le code en vigueur">Obsolète</span>
{% else %}—{% endif %}
{% for alerte in a.alertes %}<div style="font-size:0.7rem;color:#dc2626;">{{ alerte }}</div>{% endfor %}
</td>
<td>{% if a.source_page %}<button class="src-btn" data-texte="{{ a.texte|e }}" data-excerpt="{{ a.source_excerpt|default('',true)|e }}" data-page="{{ a.source_page }}">p.{{ a.source_page }}</button>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{# ==================================================================== #}
{# 3. CONTRÔLE QUALITÉ CODAGE (section repliable) #}
{# ==================================================================== #}
<details class="card section" style="margin-top:1rem;">
<summary><h3 style="display:inline;">Contrôle Qualité Codage</h3></summary>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1rem;margin-top:0.75rem;">
{# Anomalies de codage #}
<div style="border-top:4px solid #ef4444;background:#fff1f2;border-radius:8px;padding:1rem;">
<h3 style="color:#991b1b;font-size:0.9rem;margin-bottom:0.75rem;">Anomalies de codage</h3>
<div style="font-size:0.85rem;color:#7f1d1d;">
{% if dossier.veto_report and dossier.veto_report.issues %}
{% for issue in dossier.veto_report.issues if issue.severity in ['HARD', 'MEDIUM'] %}
<div style="margin-bottom:0.5rem;border-bottom:1px solid #fecaca;padding-bottom:0.25rem;">
<strong>[{{ issue.severity|replace('HARD', 'Bloquant')|replace('MEDIUM', 'À vérifier') }}]</strong> {{ issue.message }}
{% if issue.citation %}<br><em style="font-size:0.75rem;opacity:0.8;">ATIH: {{ issue.citation }}</em>{% endif %}
</div>
{% endfor %}
{% else %}
<div style="color:#059669;font-weight:600;">Aucune anomalie majeure détectée.</div>
{% endif %}
</div>
</div>
{# Valorisation CMA #}
<div style="border-top:4px solid #10b981;background:#ecfdf5;border-radius:8px;padding:1rem;">
<h3 style="color:#065f46;font-size:0.9rem;margin-bottom:0.75rem;">Valorisation (CMA)</h3>
<div style="font-size:0.85rem;color:#064e3b;">
{% set cma_alerts = [] %}
{% for alerte in dossier.alertes_codage if alerte.startswith('CMA') %}{% set _ = cma_alerts.append(alerte) %}{% endfor %}
{% if cma_alerts %}
<ul style="margin:0;padding-left:1.2rem;">
{% for alerte in cma_alerts %}<li style="margin-bottom:0.25rem;">{{ alerte }}</li>{% endfor %}
</ul>
{% else %}
<div style="opacity:0.7;">Aucune comorbidité (CMA) détectée.</div>
{% endif %}
</div>
</div>
{# Audit IA (QC) #}
<div style="border-top:4px solid #3b82f6;background:#eff6ff;border-radius:8px;padding:1rem;">
<h3 style="color:#1e40af;font-size:0.9rem;margin-bottom:0.75rem;">Audit de l'Expert IA</h3>
<div style="font-size:0.85rem;color:#1e3a8a;">
{% set qc_alerts = [] %}
{% for alerte in dossier.alertes_codage if alerte.startswith('QC:') %}{% set _ = qc_alerts.append(alerte) %}{% endfor %}
{% if qc_alerts %}
{% for alerte in qc_alerts | sort_qc_alerts %}
<div style="margin-bottom:0.5rem;border-bottom:1px solid #bfdbfe;padding-bottom:0.25rem;font-style:italic;">
{{ alerte|replace('QC: ', '') }}
</div>
{% endfor %}
{% else %}
<div style="opacity:0.7;">Aucune recommandation particulière.</div>
{% endif %}
</div>
</div>
</div>
{# Détail Contestabilité (VetoReport) #}
{% if vr and vr.issues %}
<details style="margin-top:0.75rem;">
<summary style="font-size:0.8rem;color:#64748b;cursor:pointer;">Détail des contrôles qualité ({{ vr.issues|length }})</summary>
<table style="margin-top:0.25rem;">
<thead><tr><th>Règle</th><th>Sévérité</th><th>Localisation</th><th>Message d'alerte</th><th>Source / Référence</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;" title="Anomalie bloquante — enfreint une règle ATIH, le codage est rejeté si non corrigé">Bloquant</span>
{% elif issue.severity == 'MEDIUM' %}<span class="badge" style="background:#fef3c7;color:#92400e;" title="Point à vérifier — possible incohérence nécessitant une relecture médicale">À vérifier</span>
{% else %}<span class="badge" style="background:#f0fdf4;color:#166534;" title="Suggestion d'optimisation — amélioration possible du codage sans impact bloquant">Optimisation</span>{% endif %}
</td>
<td style="font-size:0.75rem;color:#64748b;">{{ issue.where|human_where }}</td>
<td style="font-size:0.8rem;">{{ issue.message }}</td>
<td style="font-size:0.75rem;color:#475569;font-style:italic;">{{ issue.citation or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{# GHM alertes #}
{% if ghm and ghm.alertes %}
<div style="margin-top:0.75rem;">
{% for alerte in ghm.alertes %}
<div style="font-size:0.8rem;color:#c2410c;margin-bottom:0.2rem;">{{ alerte }}</div>
{% endfor %}
</div>
{% endif %}
</details>
{# ==================================================================== #}
{# 4. CONTRÔLE CPAM (si applicable) #}
{# ==================================================================== #}
{% if dossier.controles_cpam %}
<div class="card section" style="border-left:4px solid #f59e0b;">
<h3 style="color:#b45309;">Contrôle UCR ({{ 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;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>
{% elif 'confirme' in ctrl.decision_ucr|lower %}
<span class="badge" style="background:#fee2e2;color:#dc2626;">{{ ctrl.decision_ucr }}</span>
{% 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;" title="Qualité A — contre-argumentation solide, bien documentée, preuves convergentes">Qualité A</span>
{% elif ctrl.quality_tier == 'B' %}
<span class="badge" style="background:#f39c12;color:#fff;font-weight:700;" title="Qualité B — contre-argumentation acceptable mais certains points pourraient être renforcés">Qualité B</span>
{% elif ctrl.quality_tier == 'C' %}
<span class="badge" style="background:#e74c3c;color:#fff;font-weight:700;" title="Qualité C — contre-argumentation faible, preuves insuffisantes, révision recommandée">Qualité C</span>
{% endif %}
</div>
{% if ctrl.arg_ucr %}
<div style="border-left:3px solid #f59e0b;padding:0.5rem 0.75rem;background:#fffbeb;margin-bottom:0.75rem;font-size:0.85rem;color:#78350f;">
<div style="font-size:0.7rem;color:#92400e;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Argument UCR</div>
{{ ctrl.arg_ucr }}
</div>
{% endif %}
{% if ctrl.dp_ucr or ctrl.da_ucr or ctrl.dr_ucr or ctrl.actes_ucr %}
<div style="margin-bottom:0.75rem;">
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Codes contestés</div>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
{% if ctrl.dp_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;">DP: {{ ctrl.dp_ucr }}</span>{% endif %}
{% if ctrl.da_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;">DA: {{ ctrl.da_ucr }}</span>{% endif %}
{% if ctrl.dr_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;">DR: {{ ctrl.dr_ucr }}</span>{% endif %}
{% if ctrl.actes_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;">Actes: {{ ctrl.actes_ucr }}</span>{% endif %}
</div>
</div>
{% endif %}
{% 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 %}
{% if ctrl.response_data %}
<div style="margin-bottom:0.75rem;">
<div style="font-size:0.7rem;color:#1d4ed8;text-transform:uppercase;font-weight:600;margin-bottom:0.5rem;">Contre-argumentation</div>
{% if ctrl.response_data.analyse_contestation %}
<div style="border-left:3px solid #94a3b8;padding:0.5rem 0.75rem;background:#f8fafc;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Analyse de la contestation</div>
{{ ctrl.response_data.analyse_contestation | format_cpam_text }}
</div>
{% endif %}
{% if ctrl.response_data.points_accord and ctrl.response_data.points_accord|lower not in ['aucun', 'non applicable', 'n/a', ''] %}
<div style="border-left:3px solid #22c55e;padding:0.5rem 0.75rem;background:#f0fdf4;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#16a34a;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Points d'accord</div>
{{ ctrl.response_data.points_accord | format_cpam_text }}
</div>
{% endif %}
{% if ctrl.response_data.contre_arguments_medicaux %}
<div style="border-left:3px solid #3b82f6;padding:0.5rem 0.75rem;background:#eff6ff;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#1d4ed8;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Contre-arguments médicaux</div>
{{ ctrl.response_data.contre_arguments_medicaux | format_cpam_text }}
</div>
{% endif %}
{% if ctrl.response_data.preuves_dossier %}
<div style="border-left:3px solid #0ea5e9;padding:0.5rem 0.75rem;background:#f0f9ff;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#0369a1;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Preuves du dossier</div>
<ul style="margin:0.3rem 0;padding-left:1.2rem;">
{% for p in ctrl.response_data.preuves_dossier %}
{% if p is mapping %}
<li style="margin-bottom:0.3rem;">
<span style="display:inline-block;padding:1px 6px;border-radius:9999px;font-size:0.7rem;font-weight:600;background:#e0f2fe;color:#0369a1;">{{ p.element or p.get('type', '') }}</span>
{{ p.valeur or '' }} <span style="color:#64748b;">&rarr; {{ p.signification or '' }}</span>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% if ctrl.response_data.contre_arguments_asymetrie %}
<div style="border-left:3px solid #8b5cf6;padding:0.5rem 0.75rem;background:#f5f3ff;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#7c3aed;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Asymétrie d'information</div>
{{ ctrl.response_data.contre_arguments_asymetrie | format_cpam_text }}
</div>
{% endif %}
{% if ctrl.response_data.contre_arguments_reglementaires %}
<div style="border-left:3px solid #6366f1;padding:0.5rem 0.75rem;background:#eef2ff;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#4f46e5;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Contre-arguments réglementaires</div>
{{ ctrl.response_data.contre_arguments_reglementaires | format_cpam_text }}
</div>
{% endif %}
{% if ctrl.response_data.references %}
<div style="border-left:3px solid #64748b;padding:0.5rem 0.75rem;background:#f8fafc;margin-bottom:0.5rem;font-size:0.85rem;">
<div style="font-size:0.7rem;color:#475569;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Références</div>
{% for ref in ctrl.response_data.references %}
{% if ref is mapping %}
<blockquote style="margin:0.3rem 0;padding:0.3rem 0.5rem;border-left:2px solid #cbd5e1;background:#f1f5f9;font-size:0.8rem;color:#334155;">
<strong>[{{ ref.document or '' }}{% if ref.page %}, p.{{ ref.page }}{% endif %}]</strong>
{{ ref.citation or '' }}
</blockquote>
{% elif ref is string %}
<p style="margin:0.2rem 0;font-size:0.8rem;color:#334155;">{{ ref }}</p>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if ctrl.response_data.conclusion %}
<div style="border-left:3px solid #f59e0b;padding:0.5rem 0.75rem;background:#fffbeb;margin-bottom:0.5rem;font-size:0.85rem;border:1px solid #fde68a;border-left:3px solid #f59e0b;border-radius:0.25rem;">
<div style="font-size:0.7rem;color:#b45309;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Conclusion</div>
{{ ctrl.response_data.conclusion | format_cpam_text }}
</div>
{% endif %}
</div>
{% elif ctrl.contre_argumentation %}
<div style="border-left:3px solid #3b82f6;padding:0.5rem 0.75rem;background:#eff6ff;margin-bottom:0.75rem;font-size:0.85rem;color:#1e3a5f;">
<div style="font-size:0.7rem;color:#1d4ed8;text-transform:uppercase;font-weight:600;margin-bottom:0.25rem;">Contre-argumentation</div>
<pre style="white-space:pre-wrap;font-family:inherit;margin:0;">{{ ctrl.contre_argumentation }}</pre>
</div>
{% endif %}
{% if ctrl.sources_reponse %}
<details>
<summary style="font-size:0.8rem;color:#64748b;">Sources RAG ({{ ctrl.sources_reponse|length }})</summary>
{% for src in ctrl.sources_reponse %}
<pre style="font-size:0.75rem;">{{ src.document }}{% if src.code %} — {{ src.code }}{% endif %}{% if src.page %} [p.{{ src.page }}]{% endif %}
{{ src.extrait or '' }}</pre>
{% endfor %}
</details>
{% endif %}
{% 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 class="cpam-deadline-block" style="margin-top:0.75rem;padding:0.75rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;"
data-filepath="{{ filepath }}" data-ogc="{{ ctrl.numero_ogc }}">
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;font-weight:600;margin-bottom:0.5rem;">Délai réglementaire</div>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
<label style="font-size:0.8rem;color:#475569;font-weight:600;">Date notification :</label>
<input type="date" class="cpam-date-notif"
value="{{ ctrl.date_notification | date_to_iso if ctrl.date_notification else '' }}"
style="padding:0.35rem 0.5rem;border:1px solid #cbd5e1;border-radius:4px;font-size:0.8rem;">
<button onclick="setCpamDeadline(this)"
style="padding:0.35rem 0.75rem;border-radius:6px;border:none;background:#3b82f6;color:#fff;font-size:0.8rem;font-weight:600;cursor:pointer;">
Enregistrer
</button>
<span class="cpam-deadline-status" style="font-size:0.75rem;color:#64748b;">
{% if ctrl.date_limite_reponse %}Limite : {{ ctrl.date_limite_reponse }}{% endif %}
</span>
</div>
</div>
<div class="cpam-validation" style="margin-top:0.75rem;padding:0.75rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;" data-filepath="{{ filepath }}" data-ogc="{{ ctrl.numero_ogc }}">
<div style="font-size:0.7rem;color:#64748b;text-transform:uppercase;font-weight:600;margin-bottom:0.5rem;">Validation DIM</div>
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.5rem;">
<span class="cpam-val-status">
{% if ctrl.validation_dim == 'valide' %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;" title="Contre-argumentation validée par le médecin DIM — prête à être envoyée">Validé</span>
{% elif ctrl.validation_dim == 'rejete' %}
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;" title="Contre-argumentation rejetée par le médecin DIM — ne sera pas envoyée en l'état">Rejeté</span>
{% elif ctrl.validation_dim == 'en_revision' %}
<span class="badge" style="background:#fef3c7;color:#b45309;font-weight:700;" title="Contre-argumentation en cours de révision par le médecin DIM">En révision</span>
{% else %}
<span class="badge" style="background:#f1f5f9;color:#64748b;" title="Contre-argumentation non encore examinée par le médecin DIM">Non validé</span>
{% endif %}
</span>
{% if ctrl.date_validation %}
<span style="font-size:0.75rem;color:#94a3b8;">{{ ctrl.date_validation }}</span>
{% endif %}
</div>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.5rem;">
<button onclick="validateCpam(this, 'valide')" style="padding:0.35rem 0.75rem;border-radius:6px;border:none;background:#16a34a;color:#fff;font-size:0.8rem;font-weight:600;cursor:pointer;">Valider</button>
<button onclick="validateCpam(this, 'en_revision')" style="padding:0.35rem 0.75rem;border-radius:6px;border:none;background:#f59e0b;color:#fff;font-size:0.8rem;font-weight:600;cursor:pointer;">En révision</button>
<button onclick="validateCpam(this, 'rejete')" style="padding:0.35rem 0.75rem;border-radius:6px;border:none;background:#dc2626;color:#fff;font-size:0.8rem;font-weight:600;cursor:pointer;">Rejeter</button>
</div>
<textarea class="cpam-val-comment" rows="2" placeholder="Commentaire DIM (optionnel)" style="width:100%;padding:0.4rem;border:1px solid #cbd5e1;border-radius:4px;font-size:0.8rem;resize:vertical;">{{ ctrl.commentaire_dim or '' }}</textarea>
<details style="margin-top:0.5rem;">
<summary style="font-size:0.75rem;color:#64748b;cursor:pointer;">Versions précédentes</summary>
<div class="cpam-versions-list" data-filepath="{{ filepath }}" data-ogc="{{ ctrl.numero_ogc }}" style="margin-top:0.25rem;font-size:0.8rem;color:#475569;">
<em>Chargement...</em>
</div>
</details>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{# ==================================================================== #}
{# Sections complémentaires (repliables) #}
{# ==================================================================== #}
{# ---- Biologie clé ---- #}
{% if dossier.biologie_cle %}
<details class="card section">
<summary><h3 style="display:inline;">Biologie clé ({{ dossier.biologie_cle|length }})</h3></summary>
<table>
<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.quality == 'suspect' %} style="background:#fffbeb;"{% elif b.anomalie %} class="anomalie"{% endif %}>
<td>{{ b.test }}</td>
<td>{{ b.valeur or '' }}</td>
<td>
{% if b.quality == 'suspect' %}
<span class="badge" style="background:#fef3c7;color:#92400e;" title="Valeur biologique suspecte (possible erreur d'extraction ou unité) — {{ b.discard_reason or 'raison non précisée' }}">Suspect</span>
{% elif b.anomalie %}
<span class="badge" style="background:#fee2e2;color:#dc2626;" title="Valeur hors normes — anomalie biologique pouvant justifier ou appuyer un diagnostic">Oui</span>
{% if b.test in bio_normals and b.valeur_num is not none %}
{% set lo, hi = bio_normals[b.test] %}
{% if b.valeur_num > hi %}
<span style="font-size:0.7rem;color:#dc2626;"> &#8593; {{ b.valeur }} &gt; {{ hi }} <span style="color:#64748b;">(N: {{ lo }}{{ hi }})</span></span>
{% elif b.valeur_num < lo %}
<span style="font-size:0.7rem;color:#dc2626;"> &#8595; {{ b.valeur }} &lt; {{ lo }} <span style="color:#64748b;">(N: {{ lo }}{{ hi }})</span></span>
{% endif %}
{% elif b.test in bio_normals %}
{% set lo, hi = bio_normals[b.test] %}
<span style="font-size:0.7rem;color:#64748b;"> (N: {{ lo }}{{ hi }})</span>
{% endif %}
{% 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 %}
</details>
{% endif %}
{# ---- Imagerie ---- #}
{% if dossier.imagerie %}
<details class="card section">
<summary><h3 style="display:inline;">Imagerie ({{ dossier.imagerie|length }})</h3></summary>
{% for img in dossier.imagerie %}
<div style="margin-bottom:0.5rem;">
<strong>{{ img.type }}</strong>
{% if img.score %} — Score : {{ img.score }}{% endif %}
{% if img.source_page %}<button class="src-btn" data-texte="{{ img.type|e }}" data-excerpt="{{ img.source_excerpt|default('',true)|e }}" data-page="{{ img.source_page }}">p.{{ img.source_page }}</button>{% endif %}
{% if img.conclusion %}<div style="font-size:0.85rem;color:#475569;">{{ img.conclusion }}</div>{% endif %}
</div>
{% endfor %}
</details>
{% endif %}
{# ---- Traitements de sortie ---- #}
{% if dossier.traitements_sortie %}
<details class="card section">
<summary><h3 style="display:inline;">Traitements de sortie ({{ dossier.traitements_sortie|length }})</h3></summary>
<table>
<thead><tr><th>Médicament</th><th>Posologie</th><th>Code ATC</th><th>Source</th></tr></thead>
<tbody>
{% for t in dossier.traitements_sortie %}
<tr>
<td>{{ t.medicament }}</td>
<td>{{ t.posologie or '' }}</td>
<td>{% if t.code_atc %}<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ t.code_atc }}</span>{% endif %}</td>
<td>{% if t.source_page %}<button class="src-btn" data-texte="{{ t.medicament|e }}" data-excerpt="{{ t.source_excerpt|default('',true)|e }}" data-page="{{ t.source_page }}">p.{{ t.source_page }}</button>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{# ---- Antécédents ---- #}
{% if dossier.antecedents %}
<details class="card section">
<summary><h3 style="display:inline;">Antécédents ({{ dossier.antecedents|length }})</h3></summary>
<ul class="bullet">
{% for a in dossier.antecedents %}
<li>{{ a.texte }}{% if a.source_page %} <button class="src-btn" data-texte="{{ a.texte|e }}" data-excerpt="{{ a.source_excerpt|default('',true)|e }}" data-page="{{ a.source_page }}">p.{{ a.source_page }}</button>{% endif %}</li>
{% endfor %}
</ul>
</details>
{% endif %}
{# ---- Complications ---- #}
{% if dossier.complications %}
<div class="card section">
<h3>Complications ({{ dossier.complications|length }})</h3>
<ul class="bullet">
{% for c in dossier.complications %}
<li>{{ c.texte }}{% if c.source_page %} <button class="src-btn" data-texte="{{ c.texte|e }}" data-excerpt="{{ c.source_excerpt|default('',true)|e }}" data-page="{{ c.source_page }}">p.{{ c.source_page }}</button>{% endif %}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# ==================================================================== #}
{# 5. DOCUMENTS SOURCES (nouveau — en bas de page) #}
{# ==================================================================== #}
{% if siblings and siblings|length > 1 %}
<div class="card section" style="margin-top:1.5rem;">
<h3>Documents du dossier</h3>
<table class="table-dossiers">
<thead><tr><th>Document</th><th>Type</th><th>DAS</th><th>Actes</th></tr></thead>
<tbody>
{% for sib in siblings %}
<tr class="row-clickable" onclick="window.location='/dossier/{{ sib.path_rel }}'">
<td>
<span style="font-weight:600;">{{ sib.name | format_doc_name }}</span>
{% if sib.path_rel == filepath %}
<span class="badge" style="background:#dbeafe;color:#1d4ed8;margin-left:0.3rem;">actuel</span>
{% endif %}
{% if 'fusionne' in sib.name %}
<span class="badge" style="background:#ede9fe;color:#5b21b6;margin-left:0.3rem;">fusionné</span>
{% endif %}
</td>
<td>{% if sib.dossier.document_type %}<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ sib.dossier.document_type }}</span>{% endif %}</td>
<td>{{ sib.dossier.diagnostics_associes|length }}</td>
<td>{{ sib.dossier.actes_ccam|length }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{# ---- Modal source ---- #}
<div id="source-modal">
<div id="source-modal-inner">
<div id="source-header">
<span id="source-title">Document source</span>
<button id="source-close-btn" onclick="closeSource()">Fermer</button>
</div>
<div id="source-content"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
/* --- Validation DIM CPAM --- */
async function validateCpam(btn, statut) {
const container = btn.closest('.cpam-validation');
const ogc = container.dataset.ogc;
const filepath = container.dataset.filepath;
const comment = container.querySelector('.cpam-val-comment').value.trim();
try {
const resp = await fetch('/api/cpam/' + filepath + '/' + ogc + '/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({statut: statut, commentaire: comment})
});
const data = await resp.json();
if (data.ok) {
const labels = {valide:'Validé',rejete:'Rejeté',en_revision:'En révision',non_valide:'Non validé'};
const colors = {valide:'background:#d1fae5;color:#065f46',rejete:'background:#fee2e2;color:#dc2626',en_revision:'background:#fef3c7;color:#b45309',non_valide:'background:#f1f5f9;color:#64748b'};
container.querySelector('.cpam-val-status').innerHTML = '<span class="badge" style="'+colors[statut]+';font-weight:700;">'+labels[statut]+'</span>';
} else {
alert('Erreur : ' + (data.error || 'inconnue'));
}
} catch(e) { console.error('validateCpam:', e); alert('Erreur réseau'); }
}
/* --- Deadline CPAM --- */
async function setCpamDeadline(btn) {
const container = btn.closest('.cpam-deadline-block');
const ogc = container.dataset.ogc;
const filepath = container.dataset.filepath;
const dateInput = container.querySelector('.cpam-date-notif');
const statusEl = container.querySelector('.cpam-deadline-status');
if (!dateInput.value) { alert('Saisissez une date de notification'); return; }
const [y, m, d] = dateInput.value.split('-');
const dateFr = d + '/' + m + '/' + y;
try {
const resp = await fetch('/api/cpam/' + encodeURIComponent(filepath) + '/' + ogc + '/deadline', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({date_notification: dateFr})
});
const data = await resp.json();
if (data.ok) {
statusEl.textContent = 'Limite : ' + data.date_limite;
statusEl.style.color = '#065f46';
} else {
alert('Erreur : ' + (data.error || 'inconnue'));
}
} catch(e) { console.error('setCpamDeadline:', e); alert('Erreur réseau'); }
}
/* --- Chargement versions CPAM --- */
document.querySelectorAll('.cpam-versions-list').forEach(function(el) {
el.closest('details').addEventListener('toggle', async function() {
if (!this.open || el.dataset.loaded) return;
el.dataset.loaded = '1';
const fp = el.dataset.filepath;
const ogc = el.dataset.ogc;
try {
const resp = await fetch('/api/cpam/' + fp + '/' + ogc + '/versions');
const data = await resp.json();
if (!data.versions || data.versions.length === 0) {
el.innerHTML = '<em>Aucune version précédente</em>';
return;
}
let html = '<ul style="margin:0;padding-left:1.2rem;">';
data.versions.forEach(function(v) {
const tier = v.quality_tier ? ' [Qualité ' + v.quality_tier + ']' : '';
const val = v.validation_dim && v.validation_dim !== 'non_valide' ? ' — ' + v.validation_dim : '';
html += '<li>v' + v.version + ' — ' + v.timestamp + tier + val +
'<br><small style="color:#94a3b8;">' + (v.contre_argumentation || '').substring(0, 100) + '…</small></li>';
});
html += '</ul>';
el.innerHTML = html;
} catch(e) { el.innerHTML = '<em>Erreur de chargement</em>'; }
});
});
/* --- Source modal --- */
let _sourceCache = null;
const _dossierId = (function() {
const fp = {{ filepath|tojson }};
const parts = fp.split('/');
return parts.length > 1 ? parts.slice(0, -1).join('/') : '';
})();
const _sourceFiles = {{ dossier.source_files|tojson }};
function getDossierId() { return _dossierId; }
async function loadSourceTexts() {
if (_sourceCache !== null) return _sourceCache;
if (!_dossierId) { _sourceCache = {}; return _sourceCache; }
try {
const resp = await fetch('/api/source-text/' + _dossierId);
if (resp.ok) { _sourceCache = await resp.json(); }
else { _sourceCache = {}; }
} catch (e) { _sourceCache = {}; }
return _sourceCache;
}
async function pdfAvailable(dossierId, filename) {
try {
const resp = await fetch('/api/pdf/' + dossierId + '/' + encodeURIComponent(filename), {method: 'HEAD'});
return resp.ok;
} catch (e) { return false; }
}
function buildPdfUrl(dossierId, filename, page, excerpt) {
let url = '/api/pdf/' + dossierId + '/' + encodeURIComponent(filename);
const params = [];
if (excerpt) params.push('highlight=' + encodeURIComponent(excerpt));
if (page) params.push('page=' + page);
if (params.length) url += '?' + params.join('&');
url += '#page=' + (page || 1);
return url;
}
function loadPdf(dossierId, filename, page, excerpt) {
const content = document.getElementById('source-content');
const url = buildPdfUrl(dossierId, filename, page, excerpt);
content.className = 'source-content-pdf';
content.innerHTML = '<iframe src="' + url + '" style="width:100%;height:100%;border:none;"></iframe>';
document.querySelectorAll('.src-file-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.src-file-btn').forEach(b => {
if (b.textContent === filename) b.classList.add('active');
});
}
async function showSource(excerpt, page, texte) {
const highlightText = texte || excerpt;
const modal = document.getElementById('source-modal');
const modalInner = document.getElementById('source-modal-inner');
const content = document.getElementById('source-content');
const title = document.getElementById('source-title');
title.textContent = 'Document source — Page ' + page;
content.innerHTML = '<em style="color:#94a3b8;">Chargement...</em>';
content.className = '';
modalInner.className = '';
modal.style.display = 'block';
if (_sourceFiles && _sourceFiles.length > 0 && _dossierId) {
const firstFile = _sourceFiles[0];
const available = await pdfAvailable(_dossierId, firstFile);
if (available) {
modalInner.className = '';
if (_sourceFiles.length === 1) {
loadPdf(_dossierId, firstFile, page, highlightText);
} else {
const safeHighlight = (highlightText || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
let html = '<div style="padding:0.5rem 0.75rem;border-bottom:1px solid #e2e8f0;display:flex;gap:0.5rem;flex-wrap:wrap;">';
_sourceFiles.forEach(function(f) {
const safeF = f.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
html += '<button class="src-file-btn" onclick="loadPdf(\'' + _dossierId + '\', \'' + safeF + '\', ' + page + ', \'' + safeHighlight + '\')">' + f + '</button>';
});
html += '</div>';
html += '<iframe id="pdf-frame" style="width:100%;flex:1;border:none;"></iframe>';
content.className = 'source-content-pdf';
content.style.display = 'flex';
content.style.flexDirection = 'column';
content.innerHTML = html;
const iframe = content.querySelector('iframe');
iframe.src = buildPdfUrl(_dossierId, firstFile, page, highlightText);
content.querySelector('.src-file-btn').classList.add('active');
}
return;
}
}
modalInner.className = 'source-modal-text';
content.className = '';
content.style.display = '';
const texts = await loadSourceTexts();
const allText = Object.values(texts).join('\n\n--- ---\n\n');
if (!allText) {
content.innerHTML = '<em style="color:#94a3b8;">Texte source non disponible</em>';
return;
}
let searchText = (excerpt || '').trim();
if (searchText.startsWith('...')) searchText = searchText.substring(3);
if (searchText.endsWith('...')) searchText = searchText.slice(0, -3);
searchText = searchText.trim();
if (searchText.length > 10) {
let idx = allText.indexOf(searchText);
if (idx < 0 && searchText.length > 60) {
const mid = Math.floor(searchText.length / 2);
searchText = searchText.substring(mid - 30, mid + 30);
idx = allText.indexOf(searchText);
}
if (idx >= 0) {
const before = allText.substring(0, idx);
const match = allText.substring(idx, idx + searchText.length);
const after = allText.substring(idx + searchText.length);
content.innerHTML = '';
content.appendChild(document.createTextNode(before));
const mark = document.createElement('mark');
mark.textContent = match;
mark.id = 'source-highlight';
content.appendChild(mark);
content.appendChild(document.createTextNode(after));
setTimeout(() => {
const el = document.getElementById('source-highlight');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
return;
}
}
content.textContent = allText;
}
function closeSource() {
const content = document.getElementById('source-content');
content.innerHTML = '';
content.style.display = '';
content.className = '';
document.getElementById('source-modal').style.display = 'none';
}
document.getElementById('source-modal').addEventListener('click', function(e) {
if (e.target === this) closeSource();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeSource();
});
document.addEventListener('click', function(e) {
const btn = e.target.closest('.src-btn');
if (btn && btn.dataset.page) {
showSource(btn.dataset.excerpt || '', parseInt(btn.dataset.page), btn.dataset.texte || '');
}
});
// Reprocess dossier
function reprocessDossier() {
var btn = document.getElementById('btn-reprocess');
var status = document.getElementById('reprocess-status');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Traitement…';
status.textContent = '';
fetch('/admin/reprocess/{{ filepath }}', { method: 'POST', credentials: 'same-origin' })
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') === -1) throw new Error('Réponse inattendue du serveur');
return r.json();
})
.then(function(data) {
if (data.ok) {
status.style.color = '#059669';
status.textContent = data.message || 'Terminé';
setTimeout(function() { window.location.reload(); }, 1500);
} else {
status.style.color = '#dc2626';
status.textContent = data.error || 'Erreur';
btn.disabled = false;
btn.innerHTML = '&#8635; Retraiter';
}
})
.catch(function(e) {
status.style.color = '#dc2626';
status.textContent = 'Erreur : ' + e.message;
btn.disabled = false;
btn.innerHTML = '&#8635; Retraiter';
});
}
// Upload document
function uploadDocument() {
var fileInput = document.getElementById('upload-file');
var status = document.getElementById('upload-status');
if (!fileInput.files.length) return;
var formData = new FormData();
formData.append('file', fileInput.files[0]);
status.style.color = '#64748b';
status.textContent = 'Envoi de ' + fileInput.files[0].name + '…';
fetch('/admin/upload-document/{{ filepath }}', { method: 'POST', body: formData })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
status.style.color = '#059669';
status.textContent = data.message || 'Upload terminé';
setTimeout(function() { window.location.reload(); }, 2000);
} else {
status.style.color = '#dc2626';
status.textContent = data.error || 'Erreur';
}
})
.catch(function(e) {
status.style.color = '#dc2626';
status.textContent = 'Erreur : ' + e.message;
});
}
</script>
{% endblock %}