chore: nettoyage YAML base.yaml + corrections templates viewer

- base.yaml: suppression commentaires verbose, normalisation quotes YAML
- Templates: corrections mineures cpam.html, detail.html, dim.html, index.html
- admin_rules.html: ajustements interface admin règles
- test_referentiels.py: mise à jour imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dom
2026-03-07 22:07:00 +01:00
parent 79c447688c
commit e6bd7406a4
7 changed files with 294 additions and 189 deletions

View File

@@ -1,107 +1,92 @@
version: 1
# Catalogue "socle" de règles.
#
# Objectif : piloter (sans toucher au code) :
# - l'activation/désactivation de règles (vetos + décisions)
# - éventuellement un forçage de sévérité pour un VETO
#
# Important : si une règle n'est pas listée ici, elle est considérée activée.
# (=> comportement historique conservé)
packs:
vetos_core:
enabled: true
rules:
VETO-02:
enabled: true
description: "Code sans preuve exploitable"
description: Code sans preuve exploitable
VETO-03:
enabled: true
description: "Conditionnel / négation / contradictions dans la preuve"
description: Conditionnel / négation / contradictions dans la preuve
VETO-06:
enabled: true
description: "DP dupliqué dans les DAS"
description: DP dupliqué dans les DAS
VETO-07:
enabled: true
description: "Doublons DAS"
description: Doublons DAS
VETO-09:
enabled: true
description: "Contradiction biologique (plaquettes/créat)"
# force_severity: "HARD" # Optionnel : forcer la sévérité globale
description: Contradiction biologique (plaquettes/créat)
VETO-12:
enabled: true
description: "Sur-confiance (high sans preuve)"
description: Sur-confiance (high sans preuve)
VETO-15:
enabled: true
description: "Preuve issue d'un score/test (risque de sur-codage)"
description: Preuve issue d'un score/test (risque de sur-codage)
VETO-16:
enabled: true
description: "Heuristique libellé→code (hors-sujet probable)"
description: Heuristique libellé→code (hors-sujet probable)
VETO-17:
enabled: true
description: "Preuve biologique manquante => NEED_INFO (non bloquant)"
description: Preuve biologique manquante => NEED_INFO (non bloquant)
decisions_core:
enabled: true
rules:
RULE-D50-NEEDS-IRON:
enabled: true
description: "D50 sans preuve martiale => downgrade D64.9 + NEED_INFO"
description: D50 sans preuve martiale => downgrade D64.9 + NEED_INFO
RULE-D69.6-PLT-NORMAL:
enabled: true
description: "D69.6 incompatible avec plaquettes normales => ruled_out (barré)"
description: D69.6 incompatible avec plaquettes normales => ruled_out (barré)
RULE-DAS-TO-DP:
enabled: true
description: "DAS promu en DP si aucun DP extrait — sélection par pertinence/confiance/spécificité"
description: DAS promu en DP si aucun DP extrait — sélection par pertinence/confiance/spécificité
RULE-CPAM-CORRECTION-LOOP:
enabled: true
description: "Boucle de correction quand validation adversariale score ≤ 5/10"
description: Boucle de correction quand validation adversariale score ≤ 5/10
bio_electrolytes:
enabled: true
rules:
RULE-E87.1-NA-NORMAL:
enabled: true
description: "E87.1 suggérée mais Na normal => ruled_out"
description: E87.1 suggérée mais Na normal => ruled_out
RULE-E87.1-MISSING-NA:
enabled: true
description: "E87.1 suggérée mais Na absent => NEED_INFO"
description: E87.1 suggérée mais Na absent => NEED_INFO
RULE-E87.5-K-NORMAL:
enabled: true
description: "E87.5 suggérée mais K normal => ruled_out"
description: E87.5 suggérée mais K normal => ruled_out
RULE-E87.5-MISSING-K:
enabled: true
description: "E87.5 suggérée mais K absent => NEED_INFO"
description: E87.5 suggérée mais K absent => NEED_INFO
RULE-E87.6-K-NORMAL:
enabled: true
description: "E87.6 suggérée mais K normal => ruled_out"
description: E87.6 suggérée mais K normal => ruled_out
RULE-E87.6-MISSING-K:
enabled: true
description: "E87.6 suggérée mais K absent => NEED_INFO"
description: E87.6 suggérée mais K absent => NEED_INFO
atih_core:
enabled: true
rules:
VETO-20:
enabled: true
description: "Z code interdit en DP (sauf whitelist Z09/Z51/Z54/Z75/Z03/Z04/Z38/Z50/Z08)"
description: Z code interdit en DP (sauf whitelist Z09/Z51/Z54/Z75/Z03/Z04/Z38/Z50/Z08)
VETO-21:
enabled: true
description: "Code R (symptôme) en DP → CMD 23, tarification faible"
description: Code R (symptôme) en DP → CMD 23, tarification faible
VETO-22:
enabled: true
description: "Même catégorie CIM-10 3 chars en DP + DAS (redondance)"
description: Même catégorie CIM-10 3 chars en DP + DAS (redondance)
VETO-23:
enabled: true
description: "Exclusions mutuelles (E10/E11 diabète, I10/I11-I13 HTA)"
description: Exclusions mutuelles (E10/E11 diabète, I10/I11-I13 HTA)
VETO-24:
enabled: true
description: "Lésion traumatique (S/T) sans cause externe (V/W/X/Y)"
description: Lésion traumatique (S/T) sans cause externe (V/W/X/Y)
placeholders_future:
enabled: false
rules:
RULE-PDF-PROTECTED-NEED_INFO:
enabled: false
description: "PDF protégé => NEED_INFO (à implémenter si besoin)"
description: PDF protégé => NEED_INFO (à implémenter si besoin)

View File

