135 lines
6.5 KiB
Rust
135 lines
6.5 KiB
Rust
use leptos::prelude::*;
|
|
use leptos_router::hooks::use_params_map;
|
|
use crate::server::admin::{get_quiz_submissions, export_submissions_csv};
|
|
|
|
#[component]
|
|
pub fn QuizResultsPage() -> impl IntoView {
|
|
let params = use_params_map();
|
|
let quiz_id = move || params.get().get("quiz_id").and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
|
|
let submissions = Resource::new(quiz_id, |id| get_quiz_submissions(id));
|
|
let csv_data = RwSignal::new(Option::<String>::None);
|
|
|
|
let export_action = Action::new(move |_: &()| {
|
|
let id = quiz_id();
|
|
async move {
|
|
if let Ok(csv) = export_submissions_csv(id).await {
|
|
csv_data.set(Some(csv));
|
|
}
|
|
}
|
|
});
|
|
|
|
view! {
|
|
<div class="admin-page">
|
|
<div class="admin-topbar">
|
|
<div>
|
|
<h1 style="
|
|
background:linear-gradient(135deg,#4F8EFF,#8B5CF6);
|
|
-webkit-background-clip:text;
|
|
-webkit-text-fill-color:transparent;
|
|
background-clip:text;
|
|
">"Résultats"</h1>
|
|
<p style="font-size:13px;color:var(--color-enuxia-muted);margin-top:2px;">
|
|
"Liste des soumissions pour ce quiz."
|
|
</p>
|
|
</div>
|
|
<div class="topbar-actions">
|
|
<button
|
|
class="btn-purple"
|
|
on:click=move |_| { export_action.dispatch(()); }
|
|
>
|
|
"↓ Exporter CSV"
|
|
</button>
|
|
<a href=move || format!("/admin/quiz/{}/stats", quiz_id())
|
|
class="btn-ghost">"📊 Stats"</a>
|
|
<a href="/admin" class="btn-ghost">"← Dashboard"</a>
|
|
</div>
|
|
</div>
|
|
|
|
// CSV export
|
|
{move || csv_data.get().map(|csv| view! {
|
|
<div class="csv-box">
|
|
<p>"✓ CSV généré — copiez le contenu ci-dessous"</p>
|
|
<textarea readonly rows="8">{csv}</textarea>
|
|
</div>
|
|
})}
|
|
|
|
<Suspense fallback=|| view! {
|
|
<p style="color:var(--color-enuxia-muted);font-size:14px;">"Chargement..."</p>
|
|
}>
|
|
{move || submissions.get().map(|result| {
|
|
match result {
|
|
Ok(list) if list.is_empty() => view! {
|
|
<div style="
|
|
text-align:center; padding:60px 24px;
|
|
border-radius:16px;
|
|
background:var(--color-enuxia-dark-3);
|
|
border:1px solid var(--color-enuxia-border);
|
|
">
|
|
<p style="font-size:32px;margin-bottom:12px;">"📭"</p>
|
|
<p style="font-size:16px;font-weight:600;color:var(--color-enuxia-text);">
|
|
"Aucune soumission"
|
|
</p>
|
|
<p style="font-size:13px;color:var(--color-enuxia-muted);margin-top:4px;">
|
|
"Les résultats apparaîtront ici quand des étudiants auront passé le quiz."
|
|
</p>
|
|
</div>
|
|
}.into_any(),
|
|
Ok(list) => view! {
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>"Nom"</th>
|
|
<th>"Prénom"</th>
|
|
<th>"Score"</th>
|
|
<th>"%"</th>
|
|
<th>"Date"</th>
|
|
<th>"Détail"</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{list.into_iter().map(|s| {
|
|
let pct = if s.total > 0 {
|
|
(s.score * 100) / s.total
|
|
} else { 0 };
|
|
let sid = s.id;
|
|
|
|
let pct_color = if pct >= 80 { "#22C55E" }
|
|
else if pct >= 50 { "#4F8EFF" }
|
|
else { "#f87171" };
|
|
|
|
view! {
|
|
<tr>
|
|
<td style="font-weight:600;">{s.last_name.clone()}</td>
|
|
<td>{s.first_name.clone()}</td>
|
|
<td>{format!("{} / {}", s.score, s.total)}</td>
|
|
<td>
|
|
<span style=format!(
|
|
"font-weight:700;color:{};", pct_color
|
|
)>
|
|
{format!("{}%", pct)}
|
|
</span>
|
|
</td>
|
|
<td style="color:var(--color-enuxia-muted);font-size:13px;">
|
|
{s.submitted_at.format("%d/%m/%Y %H:%M").to_string()}
|
|
</td>
|
|
<td>
|
|
<a href=format!("/admin/submission/{}", sid)
|
|
class="table-link">
|
|
"Voir →"
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}).collect_view()}
|
|
</tbody>
|
|
</table>
|
|
}.into_any(),
|
|
Err(_) => view! {
|
|
<div class="field-error">"Erreur de chargement."</div>
|
|
}.into_any(),
|
|
}
|
|
})}
|
|
</Suspense>
|
|
</div>
|
|
}
|
|
} |