04be659d6b
Add routes for modules, users, notes import, recap, and islands edit. Update middleware to filter pages based on user role. feat(admin): add modal for assigning teaching, replace delete icon with SVG refactor(server): rename port variable to uppercase and add env support feat(admin): add enseignants, users, filtering and role colors refactor(AdminRoles): improve role UI and add permission mapping feat(admin-users): add role colors, role filter, and modal for creating users feat(admin): add EditModule component for module editing feat(admin): add EditUser page for editing users and managing enseignements feat(promo-select): display id and name in options for promo dropdown feat: add edit module/user routes, inline coeff editing, UI tweaks refactor: UI – icons, modal overlay, grid, subtitles, import margin
240 lines
6.9 KiB
TypeScript
240 lines
6.9 KiB
TypeScript
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 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 [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] = await Promise.all([
|
||
fetch(`/students/api/students/${numEtud}`),
|
||
fetch("/students/api/promotions"),
|
||
fetch("/admin/api/modules"),
|
||
]);
|
||
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());
|
||
} 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: Spécialisations */}
|
||
<div class="edit-section">
|
||
<p class="edit-section-title">Spécialisations</p>
|
||
<p
|
||
class="state-empty"
|
||
style="padding: 1rem 0; text-align: left"
|
||
>
|
||
Fonctionnalité non disponible (endpoint non implémenté).
|
||
</p>
|
||
</div>
|
||
|
||
{/* Section 3: Notes lecture seule */}
|
||
<div class="edit-section">
|
||
<p class="edit-section-title">Notes (lecture seule)</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">
|
||
Voir le récap complet des notes et moyennes de cet étudiant →
|
||
</span>
|
||
<a
|
||
class="btn btn-secondary"
|
||
href={`/notes/recap/${numEtud}`}
|
||
f-client-nav={false}
|
||
>
|
||
Récap notes
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|