@@ -269,18 +269,59 @@
{# ---- Modal ajout regle ---- #}
<div id="add-rule-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:1000;
display:none;align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:0.5rem;padding:1.5rem;width:400px;max-width:90vw;box-shadow:0 25px 50px rgba(0,0,0,0.2);">
<h3 style="margin:0 0 1rem;">Ajouter une regle</h3>
<input id="add-rule-id" placeholder="Identifiant (ex: VETO-99)"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.5rem;box-sizing:border-box;">
<input id="add-rule-desc" placeholder="Description"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.5rem;box-sizing:border-box;">
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:1rem;">
align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:0.5rem;padding:1.5rem;width:500px;max-width:90vw;box-shadow:0 25px 50px rgba(0,0,0,0.2);">
<h3 style="margin:0 0 0.25rem;">Ajouter une r&egrave;gle</h3>
<p id="add-rule-context" style="font-size:0.75rem;color:#64748b;margin:0 0 1rem;"></p>
{# Champs communs #}
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Identifiant *</label>
<input id="add-rule-id" placeholder="Ex: VETO-99 ou hyponatremia"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Description *</label>
<input id="add-rule-desc" placeholder="Description de la r&egrave;gle"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
{# Champs Veto/Packs (base.yaml) #}
<div id="add-fields-packs" style="display:none;">
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">S&eacute;v&eacute;rit&eacute; forc&eacute;e</label>
<select id="add-rule-severity"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
<option value="">(aucune &mdash; s&eacute;v&eacute;rit&eacute; par d&eacute;faut)</option>
<option value="HARD">HARD &mdash; Bloquant (rejet du codage)</option>
<option value="MEDIUM">MEDIUM &mdash; &Agrave; v&eacute;rifier</option>
<option value="LOW">LOW &mdash; Information</option>
</select>
</div>
{# Champs Bio (bio_rules.yaml) #}
<div id="add-fields-bio" style="display:none;">
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Codes CIM-10 * <span style="font-weight:400;text-transform:none;">(s&eacute;par&eacute;s par des virgules)</span></label>
<input id="add-rule-codes" placeholder="Ex: E87.1, E87.5"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Analyte *</label>
<input id="add-rule-analyte" placeholder="Ex: sodium, potassium, creatinine, CRP"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Type de seuil *</label>
<select id="add-rule-threshold"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
<option value="high">high &mdash; La valeur doit &ecirc;tre &eacute;lev&eacute;e pour confirmer le diagnostic</option>
<option value="low">low &mdash; La valeur doit &ecirc;tre basse pour confirmer le diagnostic</option>
</select>
<label style="font-size:0.7rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Message de contradiction</label>
<input id="add-rule-message" placeholder="Ex: natr&eacute;mie normale"
style="width:100%;padding:0.5rem;border:1px solid #d1d5db;border-radius:0.25rem;margin-bottom:0.75rem;box-sizing:border-box;">
</div>
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.5rem;">
<button onclick="closeAddRule()"
style="padding:0.4rem 1rem;border:1px solid #d1d5db;border-radius:0.25rem;background:#fff;cursor:pointer;">Annuler</button>
<button onclick="submitAddRule()"
style="padding:0.4rem 1rem;border:none;border-radius:0.25rem;background:#3b82f6;color:#fff;cursor:pointer;">Ajouter</button>
style="padding:0.4rem 1rem;border:none;border-radius:0.25rem;background:#3b82f6;color:#fff;cursor:pointer;font-weight:600;">Ajouter</button>
</div>
</div>
</div>
@@ -345,35 +386,77 @@ let _addFileId = '', _addParentPath = '';
function showAddRule(fileId, parentPath) {
_addFileId = fileId;
_addParentPath = parentPath;
// Reset tous les champs
document.getElementById('add-rule-id').value = '';
document.getElementById('add-rule-desc').value = '';
var packFields = document.getElementById('add-fields-packs');
var bioFields = document.getElementById('add-fields-bio');
var ctx = document.getElementById('add-rule-context');
packFields.style.display = 'none';
bioFields.style.display = 'none';
if (fileId === 'bio_rules') {
bioFields.style.display = 'block';
ctx.textContent = 'Règle biologique — contradiction bio \u2192 écartement automatique du diagnostic';
document.getElementById('add-rule-codes').value = '';
document.getElementById('add-rule-analyte').value = '';
document.getElementById('add-rule-threshold').value = 'high';
document.getElementById('add-rule-message').value = '';
} else if (fileId === 'base') {
packFields.style.display = 'block';
ctx.textContent = 'Règle veto/décision — contrôle de contestabilité du codage';
document.getElementById('add-rule-severity').value = '';
} else {
ctx.textContent = 'Règle générique pour le fichier ' + fileId;
}
document.getElementById('add-rule-modal').style.display = 'flex';
}
function closeAddRule() {
document.getElementById('add-rule-modal').style.display = 'none';
}
function submitAddRule() {
const ruleId = document.getElementById('add-rule-id').value.trim();
const desc = document.getElementById('add-rule-desc').value.trim();
var ruleId = document.getElementById('add-rule-id').value.trim();
var desc = document.getElementById('add-rule-desc').value.trim();
if (!ruleId) { alert('Identifiant requis'); return; }
if (!desc) { alert('Description requise'); return; }
var ruleData = {enabled: true, description: desc};
if (_addFileId === 'bio_rules') {
var codesRaw = document.getElementById('add-rule-codes').value.trim();
var analyte = document.getElementById('add-rule-analyte').value.trim();
var threshold = document.getElementById('add-rule-threshold').value;
var msg = document.getElementById('add-rule-message').value.trim();
if (!codesRaw) { alert('Codes CIM-10 requis'); return; }
if (!analyte) { alert('Analyte requis'); return; }
ruleData.codes = codesRaw.split(',').map(function(c) { return c.trim(); }).filter(Boolean);
ruleData.analyte = analyte;
ruleData.threshold_type = threshold;
if (msg) ruleData.message = msg;
delete ruleData.description; // bio_rules n'utilise pas description
} else if (_addFileId === 'base') {
var sev = document.getElementById('add-rule-severity').value;
if (sev) ruleData.force_severity = sev;
}
fetch('/api/rules/' + _addFileId + '/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
parent_path: _addParentPath,
rule_id: ruleId,
rule_data: {enabled: true, description: desc}
rule_data: ruleData
})
})
.then(r => r.json())
.then(data => {
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
showToast('Regle ajoutee', true);
showToast('Règle ajoutée', true);
closeAddRule();
setTimeout(() => location.reload(), 500);
setTimeout(function() { location.reload(); }, 500);
} else showToast('Erreur : ' + data.error, false);
})
.catch(err => showToast('Erreur reseau', false));
.catch(function(err) { showToast('Erreur réseau', false); });
}
</script>
{% endblock %}

View File

