332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|