New cockpit

This commit is contained in:
2026-05-07 20:47:49 +02:00
parent 474f18404f
commit 0bca327b80
4 changed files with 656 additions and 60 deletions

View File

@@ -4,7 +4,8 @@
"Bash(git remote *)",
"Bash(git push *)",
"Bash(git commit -m 'Add Dockerfile for Docker production deployment *)",
"Bash(git commit -m 'Auto-seed default sources on first startup *)"
"Bash(git commit -m 'Auto-seed default sources on first startup *)",
"Bash(py -c \"from app import app; print\\('OK'\\)\")"
]
}
}

93
app.py
View File

@@ -46,6 +46,46 @@ _DOMAIN_LABELS = {
'other': ('Autre', '📌'),
}
_CONDITION_LABELS = {
'hypertension': 'Hypertension',
'diabetes_risk': 'Risque diabète',
'cardiovascular_risk': 'Risque cardio-vasculaire',
'anxiety': 'Anxiété',
'digestive_issues': 'Troubles digestifs',
'back_pain': 'Douleurs dorsales',
'osteoporosis_risk': 'Risque ostéoporose',
'metabolic_syndrome': 'Syndrome métabolique',
'thyroid': 'Thyroïde',
'sleep_apnea': 'Apnée du sommeil',
}
_GOAL_LABELS = {
'weight_loss': 'Perte de poids',
'muscle_gain': 'Prise de muscle',
'longevity': 'Longévité',
'energy': 'Énergie',
'better_sleep': 'Améliorer le sommeil',
'stress_reduction': 'Réduction du stress',
'metabolic_health': 'Santé métabolique',
'cardiovascular_health': 'Santé cardio-vasculaire',
'cognitive_health': 'Santé cognitive',
}
_DIET_LABELS = {
'omnivore': 'Omnivore',
'flexitarian': 'Flexitarien',
'vegetarian': 'Végétarien',
'vegan': 'Végétalien',
'paleo': 'Paléo',
'keto': 'Kéto',
'other': 'Autre',
}
_ACTIVITY_LABELS = {
'sedentary': 'Sédentaire',
'light': 'Légère activité',
'moderate': 'Activité modérée',
'intense': 'Activité intense',
'athlete': 'Athlète',
}
# ── Models ─────────────────────────────────────────────────────────────────────
@@ -796,9 +836,37 @@ def admin_review_update(rid):
@login_required
@admin_required
def admin_library():
from pipeline import _CONDITIONS, _HEALTH_GOALS, _DIET_TYPES, _ACTIVITY_LEVELS
recs = PendingRecommendation.query.filter_by(status='approved') \
.order_by(PendingRecommendation.domain, PendingRecommendation.priority.desc()).all()
return render_template('admin/library.html', recs=recs, domain_labels=_DOMAIN_LABELS)
tree = {}
ev_total = {'A': 0, 'B': 0, 'C': 0}
for rec in recs:
d = rec.domain
ev = rec.evidence_level if rec.evidence_level in ('A', 'B', 'C') else 'C'
ev_total[ev] += 1
if d not in tree:
tree[d] = {'recs': [], 'tags': {}, 'ev': {'A': 0, 'B': 0, 'C': 0}}
tree[d]['recs'].append(rec)
tree[d]['ev'][ev] += 1
for tag in (rec.tags or []):
tag = tag.strip().lower()
if not tag:
continue
if tag not in tree[d]['tags']:
tree[d]['tags'][tag] = {'recs': [], 'ev': {'A': 0, 'B': 0, 'C': 0}}
tree[d]['tags'][tag]['recs'].append(rec)
tree[d]['tags'][tag]['ev'][ev] += 1
return render_template('admin/library.html',
recs=recs, tree=tree,
domain_labels=_DOMAIN_LABELS, ev_total=ev_total,
conditions=_CONDITIONS, health_goals=_HEALTH_GOALS,
diet_types=_DIET_TYPES, activity_levels=_ACTIVITY_LEVELS,
condition_labels=_CONDITION_LABELS, goal_labels=_GOAL_LABELS,
diet_labels=_DIET_LABELS, activity_labels=_ACTIVITY_LABELS)
@app.route('/admin/library/<int:rid>/delete', methods=['POST'])
@@ -814,6 +882,29 @@ def admin_library_delete(rid):
return redirect(url_for('admin_library'))
@app.route('/admin/library/<int:rid>/update', methods=['POST'])
@login_required
@admin_required
def admin_library_update(rid):
rec = db.get_or_404(PendingRecommendation, rid)
f = request.form
rec.target_sex = f.get('target_sex', 'all')
rec.target_age_min = int(f['target_age_min']) if f.get('target_age_min') else None
rec.target_age_max = int(f['target_age_max']) if f.get('target_age_max') else None
rec.not_for_under_18 = 'not_for_under_18' in f
rec.not_for_pregnant = 'not_for_pregnant' in f
rec.required_conditions = f.getlist('required_conditions')
rec.excluded_conditions = f.getlist('excluded_conditions')
rec.required_health_goals = f.getlist('required_health_goals')
rec.required_diet_types = f.getlist('required_diet_types')
rec.required_activity_levels = f.getlist('required_activity_levels')
rec.priority = int(f.get('priority', rec.priority))
rec.admin_notes = f.get('admin_notes', '').strip()
db.session.commit()
flash(f'Ciblage de « {rec.title} » mis à jour.', 'success')
return redirect(url_for('admin_library'))
@app.route('/admin/config', methods=['GET', 'POST'])
@login_required
@admin_required