@@ -6,7 +6,7 @@
<div style="display:flex;align-items:center;gap:0.75rem;margin-top:1rem;margin-bottom:1rem;">
<h2 style="margin:0;">Contrôles UCR</h2>
<span class="badge" style="background:#fef3c7;color:#b45309;font-size:0.85rem;padding:4px 12px;">{{ total }}</span>
<span class="badge" style="background:#fef3c7;color:#b45309;font-size:0.85rem;padding:4px 12px;" title="Nombre total de contrôles UCR (Unité de Coordination Régionale) identifiés dans les dossiers">{{ total }}</span>
</div>
{% if not controls %}
@@ -18,16 +18,16 @@
<table>
<thead>
<tr>
<th>Priorité</th>
<th title="Priorité basée sur l'impact financier estimé du contrôle">Priorité</th>
<th>Dossier</th>
<th>OGC</th>
<th>Qualité</th>
<th title="Numéro d'Ordonnance de Gestion de Caisse — identifiant du contrôle CPAM">OGC</th>
<th title="Note de qualité de la contre-argumentation générée : A=solide, B=acceptable, C=faible">Qualité</th>
<th>Titre</th>
<th>Décision UCR</th>
<th>Codes contestés</th>
<th>Délai</th>
<th>Validation</th>
<th>Contre-argumentation</th>
<th title="Décision de l'Unité de Coordination Régionale : retient le codage initial ou confirme l'anomalie">Décision UCR</th>
<th title="Codes CIM-10 ou CCAM contestés par le contrôle CPAM">Codes contestés</th>
<th title="Jours restants avant la date limite de réponse au contrôle">Délai</th>
<th title="Statut de validation par le médecin DIM">Validation</th>
<th title="Contre-argumentation médicale générée par l'IA pour répondre au contrôle">Contre-argumentation</th>
</tr>
</thead>
<tbody>
@@ -36,15 +36,15 @@
<td style="text-align:center;">
{% set fi = c.ctrl.financial_impact %}
{% if fi and fi.priorite == 'critique' %}
<span class="badge" style="background:#dc2626;color:#fff;font-weight:700;font-size:0.75rem;padding:3px 10px;">Critique</span>
<div style="font-size:0.65rem;color:#dc2626;margin-top:2px;">~{{ fi.impact_estime_euros }}€</div>
<span class="badge" style="background:#dc2626;color:#fff;font-weight:700;font-size:0.75rem;padding:3px 10px;" title="Impact financier critique — risque d'indu important, traitement prioritaire recommandé">Critique</span>
<div style="font-size:0.65rem;color:#dc2626;margin-top:2px;" title="Estimation de l'impact financier si le contrôle aboutit à un indu">~{{ fi.impact_estime_euros }}€</div>
{% elif fi and fi.priorite == 'haute' %}
<span class="badge" style="background:#f59e0b;color:#fff;font-weight:700;font-size:0.75rem;padding:3px 10px;">Haute</span>
<div style="font-size:0.65rem;color:#b45309;margin-top:2px;">~{{ fi.impact_estime_euros }}€</div>
<span class="badge" style="background:#f59e0b;color:#fff;font-weight:700;font-size:0.75rem;padding:3px 10px;" title="Impact financier élevé — attention particulière requise">Haute</span>
<div style="font-size:0.65rem;color:#b45309;margin-top:2px;" title="Estimation de l'impact financier si le contrôle aboutit à un indu">~{{ fi.impact_estime_euros }}€</div>
{% elif fi and fi.priorite == 'faible' %}
<span class="badge" style="background:#94a3b8;color:#fff;font-size:0.75rem;padding:3px 8px;">Faible</span>
<span class="badge" style="background:#94a3b8;color:#fff;font-size:0.75rem;padding:3px 8px;" title="Impact financier faible — traitement standard">Faible</span>
{% else %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-size:0.75rem;padding:3px 8px;">Normale</span>
<span class="badge" style="background:#d1fae5;color:#065f46;font-size:0.75rem;padding:3px 8px;" title="Impact financier normal">Normale</span>
{% endif %}
</td>
<td>
@@ -58,11 +58,11 @@
<td style="font-weight:600;">{{ c.ctrl.numero_ogc }}</td>
<td style="text-align:center;">
{% if c.ctrl.quality_tier == 'A' %}
<span class="badge" style="background:#2ecc71;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;">A</span>
<span class="badge" style="background:#2ecc71;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;" title="Qualité A — contre-argumentation solide, bien documentée, preuves convergentes">A</span>
{% elif c.ctrl.quality_tier == 'B' %}
<span class="badge" style="background:#f39c12;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;">B</span>
<span class="badge" style="background:#f39c12;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;" title="Qualité B — contre-argumentation acceptable mais certains points pourraient être renforcés">B</span>
{% elif c.ctrl.quality_tier == 'C' %}
<span class="badge" style="background:#e74c3c;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;">C</span>
<span class="badge" style="background:#e74c3c;color:#fff;font-weight:700;font-size:0.8rem;padding:3px 10px;" title="Qualité C — contre-argumentation faible, preuves insuffisantes, révision recommandée">C</span>
{% else %}
<span style="color:#94a3b8;font-size:0.7rem;"></span>
{% endif %}
@@ -70,19 +70,19 @@
<td style="max-width:200px;">{{ c.ctrl.titre }}</td>
<td>
{% if 'retient' in c.ctrl.decision_ucr|lower %}
<span class="badge" style="background:#d1fae5;color:#065f46;">{{ c.ctrl.decision_ucr }}</span>
<span class="badge" style="background:#d1fae5;color:#065f46;" title="L'UCR retient le codage initial de l'établissement — pas d'indu">{{ c.ctrl.decision_ucr }}</span>
{% elif 'confirme' in c.ctrl.decision_ucr|lower %}
<span class="badge" style="background:#fee2e2;color:#dc2626;">{{ c.ctrl.decision_ucr }}</span>
<span class="badge" style="background:#fee2e2;color:#dc2626;" title="L'UCR confirme l'anomalie signalée par la CPAM — indu probable">{{ c.ctrl.decision_ucr }}</span>
{% else %}
<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ c.ctrl.decision_ucr }}</span>
<span class="badge" style="background:#e0e7ff;color:#3730a3;" title="Décision UCR en attente ou autre statut">{{ c.ctrl.decision_ucr }}</span>
{% endif %}
</td>
<td>
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;">
{% if c.ctrl.dp_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;">DP: {{ c.ctrl.dp_ucr }}</span>{% endif %}
{% if c.ctrl.da_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;">DA: {{ c.ctrl.da_ucr }}</span>{% endif %}
{% if c.ctrl.dr_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;">DR: {{ c.ctrl.dr_ucr }}</span>{% endif %}
{% if c.ctrl.actes_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;">Actes: {{ c.ctrl.actes_ucr }}</span>{% endif %}
{% if c.ctrl.dp_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;" title="Diagnostic Principal contesté par la CPAM">DP: {{ c.ctrl.dp_ucr }}</span>{% endif %}
{% if c.ctrl.da_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;" title="Diagnostic Associé contesté par la CPAM">DA: {{ c.ctrl.da_ucr }}</span>{% endif %}
{% if c.ctrl.dr_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;" title="Diagnostic Relié contesté par la CPAM">DR: {{ c.ctrl.dr_ucr }}</span>{% endif %}
{% if c.ctrl.actes_ucr %}<span class="badge" style="background:#fef3c7;color:#92400e;font-size:0.65rem;" title="Actes CCAM contestés par la CPAM">Actes: {{ c.ctrl.actes_ucr }}</span>{% endif %}
{% if not c.ctrl.dp_ucr and not c.ctrl.da_ucr and not c.ctrl.dr_ucr and not c.ctrl.actes_ucr %}
{% if c.ctrl.contre_argumentation %}
<button class="btn-toggle-arg" data-row="{{ loop.index }}" style="background:none;border:1px solid #3b82f6;color:#3b82f6;border-radius:4px;padding:2px 8px;font-size:0.7rem;font-weight:600;cursor:pointer;">Voir analyse</button>
@@ -95,13 +95,13 @@
<td style="text-align:center;white-space:nowrap;">
{% if c.jours_restants is not none %}
{% if c.jours_restants < 0 %}
<span class="badge" style="background:#dc2626;color:#fff;font-weight:700;font-size:0.75rem;">Hors délai</span>
<span class="badge" style="background:#dc2626;color:#fff;font-weight:700;font-size:0.75rem;" title="Date limite de réponse dépassée — réponse urgente requise">Hors délai</span>
{% elif c.jours_restants < 7 %}
<span class="badge" style="background:#dc2626;color:#fff;font-size:0.75rem;">{{ c.jours_restants }}j</span>
<span class="badge" style="background:#dc2626;color:#fff;font-size:0.75rem;" title="Moins de 7 jours restants pour répondre au contrôle — urgence">{{ c.jours_restants }}j</span>
{% elif c.jours_restants < 15 %}
<span class="badge" style="background:#f59e0b;color:#fff;font-size:0.75rem;">{{ c.jours_restants }}j</span>
<span class="badge" style="background:#f59e0b;color:#fff;font-size:0.75rem;" title="{{ c.jours_restants }} jours restants pour répondre au contrôle">{{ c.jours_restants }}j</span>
{% else %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-size:0.75rem;">{{ c.jours_restants }}j</span>
<span class="badge" style="background:#d1fae5;color:#065f46;font-size:0.75rem;" title="{{ c.jours_restants }} jours restants pour répondre au contrôle — délai confortable">{{ c.jours_restants }}j</span>
{% endif %}
{% if c.ctrl.date_limite_reponse %}
<div style="font-size:0.6rem;color:#94a3b8;">{{ c.ctrl.date_limite_reponse }}</div>
@@ -112,11 +112,11 @@
</td>
<td style="text-align:center;">
{% if c.ctrl.validation_dim == 'valide' %}
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;font-size:0.75rem;">Validé</span>
<span class="badge" style="background:#d1fae5;color:#065f46;font-weight:700;font-size:0.75rem;" title="Contre-argumentation validée par le médecin DIM — prête à être envoyée">Validé</span>
{% elif c.ctrl.validation_dim == 'rejete' %}
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;font-size:0.75rem;">Rejeté</span>
<span class="badge" style="background:#fee2e2;color:#dc2626;font-weight:700;font-size:0.75rem;" title="Contre-argumentation rejetée par le médecin DIM">Rejeté</span>
{% elif c.ctrl.validation_dim == 'en_revision' %}
<span class="badge" style="background:#fef3c7;color:#b45309;font-weight:700;font-size:0.75rem;">En révision</span>
<span class="badge" style="background:#fef3c7;color:#b45309;font-weight:700;font-size:0.75rem;" title="Contre-argumentation en cours de révision par le médecin DIM">En révision</span>
{% else %}
<span style="color:#94a3b8;font-size:0.7rem;"></span>
{% endif %}

View File

@@ -17,10 +17,10 @@
<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>
{% if dossier.document_type %}
<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ dossier.document_type }}</span>
<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;">fusionné</span>
<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>
@@ -83,22 +83,22 @@
<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;">C — Chirurgical</span>
<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;">K — Interventionnel</span>
<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;">M — Médical</span>
<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;">Niv. {{ ghm.severite }}</span>
<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;">Niv. {{ ghm.severite }}</span>
<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;">Niv. {{ ghm.severite }}</span>
<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;">Niv. {{ ghm.severite }}</span>
<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;">{{ ghm.cma_count }} CMA, {{ ghm.cms_count }} CMS</span>
<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 %}
@@ -112,16 +112,16 @@
{% 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;">CONFORME</span>
<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;">À COMPLÉTER</span>
<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;">NON CONFORME</span>
<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;">
<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;">{{ vr.score_contestabilite }}/100</span>
<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 %}
@@ -149,13 +149,13 @@
<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;">DÉFENDABLE</span>
<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;">FRAGILE</span>
<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;">INDÉFENDABLE</span>
<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;">
@@ -293,7 +293,7 @@
<div class="card section">
<h3>Diagnostics associés ({{ dossier.diagnostics_associes|length }})</h3>
<table>
<thead><tr><th>Code CIM-10</th><th>Libellé</th><th>CMA</th><th>Confiance</th><th>Source</th></tr></thead>
<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 %}>
@@ -318,7 +318,7 @@
{% endif %}
{% if das.status == 'needs_info' %}
<div style="margin-top:0.2rem;">
<span class="badge" style="background:#fff7ed;color:#c2410c;font-size:0.7rem;">Info requise</span>
<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;">
@@ -351,7 +351,7 @@
{% 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;">CMA</span>
<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 %}
@@ -394,7 +394,7 @@
<div class="card section">
<h3>Actes CCAM ({{ dossier.actes_ccam|length }})</h3>
<table>
<thead><tr><th>Code CCAM</th><th>Libellé</th><th>Regroupement</th><th>Date</th><th>Validité</th><th>Source</th></tr></thead>
<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>
@@ -407,8 +407,8 @@
</td>
<td>{{ a.date or '' }}</td>
<td>
{% if a.validite == 'valide' %}<span class="badge" style="background:#d1fae5;color:#065f46;">Valide</span>
{% elif a.validite == 'obsolete' %}<span class="badge" style="background:#fee2e2;color:#dc2626;">Obsolète</span>
{% 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>
@@ -491,9 +491,9 @@
<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;">Bloquant</span>
{% elif issue.severity == 'MEDIUM' %}<span class="badge" style="background:#fef3c7;color:#92400e;">À vérifier</span>
{% else %}<span class="badge" style="background:#f0fdf4;color:#166534;">Optimisation</span>{% endif %}
{% 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>
@@ -539,11 +539,11 @@
<span class="badge" style="background:#e0e7ff;color:#3730a3;">{{ ctrl.decision_ucr }}</span>
{% endif %}
{% if ctrl.quality_tier == 'A' %}
<span class="badge" style="background:#2ecc71;color:#fff;font-weight:700;">Qualité A</span>
<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;">Qualité B</span>
<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;">Qualité C</span>
<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>
@@ -697,13 +697,13 @@
<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;">Validé</span>
<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;">Rejeté</span>
<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;">En révision</span>
<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;">Non validé</span>
<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 %}
@@ -745,9 +745,9 @@
<td>{{ b.valeur or '' }}</td>
<td>
{% if b.quality == 'suspect' %}
<span class="badge" style="background:#fef3c7;color:#92400e;" title="{{ b.discard_reason or '' }}">Suspect</span>
<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;">Oui</span>
<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 %}

View File

@@ -1,7 +1,23 @@
{% extends "base.html" %}
{% block title %}Synthese DIM{% endblock %}
{% block title %}Synth&egrave;se DIM{% endblock %}
{% block content %}
<style>
.dim-tooltip { position:relative;cursor:help; }
.dim-tooltip::after {
content:attr(data-tip);
position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);
background:#1e293b;color:#f1f5f9;font-size:0.65rem;font-weight:400;
padding:6px 10px;border-radius:6px;white-space:normal;width:220px;
text-align:center;line-height:1.3;pointer-events:none;opacity:0;
transition:opacity 0.15s;z-index:50;box-shadow:0 2px 8px rgba(0,0,0,0.15);
}
.dim-tooltip:hover::after { opacity:1; }
.dim-legend { display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem;padding-top:0.5rem;border-top:1px solid #e2e8f0;font-size:0.6rem;color:#64748b; }
.dim-legend span { display:inline-flex;align-items:center;gap:0.25rem; }
.dim-legend-dot { width:8px;height:8px;border-radius:50%;display:inline-block; }
</style>
<a class="back" href="/">&larr; Retour</a>
<h2 style="margin-top:1rem;">Synth&egrave;se DIM</h2>
@@ -14,47 +30,54 @@
<div class="card section">
<h3>Diagnostic Principal</h3>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.5rem;margin-bottom:0.75rem;">
<div style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div class="dim-tooltip" data-tip="DP valid&eacute; automatiquement par le pipeline sans modification n&eacute;cessaire" style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#16a34a;">{{ dim.dp.confirmed }}</div>
<div style="font-size:0.65rem;color:#64748b;">CONFIRMED</div>
<div style="font-size:0.65rem;color:#64748b;">Confirm&eacute;s</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div class="dim-tooltip" data-tip="DP n&eacute;cessitant une relecture par le m&eacute;decin DIM (confiance insuffisante ou divergence entre sources)" style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#b45309;">{{ dim.dp.review }}</div>
<div style="font-size:0.65rem;color:#64748b;">REVIEW</div>
<div style="font-size:0.65rem;color:#64748b;">&Agrave; revoir</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#dbeafe;border-radius:6px;">
<div class="dim-tooltip" data-tip="DP dont le code CIM-10 a &eacute;t&eacute; modifi&eacute; par le pipeline (arbitrage, veto ou correction automatique)" style="text-align:center;padding:0.5rem;background:#dbeafe;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#1d4ed8;">{{ dim.dp.modified }}</div>
<div style="font-size:0.65rem;color:#64748b;">Modifi&eacute;s</div>
</div>
</div>
{% if dim.dp.total %}
<div style="font-size:0.75rem;color:#64748b;margin-bottom:0.5rem;">
<div class="dim-tooltip" data-tip="Pourcentage de DP dont le code CIM-10 a &eacute;t&eacute; modifi&eacute; par rapport &agrave; la proposition initiale du LLM" style="font-size:0.75rem;color:#64748b;margin-bottom:0.5rem;display:inline-block;">
Taux de modification DP : <strong style="color:#0f172a;">{{ ((dim.dp.modified / dim.dp.total) * 100) | round(1) }}%</strong>
({{ dim.dp.modified }}/{{ dim.dp.total }})
</div>
{% endif %}
{# Confiance DP #}
<div style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;">Confiance DP</div>
<div class="dim-tooltip" data-tip="R&eacute;partition du niveau de confiance du pipeline sur le DP propos&eacute; (haute, moyenne, basse, inconnue)" style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;display:inline-block;">Niveau de confiance DP</div>
<div style="display:flex;height:20px;border-radius:4px;overflow:hidden;margin-bottom:0.5rem;">
{% set dp_tot = dim.dp.total or 1 %}
{% if dim.dp.confidence.get('high', 0) %}
<div style="width:{{ (dim.dp.confidence.get('high', 0) / dp_tot * 100)|round(1) }}%;background:#16a34a;" title="Haute: {{ dim.dp.confidence.get('high', 0) }}"></div>
<div style="width:{{ (dim.dp.confidence.get('high', 0) / dp_tot * 100)|round(1) }}%;background:#16a34a;" title="Haute : {{ dim.dp.confidence.get('high', 0) }} dossiers ({{ (dim.dp.confidence.get('high', 0) / dp_tot * 100)|round(1) }}%)"></div>
{% endif %}
{% if dim.dp.confidence.get('medium', 0) %}
<div style="width:{{ (dim.dp.confidence.get('medium', 0) / dp_tot * 100)|round(1) }}%;background:#ca8a04;" title="Moyenne: {{ dim.dp.confidence.get('medium', 0) }}"></div>
<div style="width:{{ (dim.dp.confidence.get('medium', 0) / dp_tot * 100)|round(1) }}%;background:#ca8a04;" title="Moyenne : {{ dim.dp.confidence.get('medium', 0) }} dossiers ({{ (dim.dp.confidence.get('medium', 0) / dp_tot * 100)|round(1) }}%)"></div>
{% endif %}
{% if dim.dp.confidence.get('low', 0) %}
<div style="width:{{ (dim.dp.confidence.get('low', 0) / dp_tot * 100)|round(1) }}%;background:#dc2626;" title="Basse: {{ dim.dp.confidence.get('low', 0) }}"></div>
<div style="width:{{ (dim.dp.confidence.get('low', 0) / dp_tot * 100)|round(1) }}%;background:#dc2626;" title="Basse : {{ dim.dp.confidence.get('low', 0) }} dossiers ({{ (dim.dp.confidence.get('low', 0) / dp_tot * 100)|round(1) }}%)"></div>
{% endif %}
{% if dim.dp.confidence.get('none', 0) %}
<div style="width:{{ (dim.dp.confidence.get('none', 0) / dp_tot * 100)|round(1) }}%;background:#94a3b8;" title="Aucune: {{ dim.dp.confidence.get('none', 0) }}"></div>
<div style="width:{{ (dim.dp.confidence.get('none', 0) / dp_tot * 100)|round(1) }}%;background:#94a3b8;" title="Non &eacute;valu&eacute;e : {{ dim.dp.confidence.get('none', 0) }} dossiers ({{ (dim.dp.confidence.get('none', 0) / dp_tot * 100)|round(1) }}%)"></div>
{% endif %}
</div>
{# Légende confiance #}
<div class="dim-legend" style="margin-bottom:0.5rem;">
<span><span class="dim-legend-dot" style="background:#16a34a;"></span> Haute</span>
<span><span class="dim-legend-dot" style="background:#ca8a04;"></span> Moyenne</span>
<span><span class="dim-legend-dot" style="background:#dc2626;"></span> Basse</span>
<span><span class="dim-legend-dot" style="background:#94a3b8;"></span> Non &eacute;valu&eacute;e</span>
</div>
{# Source DP #}
<div style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;">Source DP</div>
<div class="dim-tooltip" data-tip="Origine de la proposition de DP : CIM-10 (r&egrave;gles), LLM (mod&egrave;le IA), Veto (moteur de r&egrave;gles), Autre" style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;display:inline-block;">Source du DP</div>
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;font-size:0.7rem;">
{% for src, cnt in dim.dp.source.items() %}
<span style="padding:2px 8px;border-radius:4px;background:#f1f5f9;color:#334155;">{{ src }}: {{ cnt }}</span>
<span style="padding:2px 8px;border-radius:4px;background:#f1f5f9;color:#334155;">{{ src }} : {{ cnt }}</span>
{% endfor %}
</div>
</div>
@@ -63,19 +86,19 @@
<div class="card section">
<h3>Diagnostics Associ&eacute;s</h3>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.5rem;margin-bottom:0.75rem;">
<div style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div class="dim-tooltip" data-tip="DAS valid&eacute;s et conserv&eacute;s dans le codage final apr&egrave;s analyse du pipeline" style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#16a34a;">{{ dim.das.kept }}</div>
<div style="font-size:0.6rem;color:#64748b;">Conserv&eacute;s</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div class="dim-tooltip" data-tip="DAS dont le niveau de s&eacute;v&eacute;rit&eacute; a &eacute;t&eacute; abaiss&eacute; (ex. confiance high&rarr;medium) mais toujours pr&eacute;sents" style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#b45309;">{{ dim.das.downgraded }}</div>
<div style="font-size:0.6rem;color:#64748b;">D&eacute;grad&eacute;s</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div class="dim-tooltip" data-tip="DAS retir&eacute;s du codage car jug&eacute;s non pertinents ou non &eacute;tay&eacute;s par le dossier m&eacute;dical" style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#dc2626;">{{ dim.das.removed }}</div>
<div style="font-size:0.6rem;color:#64748b;">Supprim&eacute;s</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#f1f5f9;border-radius:6px;">
<div class="dim-tooltip" data-tip="DAS &eacute;cart&eacute;s d&egrave;s le d&eacute;part par les r&egrave;gles ATIH (exclusions CIM-10, doublons, codes interdits)" style="text-align:center;padding:0.5rem;background:#f1f5f9;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#64748b;">{{ dim.das.ruled_out }}</div>
<div style="font-size:0.6rem;color:#64748b;">Exclus</div>
</div>
@@ -114,26 +137,26 @@
{# --- Contestabilité (Vetos) --- #}
<div class="card section">
<h3>Contestabilit&eacute; (Veto Engine)</h3>
<h3>Contestabilit&eacute; (Moteur de r&egrave;gles)</h3>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.5rem;margin-bottom:0.75rem;">
<div style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossiers dont le codage respecte toutes les r&egrave;gles ATIH &mdash; d&eacute;fendables en cas de contr&ocirc;le" style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#16a34a;">{{ dim.veto.distribution.get('PASS', 0) }}</div>
<div style="font-size:0.65rem;color:#64748b;">PASS</div>
<div style="font-size:0.65rem;color:#64748b;">Conforme</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossiers n&eacute;cessitant un compl&eacute;ment d&rsquo;information pour trancher (pi&egrave;ces manquantes, r&eacute;sultats attendus)" style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#b45309;">{{ dim.veto.distribution.get('NEED_INFO', 0) }}</div>
<div style="font-size:0.65rem;color:#64748b;">NEED_INFO</div>
<div style="font-size:0.65rem;color:#64748b;">Info. requise</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossiers dont le codage enfreint au moins une r&egrave;gle ATIH &mdash; risque de rejet ou p&eacute;nalit&eacute; en cas de contr&ocirc;le" style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#dc2626;">{{ dim.veto.distribution.get('FAIL', 0) }}</div>
<div style="font-size:0.65rem;color:#64748b;">FAIL</div>
<div style="font-size:0.65rem;color:#64748b;">Non conforme</div>
</div>
</div>
<div style="font-size:0.75rem;color:#64748b;margin-bottom:0.75rem;">
<div class="dim-tooltip" data-tip="Moyenne sur 100 de la d&eacute;fendabilit&eacute; des codages face &agrave; un contr&ocirc;le externe (CPAM, Cour des comptes)" style="font-size:0.75rem;color:#64748b;margin-bottom:0.75rem;display:inline-block;">
Score moyen de d&eacute;fendabilit&eacute; : <strong style="color:{% if dim.veto.avg_score >= 70 %}#16a34a{% elif dim.veto.avg_score >= 40 %}#b45309{% else %}#dc2626{% endif %};">{{ dim.veto.avg_score }}/100</strong>
</div>
{% if dim.veto.top_issues %}
<div style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;">Alertes les plus fr&eacute;quentes</div>
<div style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.25rem;">R&egrave;gles les plus fr&eacute;quemment enfreintes</div>
{% for veto_id, count in dim.veto.top_issues %}
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;">
<code style="font-size:0.65rem;font-weight:600;min-width:60px;">{{ veto_id }}</code>
@@ -144,32 +167,44 @@
</div>
{% endfor %}
{% endif %}
{# Légende #}
<div class="dim-legend">
<span><span class="dim-legend-dot" style="background:#16a34a;"></span> Conforme = r&egrave;gles ATIH respect&eacute;es</span>
<span><span class="dim-legend-dot" style="background:#ca8a04;"></span> Info. requise = pi&egrave;ces &agrave; compl&eacute;ter</span>
<span><span class="dim-legend-dot" style="background:#dc2626;"></span> Non conforme = codage contestable</span>
</div>
</div>
{# --- Complétude documentaire --- #}
<div class="card section">
<h3>Compl&eacute;tude documentaire</h3>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.5rem;margin-bottom:0.75rem;">
<div style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossier complet : toutes les pi&egrave;ces justificatives sont pr&eacute;sentes pour &eacute;tayer le codage" style="text-align:center;padding:0.5rem;background:#f0fdf4;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#16a34a;">{{ dim.completude.distribution.get('defendable', 0) }}</div>
<div style="font-size:0.6rem;color:#64748b;">D&eacute;fendable</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossier incomplet : certaines pi&egrave;ces manquent, le codage peut &ecirc;tre contest&eacute; en contr&ocirc;le" style="text-align:center;padding:0.5rem;background:#fef3c7;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#b45309;">{{ dim.completude.distribution.get('fragile', 0) }}</div>
<div style="font-size:0.6rem;color:#64748b;">Fragile</div>
</div>
<div style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div class="dim-tooltip" data-tip="Dossier tr&egrave;s incomplet : pi&egrave;ces essentielles absentes, codage ind&eacute;fendable en cas de contr&ocirc;le CPAM" style="text-align:center;padding:0.5rem;background:#fee2e2;border-radius:6px;">
<div style="font-size:1.4rem;font-weight:700;color:#dc2626;">{{ dim.completude.distribution.get('indefendable', 0) }}</div>
<div style="font-size:0.6rem;color:#64748b;">Ind&eacute;fendable</div>
</div>
</div>
<div style="font-size:0.75rem;color:#64748b;margin-bottom:0.75rem;">
Score moyen : <strong style="color:{% if dim.completude.avg_score >= 70 %}#16a34a{% elif dim.completude.avg_score >= 40 %}#b45309{% else %}#dc2626{% endif %};">{{ dim.completude.avg_score }}/100</strong>
<div class="dim-tooltip" data-tip="Score de 0 &agrave; 100 refl&eacute;tant la qualit&eacute; documentaire moyenne des dossiers analys&eacute;s" style="font-size:0.75rem;color:#64748b;margin-bottom:0.75rem;display:inline-block;">
Score moyen de compl&eacute;tude : <strong style="color:{% if dim.completude.avg_score >= 70 %}#16a34a{% elif dim.completude.avg_score >= 40 %}#b45309{% else %}#dc2626{% endif %};">{{ dim.completude.avg_score }}/100</strong>
</div>
{# Légende #}
<div class="dim-legend" style="margin-bottom:0.75rem;">
<span><span class="dim-legend-dot" style="background:#16a34a;"></span> D&eacute;fendable = dossier complet</span>
<span><span class="dim-legend-dot" style="background:#ca8a04;"></span> Fragile = pi&egrave;ces manquantes</span>
<span><span class="dim-legend-dot" style="background:#dc2626;"></span> Ind&eacute;fendable = documentation insuffisante</span>
</div>
{# --- Synthèse CPAM --- #}
{% if dim.cpam.total %}
<div style="border-top:1px solid #e2e8f0;padding-top:0.75rem;margin-top:0.5rem;">
<div style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.5rem;">Contr&ocirc;les CPAM</div>
<div class="dim-tooltip" data-tip="Simulations de contr&ocirc;les CPAM identifi&eacute;s par le pipeline UCR &mdash; impact financier estim&eacute; en cas d&rsquo;indu" style="font-size:0.7rem;font-weight:600;color:#64748b;margin-bottom:0.5rem;display:inline-block;">Contr&ocirc;les CPAM</div>
<div style="display:flex;gap:1rem;font-size:0.75rem;margin-bottom:0.5rem;">
<span><strong>{{ dim.cpam.total }}</strong> contr&ocirc;les</span>
{% if dim.cpam.impact_total %}
@@ -178,7 +213,7 @@
</div>
<div style="display:flex;flex-wrap:wrap;gap:0.4rem;font-size:0.65rem;">
{% for prio, cnt in dim.cpam.by_priority.items() %}
<span style="padding:1px 6px;border-radius:3px;background:{% if prio == 'critique' %}#fee2e2{% elif prio == 'haute' %}#fef3c7{% else %}#f1f5f9{% endif %};color:{% if prio == 'critique' %}#991b1b{% elif prio == 'haute' %}#92400e{% else %}#475569{% endif %};">{{ prio }}: {{ cnt }}</span>
<span style="padding:1px 6px;border-radius:3px;background:{% if prio == 'critique' %}#fee2e2{% elif prio == 'haute' %}#fef3c7{% else %}#f1f5f9{% endif %};color:{% if prio == 'critique' %}#991b1b{% elif prio == 'haute' %}#92400e{% else %}#475569{% endif %};">{{ prio }} : {{ cnt }}</span>
{% endfor %}
</div>
</div>
@@ -195,14 +230,14 @@
{% if dim.alertes.fail %}
<div style="margin-bottom:1rem;">
<div style="font-size:0.7rem;font-weight:700;color:#dc2626;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fecaca;">
Veto FAIL &mdash; Codage contestable ({{ dim.alertes.fail | length }})
<div class="dim-tooltip" data-tip="Dossiers dont le codage enfreint des r&egrave;gles ATIH &mdash; &agrave; corriger en priorit&eacute; avant envoi" style="font-size:0.7rem;font-weight:700;color:#dc2626;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fecaca;display:inline-block;">
Non conformes &mdash; Codage contestable ({{ dim.alertes.fail | length }})
</div>
{% for d in dim.alertes.fail %}
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;font-size:0.75rem;border-bottom:1px solid #f8fafc;">
<a href="/dossier/{{ d.path }}" style="color:#1d4ed8;font-weight:500;min-width:180px;">{{ d.name }}</a>
<span style="color:#64748b;">Score {{ d.score }}/100</span>
<span style="color:#94a3b8;">{{ d.issues }} issues</span>
<span style="color:#94a3b8;">{{ d.issues }} anomalie{{ 's' if d.issues > 1 else '' }}</span>
</div>
{% endfor %}
</div>
@@ -210,8 +245,8 @@
{% if dim.alertes.review %}
<div style="margin-bottom:1rem;">
<div style="font-size:0.7rem;font-weight:700;color:#b45309;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fef3c7;">
DP en REVIEW &mdash; &Agrave; valider ({{ dim.alertes.review | length }})
<div class="dim-tooltip" data-tip="DP dont la confiance est insuffisante &mdash; le m&eacute;decin DIM doit valider ou corriger le code propos&eacute;" style="font-size:0.7rem;font-weight:700;color:#b45309;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fef3c7;display:inline-block;">
DP &agrave; revoir &mdash; Validation requise ({{ dim.alertes.review | length }})
</div>
{% for d in dim.alertes.review %}
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;font-size:0.75rem;border-bottom:1px solid #f8fafc;">
@@ -225,14 +260,14 @@
{% if dim.alertes.indefendable %}
<div>
<div style="font-size:0.7rem;font-weight:700;color:#dc2626;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fecaca;">
Compl&eacute;tude ind&eacute;fendable ({{ dim.alertes.indefendable | length }})
<div class="dim-tooltip" data-tip="Dossiers dont la documentation est trop lacunaire pour justifier le codage en cas de contr&ocirc;le" style="font-size:0.7rem;font-weight:700;color:#dc2626;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.35rem;padding-bottom:0.2rem;border-bottom:1px solid #fecaca;display:inline-block;">
Documentation ind&eacute;fendable ({{ dim.alertes.indefendable | length }})
</div>
{% for d in dim.alertes.indefendable %}
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;font-size:0.75rem;border-bottom:1px solid #f8fafc;">
<a href="/dossier/{{ d.path }}" style="color:#1d4ed8;font-weight:500;min-width:180px;">{{ d.name }}</a>
<span style="color:#64748b;">Score {{ d.score }}/100</span>
<span style="color:#94a3b8;">{{ d.manquants }} docs manquants</span>
<span style="color:#94a3b8;">{{ d.manquants }} pi&egrave;ce{{ 's' if d.manquants > 1 else '' }} manquante{{ 's' if d.manquants > 1 else '' }}</span>
</div>
{% endfor %}
</div>

View File

@@ -7,12 +7,12 @@
{% if stats %}
<div class="card" style="margin-bottom:1.5rem;padding:0.75rem 1.25rem;display:flex;flex-wrap:wrap;gap:1rem;align-items:center;">
<span style="font-size:0.8rem;font-weight:700;color:#475569;">Vue globale</span>
<span class="badge-count badge-das">{{ stats.total_dossiers }} dossiers</span>
<span class="badge-count badge-das">{{ stats.total_das }} DAS</span>
<span class="badge-count badge-actes">{{ stats.total_actes }} actes</span>
{% if stats.total_alertes %}<span class="badge-count badge-alertes">{{ stats.total_alertes }} alertes</span>{% endif %}
{% if stats.total_cma %}<span class="badge-count badge-cma">{{ stats.total_cma }} CMA</span>{% endif %}
{% if stats.total_cpam %}<span class="badge-count" style="background:#fef3c7;color:#92400e;">{{ stats.total_cpam }} CPAM</span>{% endif %}
<span class="badge-count badge-das" title="Nombre total de dossiers patients analysés par le pipeline">{{ stats.total_dossiers }} dossiers</span>
<span class="badge-count badge-das" title="Diagnostics Associés Significatifs — codes CIM-10 secondaires identifiés dans tous les dossiers">{{ stats.total_das }} DAS</span>
<span class="badge-count badge-actes" title="Actes CCAM (Classification Commune des Actes Médicaux) identifiés dans tous les dossiers">{{ stats.total_actes }} actes</span>
{% if stats.total_alertes %}<span class="badge-count badge-alertes" title="Alertes de codage — anomalies, incohérences ou points de vigilance détectés par le pipeline IA et le moteur de règles ATIH">{{ stats.total_alertes }} alertes</span>{% endif %}
{% if stats.total_cma %}<span class="badge-count badge-cma" title="Comorbidités et Morbidités Associées — diagnostics secondaires qui augmentent le niveau de sévérité du GHM et donc la valorisation T2A">{{ stats.total_cma }} CMA</span>{% endif %}
{% if stats.total_cpam %}<span class="badge-count" style="background:#fef3c7;color:#92400e;" title="Contrôles UCR (Unité de Coordination Régionale) simulés — scénarios de contestation CPAM avec contre-argumentation générée">{{ stats.total_cpam }} CPAM</span>{% endif %}
{% if stats.processing_time_total %}
<span style="font-size:0.75rem;color:#64748b;">Total : {{ stats.processing_time_total|format_duration }}</span>
{% endif %}
@@ -32,12 +32,12 @@
<thead>
<tr>
<th>Patient</th>
<th>DP</th>
<th>DAS</th>
<th>Actes</th>
<th>Sévérité</th>
<th>Alertes</th>
<th>CPAM</th>
<th title="Diagnostic Principal — code CIM-10 principal du séjour">DP</th>
<th title="Diagnostics Associés Significatifs — codes CIM-10 secondaires">DAS</th>
<th title="Actes CCAM — actes médicaux et chirurgicaux codés">Actes</th>
<th title="Niveau de sévérité GHM (1=léger, 4=très sévère) — détermine la valorisation T2A">Sévérité</th>
<th title="Alertes de codage — anomalies ou points de vigilance détectés">Alertes</th>
<th title="Contrôles UCR/CPAM simulés — scénarios de contestation">CPAM</th>
</tr>
</thead>
<tbody>
@@ -81,14 +81,14 @@
</td>
<td>
{% if gstats.das_count is defined and gstats.das_count > 0 %}
<span class="badge-count badge-das">{{ gstats.das_count }}</span>
<span class="badge-count badge-das" title="Diagnostics Associés Significatifs pour ce dossier">{{ gstats.das_count }}</span>
{% else %}
<span style="color:#cbd5e1;">0</span>
{% endif %}
</td>
<td>
{% if gstats.actes_count is defined and gstats.actes_count > 0 %}
<span class="badge-count badge-actes">{{ gstats.actes_count }}</span>
<span class="badge-count badge-actes" title="Actes CCAM identifiés dans ce dossier">{{ gstats.actes_count }}</span>
{% else %}
<span style="color:#cbd5e1;">0</span>
{% endif %}
@@ -102,14 +102,14 @@
</td>
<td>
{% if gstats.alertes_count is defined and gstats.alertes_count > 0 %}
<span class="badge-count badge-alertes">{{ gstats.alertes_count }}</span>
<span class="badge-count badge-alertes" title="Alertes de codage détectées dans ce dossier">{{ gstats.alertes_count }}</span>
{% else %}
<span style="color:#cbd5e1;">0</span>
{% endif %}
</td>
<td>
{% if d.controles_cpam %}
<span class="badge-count" style="background:#fef3c7;color:#92400e;">{{ d.controles_cpam|length }}</span>
<span class="badge-count" style="background:#fef3c7;color:#92400e;" title="Contrôles UCR/CPAM simulés pour ce dossier">{{ d.controles_cpam|length }}</span>
{% else %}
<span style="color:#cbd5e1;"></span>
{% endif %}

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from unittest.mock import patch, MagicMock
@@ -74,13 +75,13 @@ class TestReferentielManager:
def test_file_stored_on_disk(self, manager, tmp_path):
ref = manager.add_file("test.txt", b"file content here")
stored_path = manager._dir / ref["stored_name"]
stored_path = manager._base / ref["stored_name"]
assert stored_path.exists()
assert stored_path.read_bytes() == b"file content here"
def test_remove_deletes_file(self, manager):
ref = manager.add_file("test.txt", b"content")
stored_path = manager._dir / ref["stored_name"]
stored_path = manager._base / ref["stored_name"]
assert stored_path.exists()
manager.remove(ref["id"])
assert not stored_path.exists()
@@ -144,9 +145,10 @@ class TestReferentielRoutes:
def app(self, tmp_path):
"""Crée une app Flask de test avec un manager temporaire."""
from src.viewer.app import create_app
app = create_app()
app.config["TESTING"] = True
return app
with patch.dict(os.environ, {"T2A_DEMO_USER": "", "T2A_DEMO_PASS": ""}):
app = create_app()
app.config["TESTING"] = True
yield app
@pytest.fixture
def client(self, app):
@@ -155,7 +157,7 @@ class TestReferentielRoutes:
def test_admin_page_loads(self, client):
resp = client.get("/admin/referentiels")
assert resp.status_code == 200
assert "Référentiels RAG" in resp.data.decode()
assert "Referentiels" in resp.data.decode()
def test_upload_no_file(self, client):
resp = client.post("/admin/referentiels/upload")