Files
opencare/templates/admin/library.html
2026-05-07 21:02:31 +02:00

488 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% block title %}Bibliothèque — Admin OpenCare{% endblock %}
{% block content %}
<div class="admin-layout">
{% include 'admin/_sidebar.html' %}
<div class="admin-main admin-main--cockpit">
{# ── Header ─────────────────────────────────────────────────────── #}
<div class="lib-header px-4 py-3 border-bottom bg-white d-flex justify-content-between align-items-center flex-shrink-0">
<div>
<h1 class="admin-page-title mb-0">
<i class="fas fa-book-medical me-2 text-accent"></i>Bibliothèque
</h1>
<p class="text-muted mb-0 small">{{ recs|length }} recommandation(s) approuvée(s) et active(s)</p>
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex gap-1 align-items-center">
<span class="evidence-badge ev-A">A <span class="ms-1 opacity-75">{{ ev_total.A }}</span></span>
<span class="evidence-badge ev-B">B <span class="ms-1 opacity-75">{{ ev_total.B }}</span></span>
<span class="evidence-badge ev-C">C <span class="ms-1 opacity-75">{{ ev_total.C }}</span></span>
</div>
<a href="{{ url_for('admin_review', status='pending') }}" class="btn btn-sm btn-primary">
<i class="fas fa-inbox me-1"></i>File d'attente
</a>
</div>
</div>
{% if recs %}
{# ── Cockpit body ────────────────────────────────────────────────── #}
<div class="lib-cockpit">
{# ── Left tree nav ──────────────────────────────────────────────── #}
<div class="lib-tree">
<div class="p-2">
<button class="lib-tree-node active" data-filter-domain="" data-filter-tag="">
<span><i class="fas fa-layer-group me-2 opacity-50" style="font-size:.8rem;"></i>Toutes</span>
<div class="d-flex align-items-center gap-1 flex-shrink-0">
{% if ev_total.A %}<span class="lib-tree-ev ev-A">{{ ev_total.A }}</span>{% endif %}
{% if ev_total.B %}<span class="lib-tree-ev ev-B">{{ ev_total.B }}</span>{% endif %}
{% if ev_total.C %}<span class="lib-tree-ev ev-C">{{ ev_total.C }}</span>{% endif %}
<span class="badge bg-secondary rounded-pill ms-1" style="font-size:.65rem;">{{ recs|length }}</span>
</div>
</button>
<div class="my-2" style="border-top:1px solid var(--border);"></div>
{% for domain_key, label in domain_labels.items() %}
{% if domain_key in tree %}
{% set node = tree[domain_key] %}
<div class="mb-1">
<button class="lib-tree-node lib-tree-domain-btn"
data-filter-domain="{{ domain_key }}" data-filter-tag=""
{% if node.tags %}data-bs-toggle="collapse" data-bs-target="#tt-{{ domain_key }}"{% endif %}>
<span class="text-truncate">{{ label[1] }}&nbsp;{{ label[0] }}</span>
<div class="d-flex align-items-center gap-1 flex-shrink-0">
{% if node.ev.A %}<span class="lib-tree-ev ev-A">{{ node.ev.A }}</span>{% endif %}
{% if node.ev.B %}<span class="lib-tree-ev ev-B">{{ node.ev.B }}</span>{% endif %}
{% if node.ev.C %}<span class="lib-tree-ev ev-C">{{ node.ev.C }}</span>{% endif %}
<span class="badge bg-secondary rounded-pill ms-1" style="font-size:.65rem;">{{ node.recs|length }}</span>
{% if node.tags %}<i class="fas fa-chevron-right lib-tree-chevron ms-1 text-muted" style="font-size:.6rem;transition:transform .2s;"></i>{% endif %}
</div>
</button>
{% if node.tags %}
<div class="collapse" id="tt-{{ domain_key }}">
{% for tag, tnode in node.tags.items()|sort %}
<button class="lib-tree-node lib-tree-tag"
data-filter-domain="{{ domain_key }}" data-filter-tag="{{ tag }}">
<span class="text-truncate">
<i class="fas fa-hashtag me-1 opacity-40" style="font-size:.6rem;"></i>{{ tag }}
</span>
<div class="d-flex align-items-center gap-1 flex-shrink-0">
{% if tnode.ev.A %}<span class="lib-tree-ev ev-A">{{ tnode.ev.A }}</span>{% endif %}
{% if tnode.ev.B %}<span class="lib-tree-ev ev-B">{{ tnode.ev.B }}</span>{% endif %}
{% if tnode.ev.C %}<span class="lib-tree-ev ev-C">{{ tnode.ev.C }}</span>{% endif %}
<span class="badge bg-secondary rounded-pill ms-1" style="font-size:.65rem;">{{ tnode.recs|length }}</span>
</div>
</button>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{# ── Right cards panel ──────────────────────────────────────────── #}
<div class="lib-cards">
<div id="lib-empty" class="text-center py-5 text-muted d-none">
<i class="fas fa-filter fa-2x mb-3 opacity-25"></i>
<p class="mb-0">Aucune recommandation dans cette catégorie.</p>
</div>
{% for rec in recs %}
{% set is_universal = not rec.required_conditions and not rec.required_health_goals and not rec.required_diet_types and not rec.required_activity_levels %}
{% set has_gates = rec.not_for_under_18 or rec.not_for_pregnant or (rec.target_sex and rec.target_sex != 'all') or rec.target_age_min or rec.target_age_max or rec.excluded_conditions %}
<div class="lib-card mb-2"
data-domain="{{ rec.domain }}"
data-tags="{{ rec.tags | join(' ') | lower }}"
id="rec-{{ rec.id }}">
{# Card header — click to expand #}
<div class="lib-card-head d-flex align-items-start gap-2 p-3"
data-bs-toggle="collapse" data-bs-target="#cb-{{ rec.id }}"
aria-expanded="false" aria-controls="cb-{{ rec.id }}">
<span class="evidence-badge ev-{{ rec.evidence_level }} flex-shrink-0" style="margin-top:2px;">{{ rec.evidence_level }}</span>
<div class="flex-grow-1 min-w-0">
<div class="fw-semibold" style="line-height:1.3;">{{ rec.title }}</div>
<div class="d-flex flex-wrap gap-1 mt-1">
{% if is_universal %}
<span class="tag-pill tag-pill-green">🌍 Universel</span>
{% else %}
<span class="tag-pill tag-pill-blue">🎯 Ciblé</span>
{% endif %}
{% if rec.target_sex == 'male' %}<span class="tag-pill">♂ Hommes</span>
{% elif rec.target_sex == 'female' %}<span class="tag-pill">♀ Femmes</span>{% endif %}
{% if rec.target_age_min or rec.target_age_max %}
<span class="tag-pill">{{ rec.target_age_min or '?' }}{{ rec.target_age_max or '∞' }} ans</span>
{% endif %}
{% for t in rec.tags[:4] %}<span class="tag-pill">{{ t }}</span>{% endfor %}
</div>
</div>
<i class="fas fa-chevron-down lib-chevron text-muted flex-shrink-0" style="margin-top:4px;"></i>
</div>
{# Card body — collapsible #}
<div class="collapse" id="cb-{{ rec.id }}">
<div class="lib-card-body border-top px-3 pb-3 pt-2">
{% if rec.summary %}
<p class="text-muted small mb-2">{{ rec.summary }}</p>
{% endif %}
{% if rec.details %}
<p class="small mb-3 text-secondary">{{ rec.details }}</p>
{% endif %}
{# Intervention + Effect #}
{% if rec.intervention or rec.effect %}
<div class="row g-2 mb-3">
{% if rec.intervention %}
<div class="col-md-6">
<div class="p-2 rounded-2" style="background:#f0fdf4;border:1px solid #bbf7d0;">
<div class="small fw-semibold text-success mb-1"><i class="fas fa-play-circle me-1"></i>Intervention</div>
<div class="small">{{ rec.intervention }}</div>
</div>
</div>
{% endif %}
{% if rec.effect %}
<div class="col-md-6">
<div class="p-2 rounded-2" style="background:#eff6ff;border:1px solid #bfdbfe;">
<div class="small fw-semibold text-primary mb-1"><i class="fas fa-chart-line me-1"></i>Effet attendu</div>
<div class="small">{{ rec.effect }}</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
{# Source citation #}
{% if rec.source_title %}
<div class="small text-muted mb-3 p-2 rounded-2" style="background:#f8fafc;border:1px solid var(--border);">
<i class="fas fa-book me-1"></i>
<em>{{ rec.source_title }}</em>
{% if rec.source_doi %} — <code class="small">{{ rec.source_doi }}</code>{% endif %}
{% if rec.study_type %}<span class="tag-pill ms-1">{{ rec.study_type.replace('_',' ') }}</span>{% endif %}
</div>
{% endif %}
{# ── Targeting breakdown ─────────────────────────────────── #}
<div class="targeting-panel p-3 mb-3">
<div class="d-flex align-items-center gap-2 mb-2 flex-wrap">
<span class="text-uppercase fw-bold text-muted" style="font-size:.65rem;letter-spacing:.06em;">Ciblage profil</span>
{% if is_universal %}
<span class="tag-pill tag-pill-green">🌍 Universel — affiché à tous</span>
{% else %}
<span class="tag-pill tag-pill-blue">🎯 Conditionnel</span>
{% endif %}
</div>
{% if has_gates %}
<div class="mb-2">
<div class="small fw-semibold text-danger mb-1" style="font-size:.75rem;">
<i class="fas fa-ban me-1"></i>Exclusions (portes dures — toutes doivent passer)
</div>
<div class="d-flex flex-wrap gap-1">
{% if rec.not_for_under_18 %}<span class="tag-pill tag-pill-red">✗ Pas &lt; 18 ans</span>{% endif %}
{% if rec.not_for_pregnant %}<span class="tag-pill tag-pill-red">✗ Pas grossesse</span>{% endif %}
{% if rec.target_sex == 'male' %}<span class="tag-pill tag-pill-red">♂ Hommes uniquement</span>{% endif %}
{% if rec.target_sex == 'female' %}<span class="tag-pill tag-pill-red">♀ Femmes uniquement</span>{% endif %}
{% if rec.target_age_min %}<span class="tag-pill tag-pill-red">≥ {{ rec.target_age_min }} ans</span>{% endif %}
{% if rec.target_age_max %}<span class="tag-pill tag-pill-red">≤ {{ rec.target_age_max }} ans</span>{% endif %}
{% for c in rec.excluded_conditions %}
<span class="tag-pill tag-pill-red">Exclu si: {{ condition_labels.get(c, c) }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if not is_universal %}
<div>
<div class="small fw-semibold text-primary mb-1" style="font-size:.75rem;">
<i class="fas fa-bolt me-1"></i>Déclencheurs — logique OU (un groupe suffit)
</div>
<div class="d-flex flex-wrap gap-1">
{% for c in rec.required_conditions %}
<span class="tag-pill tag-pill-blue">Si: {{ condition_labels.get(c, c) }}</span>
{% endfor %}
{% for g in rec.required_health_goals %}
<span class="tag-pill tag-pill-blue">Objectif: {{ goal_labels.get(g, g) }}</span>
{% endfor %}
{% for d in rec.required_diet_types %}
<span class="tag-pill tag-pill-blue">Régime: {{ diet_labels.get(d, d) }}</span>
{% endfor %}
{% for a in rec.required_activity_levels %}
<span class="tag-pill tag-pill-blue">Activité: {{ activity_labels.get(a, a) }}</span>
{% endfor %}
</div>
</div>
{% else %}
<p class="text-muted small mb-0">
<i class="fas fa-info-circle me-1"></i>Aucun critère de déclenchement — affichée à tous les utilisateurs (sous réserve des exclusions ci-dessus si définies).
</p>
{% endif %}
</div>
{# Actions #}
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal" data-bs-target="#modal-{{ rec.id }}">
<i class="fas fa-sliders-h me-1"></i>Modifier le ciblage
</button>
<form action="{{ url_for('admin_library_delete', rid=rec.id) }}" method="post"
onsubmit="return confirm('Archiver cette recommandation ?')">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-archive me-1"></i>Archiver
</button>
</form>
</div>
</div>
</div>
</div>
{# ── Edit targeting modal ───────────────────────────────────── #}
<div class="modal fade" id="modal-{{ rec.id }}" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form action="{{ url_for('admin_library_update', rid=rec.id) }}" method="POST">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-sliders-h me-2"></i>Ciblage
<span class="text-muted fw-normal small ms-1">— {{ rec.title[:60] }}{% if rec.title|length > 60 %}…{% endif %}</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{# Demographics #}
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label small fw-semibold">Sexe cible</label>
<select name="target_sex" class="form-select form-select-sm">
<option value="all" {{ 'selected' if rec.target_sex in ('all', None, '') }}>Tous</option>
<option value="male" {{ 'selected' if rec.target_sex == 'male' }}>Hommes ♂</option>
<option value="female" {{ 'selected' if rec.target_sex == 'female' }}>Femmes ♀</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Âge min</label>
<input type="number" name="target_age_min" class="form-control form-control-sm"
value="{{ rec.target_age_min or '' }}" placeholder="—" min="0" max="120">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Âge max</label>
<input type="number" name="target_age_max" class="form-control form-control-sm"
value="{{ rec.target_age_max or '' }}" placeholder="—" min="0" max="120">
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label small fw-semibold">Priorité (110)</label>
<input type="number" name="priority" class="form-control form-control-sm"
value="{{ rec.priority }}" min="1" max="10">
</div>
<div class="col-md-8 d-flex align-items-end gap-4 pb-1">
<div class="form-check">
<input type="checkbox" name="not_for_under_18" class="form-check-input"
id="u18-{{ rec.id }}" {{ 'checked' if rec.not_for_under_18 }}>
<label class="form-check-label small" for="u18-{{ rec.id }}">
<span class="tag-pill tag-pill-red">Exclure &lt; 18 ans</span>
</label>
</div>
<div class="form-check">
<input type="checkbox" name="not_for_pregnant" class="form-check-input"
id="preg-{{ rec.id }}" {{ 'checked' if rec.not_for_pregnant }}>
<label class="form-check-label small" for="preg-{{ rec.id }}">
<span class="tag-pill tag-pill-red">Exclure grossesse</span>
</label>
</div>
</div>
</div>
<hr class="my-3">
{# Conditions #}
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-semibold text-danger">
<i class="fas fa-ban me-1"></i>Conditions exclues
<span class="text-muted fw-normal">(porte dure)</span>
</label>
<div class="multi-check-grid">
{% for c in conditions %}
<label class="multi-check-item form-check">
<input type="checkbox" name="excluded_conditions" value="{{ c }}"
class="form-check-input" {{ 'checked' if c in rec.excluded_conditions }}>
{{ condition_labels.get(c, c) }}
</label>
{% endfor %}
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold text-primary">
<i class="fas fa-bolt me-1"></i>Conditions déclenchantes
<span class="text-muted fw-normal">(OU)</span>
</label>
<div class="multi-check-grid">
{% for c in conditions %}
<label class="multi-check-item form-check">
<input type="checkbox" name="required_conditions" value="{{ c }}"
class="form-check-input" {{ 'checked' if c in rec.required_conditions }}>
{{ condition_labels.get(c, c) }}
</label>
{% endfor %}
</div>
</div>
</div>
{# Goals + Diet #}
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-semibold text-primary">
<i class="fas fa-bullseye me-1"></i>Objectifs santé
<span class="text-muted fw-normal">(OU)</span>
</label>
<div class="multi-check-grid">
{% for g in health_goals %}
<label class="multi-check-item form-check">
<input type="checkbox" name="required_health_goals" value="{{ g }}"
class="form-check-input" {{ 'checked' if g in rec.required_health_goals }}>
{{ goal_labels.get(g, g) }}
</label>
{% endfor %}
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold text-primary">
<i class="fas fa-utensils me-1"></i>Types de régime
<span class="text-muted fw-normal">(OU)</span>
</label>
<div class="multi-check-grid">
{% for d in diet_types %}
<label class="multi-check-item form-check">
<input type="checkbox" name="required_diet_types" value="{{ d }}"
class="form-check-input" {{ 'checked' if d in rec.required_diet_types }}>
{{ diet_labels.get(d, d) }}
</label>
{% endfor %}
</div>
</div>
</div>
{# Activity #}
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-semibold text-primary">
<i class="fas fa-running me-1"></i>Niveaux d'activité
<span class="text-muted fw-normal">(OU)</span>
</label>
<div class="multi-check-grid">
{% for a in activity_levels %}
<label class="multi-check-item form-check">
<input type="checkbox" name="required_activity_levels" value="{{ a }}"
class="form-check-input" {{ 'checked' if a in rec.required_activity_levels }}>
{{ activity_labels.get(a, a) }}
</label>
{% endfor %}
</div>
</div>
</div>
<hr class="my-3">
<div>
<label class="form-label small fw-semibold">Notes admin</label>
<textarea name="admin_notes" class="form-control form-control-sm" rows="2"
placeholder="Notes internes (non visibles par les utilisateurs)">{{ rec.admin_notes or '' }}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-sm btn-primary">
<i class="fas fa-save me-1"></i>Enregistrer le ciblage
</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="text-center py-5 text-muted">
<i class="fas fa-book-medical fa-3x mb-3 opacity-25"></i>
<p class="mb-0">Aucune recommandation approuvée. Allez dans la file d'attente pour en valider.</p>
<a href="{{ url_for('admin_review', status='pending') }}" class="btn btn-primary mt-3">
<i class="fas fa-inbox me-1"></i>File d'attente
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
// ── Tree filtering ────────────────────────────────────────────────
document.querySelectorAll('.lib-tree-node').forEach(function (btn) {
btn.addEventListener('click', function () {
var domain = this.dataset.filterDomain;
var tag = this.dataset.filterTag;
document.querySelectorAll('.lib-tree-node').forEach(function (b) {
b.classList.remove('active');
});
this.classList.add('active');
var visible = 0;
document.querySelectorAll('.lib-card').forEach(function (card) {
var show;
if (!domain) {
show = true;
} else if (!tag) {
show = card.dataset.domain === domain;
} else {
var tags = (card.dataset.tags || '').split(' ');
show = card.dataset.domain === domain && tags.indexOf(tag) !== -1;
}
card.style.display = show ? '' : 'none';
if (show) visible++;
});
var emptyEl = document.getElementById('lib-empty');
if (emptyEl) emptyEl.classList.toggle('d-none', visible > 0);
});
});
// ── Tree chevron rotation on collapse ────────────────────────────
document.querySelectorAll('.lib-tree-domain-btn[data-bs-toggle="collapse"]').forEach(function (btn) {
var target = document.querySelector(btn.dataset.bsTarget);
if (!target) return;
var chevron = btn.querySelector('.lib-tree-chevron');
if (!chevron) return;
target.addEventListener('show.bs.collapse', function () {
chevron.style.transform = 'rotate(90deg)';
});
target.addEventListener('hide.bs.collapse', function () {
chevron.style.transform = '';
});
});
})();
</script>
{% endblock %}