View File

@@ -563,6 +563,112 @@ body {
border: 1px solid #fde68a;
}
/* ── Library cockpit ──────────────────────────────────────────── */
.admin-main--cockpit {
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.lib-header {
flex-shrink: 0;
}
.lib-cockpit {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
.lib-tree {
width: 240px;
flex-shrink: 0;
overflow-y: auto;
background: #fff;
border-right: 1px solid var(--border);
}
.lib-cards {
flex: 1;
overflow-y: auto;
padding: 1rem 1.5rem;
background: var(--bg);
}
.lib-tree-node {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.4rem 0.6rem;
border-radius: var(--radius-sm);
border: none;
background: none;
text-align: left;
font-size: 0.82rem;
font-weight: 500;
color: var(--muted);
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.lib-tree-node:hover { background: #f1f5f9; color: var(--text); }
.lib-tree-node.active { background: #eff6ff; color: var(--primary); font-weight: 600; }
.lib-tree-tag { padding-left: 1.4rem; font-size: 0.79rem; }
.lib-tree-ev {
display: inline-block;
min-width: 1.1rem;
text-align: center;
font-size: 0.63rem;
font-weight: 700;
border-radius: 3px;
padding: 0.05rem 0.2rem;
}
.lib-tree-ev.ev-A { background: #dcfce7; color: #166534; }
.lib-tree-ev.ev-B { background: #fef9c3; color: #854d0e; }
.lib-tree-ev.ev-C { background: #ffedd5; color: #9a3412; }
.lib-card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: box-shadow 0.15s;
}
.lib-card:hover { box-shadow: var(--shadow); }
.lib-card-head {
cursor: pointer;
user-select: none;
transition: background 0.12s;
}
.lib-card-head:hover { background: #f8fafc; }
.lib-card-head[aria-expanded="true"] { background: #f8fafc; }
.lib-card-head[aria-expanded="true"] .lib-chevron { transform: rotate(180deg); }
.lib-chevron { transition: transform 0.2s; }
.targeting-panel {
background: #f8fafc;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.multi-check-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.25rem;
}
.multi-check-item {
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.3rem 0.5rem 0.3rem 1.75rem;
margin: 0;
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
font-size: 0.79rem;
line-height: 1.3;
}
.multi-check-item:has(input:checked) {
background: #eff6ff;
border-color: var(--primary);
color: var(--primary);
}
/* ── Crawl log terminal ───────────────────────────────────────── */
.crawl-log-box {
background: #0f172a;

View File

@@ -5,85 +5,483 @@
<div class="admin-layout">
{% include 'admin/_sidebar.html' %}
<div class="admin-main">
<div class="admin-page-header">
<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">
<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">{{ recs|length }} recommandation(s) approuvée(s) et active(s)</p>
<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 %}
{% set current_domain = namespace(val='') %}
{% for rec in recs %}
{% if rec.domain != current_domain.val %}
{% set current_domain.val = rec.domain %}
<div class="admin-domain-divider">
{{ domain_labels.get(rec.domain, (rec.domain, ''))[1] }}
{{ domain_labels.get(rec.domain, (rec.domain, ''))[0] }}
<span class="text-muted ms-1 fw-normal small">
({{ recs|selectattr('domain', 'equalto', rec.domain)|list|length }})
</span>
</div>
{% endif %}
{# ── Cockpit body ────────────────────────────────────────────────── #}
<div class="lib-cockpit">
<div class="card border-0 shadow-sm rounded-3 mb-2">
<div class="card-body p-3">
<div class="d-flex align-items-start gap-3">
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
<span class="evidence-badge ev-{{ rec.evidence_level }}">Niveau {{ rec.evidence_level }}</span>
<span class="badge bg-light text-muted border small">Priorité {{ rec.priority }}</span>
<span class="text-muted small">{{ rec.reviewed_at.strftime('%d/%m/%Y') if rec.reviewed_at else '—' }}</span>
{# ── 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>
<strong>{{ rec.title }}</strong>
<p class="text-muted small mb-1 mt-1">{{ rec.summary }}</p>
{% if rec.source_title %}
<div class="small text-muted">
<i class="fas fa-book me-1"></i>{{ rec.source_title }}
{% if rec.source_doi %} — {{ rec.source_doi }}{% endif %}
</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 %}
<!-- Targeting pills -->
<div class="d-flex flex-wrap gap-1 mt-2">
{% if rec.target_sex and rec.target_sex != 'all' %}
<span class="tag-pill">{{ 'Hommes' if rec.target_sex == 'male' else 'Femmes' }}</span>
</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 c in rec.required_conditions %}
<span class="tag-pill tag-pill-blue">Si: {{ c.replace('_',' ') }}</span>
{% endfor %}
{% for c in rec.excluded_conditions %}
<span class="tag-pill tag-pill-red">Exclu: {{ c.replace('_',' ') }}</span>
{% endfor %}
{% if not rec.required_conditions and not rec.required_health_goals and not rec.required_diet_types and not rec.required_activity_levels %}
<span class="tag-pill tag-pill-green">Universel</span>
{% 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>
<form action="{{ url_for('admin_library_delete', rid=rec.id) }}" method="post"
onsubmit="return confirm('Retirer « {{ rec.title }} » de la bibliothèque ?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Retirer">
<i class="fas fa-archive"></i>
{% 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('Retirer « {{ rec.title|replace(&quot;'&quot;, '') }} » de la bibliothèque ?')">
<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>Aller à la file d'attente
<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 %}