Files
PolyMPR/routes/(apps)/students/(_islands)/EditStudents.tsx
T
2026-05-01 14:14:33 +02:00

298 lines
8.9 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 Student = {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
};
type Promo = { id: string; annee: string };
type Module = { id: string; nom: string };
type Mobilite = { id: number; duree: number; status: string };
type Stage = { id: number; duree: number };
type Props = { numEtud: number };
function anneeLabel(idPromo: string): string {
const m = idPromo.match(/^(\d+)A/);
if (!m) return "";
const n = m[1];
if (n === "3") return "3ème année";
if (n === "4") return "4ème année";
if (n === "5") return "5ème année";
return `${n}ème année`;
}
export default function EditStudents({ numEtud }: Props) {
const [student, setStudent] = useState<Student | null>(null);
const [promos, setPromos] = useState<Promo[]>([]);
const [_modules, setModules] = useState<Module[]>([]);
const [mobWeeks, setMobWeeks] = useState(0);
const [stageWeeks, setStageWeeks] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [saveMsg, setSaveMsg] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
// Edit form state
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [idPromo, setIdPromo] = useState("");
useEffect(() => {
async function load() {
try {
const [sRes, pRes, mRes, mobRes, stRes] = await Promise.all([
fetch(`/students/api/students/${numEtud}`),
fetch("/students/api/promotions"),
fetch("/notes/api/modules"),
fetch(`/mobility/api/mobilites?numEtud=${numEtud}`),
fetch(`/stages/api/stages?numEtud=${numEtud}`),
]);
if (!sRes.ok) throw new Error("Élève introuvable");
const s: Student = await sRes.json();
setStudent(s);
setNom(s.nom);
setPrenom(s.prenom);
setIdPromo(s.idPromo);
if (pRes.ok) setPromos(await pRes.json());
if (mRes.ok) setModules(await mRes.json());
if (mobRes.ok) {
const mobs: Mobilite[] = await mobRes.json();
setMobWeeks(
mobs.filter((m) => m.status === "validated").reduce(
(s, m) => s + m.duree,
0,
),
);
}
if (stRes.ok) {
const stages: Stage[] = await stRes.json();
setStageWeeks(stages.reduce((s, st) => s + st.duree, 0));
}
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setLoading(false);
}
}
load();
}, [numEtud]);
async function saveInfos() {
if (!student) return;
setSaving(true);
setSaveMsg(null);
try {
const res = await fetch(`/students/api/students/${numEtud}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
nom: nom.trim(),
prenom: prenom.trim(),
idPromo,
}),
});
if (!res.ok) throw new Error("Modification échouée");
const updated: Student = await res.json();
setStudent(updated);
setSaveMsg("Informations enregistrées.");
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setSaving(false);
}
}
async function deleteStudent() {
if (!confirm(`Supprimer définitivement l'élève #${numEtud} ?`)) return;
try {
const res = await fetch(`/students/api/students/${numEtud}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("Suppression échouée");
globalThis.location.href = "/students/consult";
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
}
}
if (loading) {
return (
<div class="page-content">
<p class="state-loading">Chargement</p>
</div>
);
}
if (error && !student) {
return (
<div class="page-content">
<p class="state-error">{error}</p>
</div>
);
}
if (!student) return null;
return (
<div class="page-content">
<a
class="back-link"
href="/students/consult"
f-partial="/students/partials/consult"
>
Retour à la liste
</a>
<h2 class="page-title" style="border-bottom: none; margin-bottom: 0.5rem">
Édition {student.prenom} {student.nom}
</h2>
{/* Info bar */}
<div class="info-bar">
<span class="numEtud-chip">{student.numEtud}</span>
<span>{student.idPromo}</span>
<span class="col-dim">{anneeLabel(student.idPromo)}</span>
</div>
{error && <p class="state-error">{error}</p>}
{saveMsg && (
<p style="font-size: 0.82rem; color: light-dark(var(--light-accent-color), var(--dark-accent-color)); margin-bottom: 0.5rem">
{saveMsg}
</p>
)}
{/* Section 1: Informations générales */}
<div class="edit-section">
<p class="edit-section-title">Informations générales</p>
<div class="form-grid">
<div class="form-field">
<label>Nom</label>
<input
class="form-input"
value={nom}
onInput={(e) => setNom((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-field">
<label>Prénom</label>
<input
class="form-input"
value={prenom}
onInput={(e) => setPrenom((e.target as HTMLInputElement).value)}
/>
</div>
<div class="form-field">
<label>N° Étudiant</label>
<input
class="form-input"
value={student.numEtud}
disabled
style="opacity: 0.6"
/>
</div>
<div class="form-field">
<label>Promo</label>
<select
class="filter-select"
value={idPromo}
onChange={(e) =>
setIdPromo((e.target as HTMLSelectElement).value)}
style="min-width: 0"
>
{promos.map((p) => <option key={p.id} value={p.id}>{p.id}
</option>)}
</select>
</div>
</div>
<div style="display: flex; gap: 0.5rem; justify-content: space-between; flex-wrap: wrap">
<button
type="button"
class="btn btn-primary"
onClick={saveInfos}
disabled={saving}
>
{saving ? "…" : "Enregistrer infos"}
</button>
<button
type="button"
class="btn btn-danger"
onClick={deleteStudent}
>
Supprimer l'élève
</button>
</div>
</div>
{/* Section 2: Notes */}
<div class="edit-section">
<p class="edit-section-title">Notes</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span class="col-dim" style="font-size: 0.82rem">
Récap complet des notes et moyennes
</span>
<a
class="btn btn-secondary"
href={`/notes/recap/${numEtud}`}
f-client-nav={false}
>
Voir les notes
</a>
</div>
</div>
{/* Section 3: Mobilités */}
<div class="edit-section">
<p class="edit-section-title">Mobilités</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
<span
style={{
fontFamily: "monospace",
fontWeight: "var(--font-weight-bold)",
color: mobWeeks >= 12 ? "#22c55e" : "#dc2626",
}}
>
{mobWeeks}/12 semaines validées
</span>
</span>
<a
class="btn btn-secondary"
href={`/mobility/overview/${numEtud}`}
f-client-nav={false}
>
Consulter
</a>
</div>
</div>
{/* Section 4: Stages */}
<div class="edit-section">
<p class="edit-section-title">Stages</p>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
<span
style={{
fontFamily: "monospace",
fontWeight: "var(--font-weight-bold)",
color: stageWeeks >= 40 ? "#22c55e" : "#dc2626",
}}
>
{stageWeeks}/40 semaines
</span>
</span>
<a
class="btn btn-secondary"
href={`/stages/overview/${numEtud}`}
f-client-nav={false}
>
Consulter
</a>
</div>
</div>
</div>
);
}