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

332 lines
11 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 Enseignement = { idProf: string; idModule: string; idPromo: string };
type Module = { id: string; nom: string };
type Promo = { id: string; annee: string };
export default function AdminEnseignements() {
const [enseignements, setEnseignements] = useState<Enseignement[]>([]);
const [modules, setModules] = useState<Module[]>([]);
const [promos, setPromos] = useState<Promo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Filters
const [filterPromo, setFilterPromo] = useState("");
const [filterModule, setFilterModule] = useState("");
const [filterEnseignant, setFilterEnseignant] = useState("");
// Add form
const [showAdd, setShowAdd] = useState(false);
const [addPromo, setAddPromo] = useState("");
const [addModule, setAddModule] = useState("");
const [addProf, setAddProf] = useState("");
const [adding, setAdding] = useState(false);
const [addError, setAddError] = useState<string | null>(null);
async function load() {
try {
const [eRes, mRes, pRes] = await Promise.all([
fetch("/admin/api/enseignements"),
fetch("/admin/api/modules"),
fetch("/students/api/promotions"),
]);
if (!eRes.ok) throw new Error("Impossible de charger les enseignements");
setEnseignements(await eRes.json());
if (mRes.ok) setModules(await mRes.json());
if (pRes.ok) setPromos(await pRes.json());
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
async function deleteEnseignement(
idProf: string,
idModule: string,
idPromo: string,
) {
if (
!confirm(
`Supprimer l'assignation ${idProf}${idModule} / ${idPromo} ?`,
)
) return;
try {
const res = await fetch(
`/admin/api/enseignements/${encodeURIComponent(idProf)}/${
encodeURIComponent(idModule)
}/${encodeURIComponent(idPromo)}`,
{ method: "DELETE" },
);
if (!res.ok) throw new Error("Suppression échouée");
await load();
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
}
}
async function addEnseignement() {
if (!addProf.trim() || !addModule || !addPromo) {
setAddError("Tous les champs sont requis");
return;
}
setAdding(true);
setAddError(null);
try {
const res = await fetch("/admin/api/enseignements", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
idProf: addProf.trim(),
idModule: addModule,
idPromo: addPromo,
}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Création échouée");
}
setAddProf("");
setAddModule("");
setAddPromo("");
setShowAdd(false);
await load();
} catch (e) {
setAddError(e instanceof Error ? e.message : "Erreur");
} finally {
setAdding(false);
}
}
const moduleMap = Object.fromEntries(modules.map((m) => [m.id, m]));
const filtered = enseignements.filter((e) => {
const matchPromo = !filterPromo || e.idPromo === filterPromo;
const matchModule = !filterModule || e.idModule === filterModule;
const matchEns = !filterEnseignant ||
e.idProf.toLowerCase().includes(filterEnseignant.toLowerCase());
return matchPromo && matchModule && matchEns;
});
return (
<div class="page-content">
<h2 class="page-title">Assignations Enseignant ECUE / Promo</h2>
{error && <p class="state-error">{error}</p>}
<div class="filters">
<select
class="filter-select"
value={filterPromo}
onChange={(e) =>
setFilterPromo((e.target as HTMLSelectElement).value)}
>
<option value="">Promo </option>
{promos.map((p) => <option key={p.id} value={p.id}>{p.id}</option>)}
</select>
<select
class="filter-select"
value={filterModule}
onChange={(e) =>
setFilterModule((e.target as HTMLSelectElement).value)}
>
<option value="">ECUE </option>
{modules.map((m) => (
<option key={m.id} value={m.id}>{m.id} {m.nom}</option>
))}
</select>
<input
class="filter-input"
placeholder="Enseignant ▾"
value={filterEnseignant}
onInput={(e) =>
setFilterEnseignant((e.target as HTMLInputElement).value)}
/>
<button
type="button"
class="btn btn-secondary"
onClick={() => {
setFilterPromo("");
setFilterModule("");
setFilterEnseignant("");
}}
>
Filtrer
</button>
<button
type="button"
class="btn btn-primary"
onClick={() => setShowAdd((v) => !v)}
style="margin-left: auto"
>
+ Assigner
</button>
</div>
{showAdd && (
<div class="modal-overlay" onClick={() => setShowAdd(false)}>
<div class="modal-box" onClick={(e) => e.stopPropagation()}>
<p class="modal-title">Assigner un enseignement</p>
{addError && (
<p class="state-error" style="padding: 0.3rem 0.5rem">
{addError}
</p>
)}
<div class="modal-form">
<div class="form-field">
<label>Promo</label>
<select
class="filter-select"
value={addPromo}
onChange={(e) =>
setAddPromo((e.target as HTMLSelectElement).value)}
style="min-width: 0; width: 100%"
>
<option value="">Promo...</option>
{promos.map((p) => (
<option key={p.id} value={p.id}>{p.id}</option>
))}
</select>
</div>
<div class="form-field">
<label>ECUE</label>
<select
class="filter-select"
value={addModule}
onChange={(e) =>
setAddModule((e.target as HTMLSelectElement).value)}
style="min-width: 0; width: 100%"
>
<option value="">ECUE...</option>
{modules.map((m) => (
<option key={m.id} value={m.id}>
{m.id} -- {m.nom}
</option>
))}
</select>
</div>
<div class="form-field">
<label>User ID enseignant</label>
<input
class="form-input"
placeholder="User ID enseignant..."
value={addProf}
onInput={(e) =>
setAddProf((e.target as HTMLInputElement).value)}
style="min-width: 0; width: 100%"
/>
</div>
</div>
<div class="modal-actions">
<button
type="button"
class="btn btn-secondary"
onClick={() => setShowAdd(false)}
>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
onClick={addEnseignement}
disabled={adding}
>
{adding ? "..." : "+ Assigner"}
</button>
</div>
</div>
</div>
)}
{loading
? <p class="state-loading">Chargement</p>
: (
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Promo</th>
<th>ECUE</th>
<th>Enseignant (User.id)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filtered.length === 0
? (
<tr>
<td colspan={4} class="state-empty">
Aucun enseignement trouvé
</td>
</tr>
)
: filtered.map((e) => {
const mod = moduleMap[e.idModule];
return (
<tr key={`${e.idProf}-${e.idModule}-${e.idPromo}`}>
<td>
<span class="promo-chip">{e.idPromo}</span>
</td>
<td class="col-promo">
{mod ? `${mod.id} ${mod.nom}` : e.idModule}
</td>
<td>{e.idProf}</td>
<td>
<div class="col-actions">
<button
type="button"
class="btn btn-sm btn-danger"
onClick={() =>
deleteEnseignement(
e.idProf,
e.idModule,
e.idPromo,
)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<rect
x="5"
y="6"
width="14"
height="16"
rx="1"
/>
</svg>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
<div class="info-note">
<p>
Un même ECUE peut être enseigné par plusieurs utilisateurs sur une
même promo.
</p>
<p class="info-note-dim">
Clé composite = idProf (User.Id) + idModule + idPromo
</p>
</div>
</div>
);
}