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>
1194 lines
65 KiB
HTML
1194 lines
65 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ dossier.source_file or filepath }}{% endblock %}
|
||
|
||
{% block content %}
|
||
<a class="back" href="/">← 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;">↻ 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;">
|
||
📎 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;">✓ {{ check.resume }}</span>
|
||
{% elif check.verdict == 'fragile' %}
|
||
<span style="font-size:0.75rem;color:#d97706;font-weight:600;">⚠ {{ check.resume }}</span>
|
||
{% else %}
|
||
<span style="font-size:0.75rem;color:#dc2626;font-weight:600;">✗ {{ 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 }}">
|
||
✓ {{ 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 }}">
|
||
⚠ {{ 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 }}">
|
||
~ {{ 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 }}">
|
||
✓ {{ 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 }}">
|
||
✗ {{ 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 }}">
|
||
— {{ 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;">→ {{ 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;">→ {{ 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;">→ {{ 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;"> ↑ {{ b.valeur }} > {{ hi }} <span style="color:#64748b;">(N: {{ lo }}–{{ hi }})</span></span>
|
||
{% elif b.valeur_num < lo %}
|
||
<span style="font-size:0.7rem;color:#dc2626;"> ↓ {{ b.valeur }} < {{ 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 = '↻ Retraiter';
|
||
}
|
||
})
|
||
.catch(function(e) {
|
||
status.style.color = '#dc2626';
|
||
status.textContent = 'Erreur : ' + e.message;
|
||
btn.disabled = false;
|
||
btn.innerHTML = '↻ 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 %}
|