feat: cascade deletes, student notes, import popups, module reorganization
- Cascade delete on all entities (student, module, UE, user, role, promotion) - Fix Response body reuse bug (factory functions instead of constants) - Student note viewing via CAS uid (strip non-digit prefix) - Fix middleware page visibility for students in LOCAL mode - Import result popup component (shared across all import pages) - Fix student import to use numEtud from Excel - Bulk student selection with promo change and delete - Move UE/UE-Module API and pages from notes to admin module - Move promotions page from students to admin module - Multi-year maquette import with per-year promo selection - Inline promo creation in maquette import - Static Excel templates (students, notes, maquette) - Fix XLSX export using blob download instead of writeFile - Allow students to read modules list (GET /modules)
This commit is contained in:
@@ -15,6 +15,9 @@ export default function ConsultStudents() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filterPromo, setFilterPromo] = useState("");
|
||||
const [filterNom, setFilterNom] = useState("");
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [bulkPromo, setBulkPromo] = useState("");
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -44,6 +47,11 @@ export default function ConsultStudents() {
|
||||
});
|
||||
if (!res.ok) throw new Error("Suppression échouée");
|
||||
await load();
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(numEtud);
|
||||
return next;
|
||||
});
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Erreur");
|
||||
}
|
||||
@@ -56,6 +64,85 @@ export default function ConsultStudents() {
|
||||
return matchPromo && matchNom;
|
||||
});
|
||||
|
||||
const filteredIds = new Set(filtered.map((s) => s.numEtud));
|
||||
const selectedInView = [...selected].filter((id) => filteredIds.has(id));
|
||||
const allFilteredSelected = filtered.length > 0 &&
|
||||
filtered.every((s) => selected.has(s.numEtud));
|
||||
|
||||
function toggleOne(numEtud: number) {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(numEtud)) next.delete(numEtud);
|
||||
else next.add(numEtud);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
if (allFilteredSelected) {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const s of filtered) next.delete(s.numEtud);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const s of filtered) next.add(s.numEtud);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
const count = selectedInView.length;
|
||||
if (count === 0) return;
|
||||
if (
|
||||
!confirm(`Supprimer définitivement ${count} élève(s) sélectionné(s) ?`)
|
||||
) return;
|
||||
setBulkBusy(true);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
selectedInView.map((id) =>
|
||||
fetch(`/students/api/students/${id}`, { method: "DELETE" })
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => !r.ok).length;
|
||||
if (failed > 0) setError(`${failed} suppression(s) échouée(s)`);
|
||||
setSelected(new Set());
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Erreur");
|
||||
} finally {
|
||||
setBulkBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkChangePromo() {
|
||||
if (!bulkPromo || selectedInView.length === 0) return;
|
||||
setBulkBusy(true);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
selectedInView.map((id) =>
|
||||
fetch(`/students/api/students/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ idPromo: bulkPromo }),
|
||||
})
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => !r.ok).length;
|
||||
if (failed > 0) setError(`${failed} modification(s) échouée(s)`);
|
||||
setSelected(new Set());
|
||||
setBulkPromo("");
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Erreur");
|
||||
} finally {
|
||||
setBulkBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="page-content">
|
||||
<h2 class="page-title">Gestion des Élèves</h2>
|
||||
@@ -93,6 +180,44 @@ export default function ConsultStudents() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bulk actions bar */}
|
||||
{selectedInView.length > 0 && (
|
||||
<div class="bulk-bar">
|
||||
<span class="bulk-count">
|
||||
{selectedInView.length} sélectionné(s)
|
||||
</span>
|
||||
<div class="bulk-actions">
|
||||
<select
|
||||
class="filter-select"
|
||||
value={bulkPromo}
|
||||
onChange={(e) =>
|
||||
setBulkPromo((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">Changer de promo…</option>
|
||||
{promos.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.id}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
disabled={!bulkPromo || bulkBusy}
|
||||
onClick={bulkChangePromo}
|
||||
>
|
||||
Appliquer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
disabled={bulkBusy}
|
||||
onClick={bulkDelete}
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading
|
||||
? <p class="state-loading">Chargement…</p>
|
||||
: (
|
||||
@@ -100,6 +225,13 @@ export default function ConsultStudents() {
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 2.5rem">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allFilteredSelected && filtered.length > 0}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
</th>
|
||||
<th>N° étud.</th>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
@@ -111,13 +243,23 @@ export default function ConsultStudents() {
|
||||
{filtered.length === 0
|
||||
? (
|
||||
<tr>
|
||||
<td colspan={5} class="state-empty">
|
||||
<td colspan={6} class="state-empty">
|
||||
Aucun élève trouvé
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
: filtered.map((s) => (
|
||||
<tr key={s.numEtud}>
|
||||
<tr
|
||||
key={s.numEtud}
|
||||
class={selected.has(s.numEtud) ? "row-selected" : ""}
|
||||
>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(s.numEtud)}
|
||||
onChange={() => toggleOne(s.numEtud)}
|
||||
/>
|
||||
</td>
|
||||
<td class="col-dim">{s.numEtud}</td>
|
||||
<td>{s.nom}</td>
|
||||
<td>{s.prenom}</td>
|
||||
|
||||
Reference in New Issue
Block a user