Files
PolyMPR/routes/(apps)/students/(_islands)/AdminPromotions.tsx
T

236 lines
7.3 KiB
TypeScript
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.
import { useEffect, useState } from "preact/hooks";
type Promotion = { id: string; annee: string | null };
type Student = { numEtud: number; idPromo: string };
function parsePromo(id: string) {
const m = id.match(/^(\d+A)(FISE|FISA)(.+)$/);
if (!m) return { annee: id, filiere: "?", anneeSco: "?" };
return { annee: m[1], filiere: m[2], anneeSco: m[3] };
}
const ANNEES = ["3A", "4A", "5A"];
const FILIERES = ["FISE", "FISA"];
export default function AdminPromotions() {
const [promos, setPromos] = useState<Promotion[]>([]);
const [students, setStudents] = useState<Student[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
// PromoBuilder state
const [selectedAnnee, setSelectedAnnee] = useState("4A");
const [selectedFiliere, setSelectedFiliere] = useState("FISE");
const [anneeSco, setAnneeSco] = useState("");
const generatedId = anneeSco.trim()
? `${selectedAnnee}${selectedFiliere}${anneeSco.trim()}`
: "";
async function load() {
try {
const [pRes, sRes] = await Promise.all([
fetch("/students/api/promotions"),
fetch("/students/api/students"),
]);
if (!pRes.ok) throw new Error("Impossible de charger les promotions");
setPromos(await pRes.json());
if (sRes.ok) setStudents(await sRes.json());
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
async function createPromo() {
if (!generatedId) return;
setCreating(true);
try {
const res = await fetch("/students/api/promotions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
idPromo: generatedId,
annee: selectedAnnee,
}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Création échouée");
}
setAnneeSco("");
await load();
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setCreating(false);
}
}
async function deletePromo(id: string) {
if (!confirm(`Supprimer la promotion ${id} ?`)) return;
try {
const res = await fetch(
`/students/api/promotions/${encodeURIComponent(id)}`,
{ method: "DELETE" },
);
if (!res.ok) throw new Error("Suppression échouée");
await load();
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
}
}
function studentCount(idPromo: string) {
return students.filter((s) => s.idPromo === idPromo).length;
}
return (
<div class="page-content">
<h2 class="page-title">Gestion des Promotions</h2>
{error && <p class="state-error">{error}</p>}
{/* PromoBuilder */}
<div class="promo-builder">
<p class="promo-builder-title">Créer une promotion</p>
<p class="promo-builder-subtitle">
POST /promotions idPromo est généré automatiquement
</p>
<div class="promo-builder-row">
<div class="promo-builder-field">
<label>Année</label>
<div class="pill-group">
{ANNEES.map((a) => (
<button
key={a}
type="button"
class={`pill-btn${selectedAnnee === a ? " active" : ""}`}
onClick={() => setSelectedAnnee(a)}
>
{a}
</button>
))}
</div>
</div>
<div class="promo-builder-field">
<label>Filière</label>
<div class="pill-group">
{FILIERES.map((f) => (
<button
key={f}
type="button"
class={`pill-btn${selectedFiliere === f ? " active" : ""}`}
onClick={() => setSelectedFiliere(f)}
>
{f}
</button>
))}
</div>
</div>
<div class="promo-builder-field">
<label>Année scolaire</label>
<input
class="form-input"
placeholder="ex: 25/26, 24/27…"
value={anneeSco}
onInput={(e) => setAnneeSco((e.target as HTMLInputElement).value)}
style="min-width: 9rem"
/>
</div>
</div>
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap">
<div style="display: flex; align-items: center; gap: 0.5rem">
<span style="font-size: 0.78rem; color: light-dark(var(--light-foreground-dim), var(--dark-foreground-dim))">
idPromo généré :
</span>
<span class="promo-id-preview">
{generatedId || "—"}
</span>
</div>
<button
type="button"
class="btn btn-primary"
onClick={createPromo}
disabled={creating || !generatedId}
>
{creating ? "…" : "+ Créer la promo"}
</button>
</div>
</div>
{/* Existing promotions table */}
<p style="font-size: 0.82rem; font-weight: var(--font-weight-bold); margin-bottom: 0.5rem">
Promotions existantes
</p>
{loading
? <p class="state-loading">Chargement</p>
: (
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th>idPromo</th>
<th>Année</th>
<th>Filière</th>
<th>Année sco.</th>
<th>Nb étudiants</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{promos.length === 0
? (
<tr>
<td colspan={6} class="state-empty">
Aucune promotion enregistrée
</td>
</tr>
)
: promos.map((p) => {
const parsed = parsePromo(p.id);
const count = studentCount(p.id);
return (
<tr key={p.id}>
<td>
<span class="promo-chip">{p.id}</span>
</td>
<td>{parsed.annee}</td>
<td>
<span class="filiere-chip">{parsed.filiere}</span>
</td>
<td>{parsed.anneeSco}</td>
<td class="col-dim">
{count} étudiant{count !== 1 ? "s" : ""}
</td>
<td>
<button
type="button"
class="btn btn-sm btn-danger"
onClick={() => deletePromo(p.id)}
>
🗑
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
}