Files
PolyMPR/routes/(apps)/notes/(_islands)/AdminUEs.tsx
T

348 lines
12 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 UE = { id: number; nom: string };
type UEModule = {
idModule: string;
idUE: number;
idPromo: string;
coeff: number;
};
type Module = { id: string; nom: string };
type Promo = { id: string; annee: string };
export default function AdminUEs() {
const [ues, setUes] = useState<UE[]>([]);
const [ueModules, setUeModules] = useState<UEModule[]>([]);
const [modules, setModules] = useState<Module[]>([]);
const [promos, setPromos] = useState<Promo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedUe, setSelectedUe] = useState<UE | null>(null);
// New UE form
const [newUeNom, setNewUeNom] = useState("");
const [creatingUe, setCreatingUe] = useState(false);
// Add UE-module form
const [addModuleId, setAddModuleId] = useState("");
const [addPromoId, setAddPromoId] = useState("");
const [addCoeff, setAddCoeff] = useState("1");
const [adding, setAdding] = useState(false);
const [addError, setAddError] = useState<string | null>(null);
async function load() {
try {
const [uRes, umRes, mRes, pRes] = await Promise.all([
fetch("/notes/api/ues"),
fetch("/notes/api/ue-modules"),
fetch("/admin/api/modules"),
fetch("/students/api/promotions"),
]);
if (!uRes.ok) throw new Error("Impossible de charger les UEs");
const uesData: UE[] = await uRes.json();
setUes(uesData);
if (umRes.ok) setUeModules(await umRes.json());
if (mRes.ok) setModules(await mRes.json());
if (pRes.ok) setPromos(await pRes.json());
// Keep selection in sync
setSelectedUe((prev) =>
prev ? uesData.find((u) => u.id === prev.id) ?? null : null
);
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
async function createUE() {
if (!newUeNom.trim()) return;
setCreatingUe(true);
try {
const res = await fetch("/notes/api/ues", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ nom: newUeNom.trim() }),
});
if (!res.ok) throw new Error("Création échouée");
setNewUeNom("");
await load();
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setCreatingUe(false);
}
}
async function deleteUeModule(
idModule: string,
idUE: number,
idPromo: string,
) {
if (!confirm("Supprimer ce module de la UE ?")) return;
try {
const res = await fetch(
`/notes/api/ue-modules/${encodeURIComponent(idModule)}/${idUE}/${
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 addUeModule() {
if (!selectedUe || !addModuleId || !addPromoId) {
setAddError("Module et Promo sont requis");
return;
}
const coeff = parseFloat(addCoeff);
if (isNaN(coeff) || coeff <= 0) {
setAddError("Coefficient invalide");
return;
}
setAdding(true);
setAddError(null);
try {
const res = await fetch("/notes/api/ue-modules", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
idModule: addModuleId,
idUE: selectedUe.id,
idPromo: addPromoId,
coeff,
}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Création échouée");
}
setAddModuleId("");
setAddPromoId("");
setAddCoeff("1");
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 selectedUeModules = selectedUe
? ueModules.filter((um) => um.idUE === selectedUe.id)
: [];
return (
<div class="page-content">
<h2 class="page-title">Gestion des UEs</h2>
<p
class="col-dim"
style="font-size: 0.78rem; margin: -0.5rem 0 1rem"
>
UE = Unité d'Enseignement regroupant plusieurs modules
</p>
{error && <p class="state-error">{error}</p>}
{loading
? <p class="state-loading">Chargement</p>
: (
<div class="ue-split">
{/* Left panel UE list */}
<div class="ue-panel-left">
<div class="panel-box">
<p class="panel-box-title">UEs existantes</p>
<div class="form-row" style="margin-bottom: 0.75rem">
<input
class="form-input"
placeholder="Nom de la nouvelle UE…"
value={newUeNom}
onInput={(e) =>
setNewUeNom((e.target as HTMLInputElement).value)}
onKeyDown={(e) => e.key === "Enter" && createUE()}
style="min-width: 0; flex: 1"
/>
</div>
<button
type="button"
class="btn btn-primary"
onClick={createUE}
disabled={creatingUe}
style="width: 100%; justify-content: center; margin-bottom: 0.5rem"
>
+ Nouvelle UE
</button>
<div>
{ues.map((ue) => (
<div
key={ue.id}
class={`ue-list-item${
selectedUe?.id === ue.id ? " active" : ""
}`}
onClick={() => {
setSelectedUe(ue);
setAddError(null);
}}
>
{ue.nom}
</div>
))}
{ues.length === 0 && (
<p class="state-empty" style="padding: 1rem 0">
Aucune UE
</p>
)}
</div>
</div>
</div>
{/* Right panel UE detail */}
<div class="ue-panel-right">
{selectedUe
? (
<div class="panel-box">
<p class="panel-box-title">{selectedUe.nom}</p>
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
Modules assignés (UE_Module)
</p>
<div class="data-table-wrap" style="margin-bottom: 1rem">
<table class="data-table">
<thead>
<tr>
<th>Module</th>
<th>Promo</th>
<th>Coeff</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{selectedUeModules.length === 0
? (
<tr>
<td colspan={4} class="state-empty">
Aucun module assigné
</td>
</tr>
)
: selectedUeModules.map((um) => {
const mod = moduleMap[um.idModule];
return (
<tr
key={`${um.idModule}-${um.idPromo}`}
>
<td class="col-promo">
{mod
? `${mod.id} ${mod.nom}`
: um.idModule}
</td>
<td>
<span class="promo-chip">{um.idPromo}</span>
</td>
<td>{um.coeff}</td>
<td>
<button
type="button"
class="btn btn-sm btn-danger"
onClick={() =>
deleteUeModule(
um.idModule,
um.idUE,
um.idPromo,
)}
>
🗑
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
Ajouter un module à cette UE
</p>
{addError && (
<p class="state-error" style="padding: 0.3rem 0.5rem">
{addError}
</p>
)}
<div class="form-row">
<select
class="filter-select"
value={addModuleId}
onChange={(e) =>
setAddModuleId(
(e.target as HTMLSelectElement).value,
)}
style="min-width: 12rem"
>
<option value="">Module </option>
{modules.map((m) => (
<option key={m.id} value={m.id}>
{m.id} {m.nom}
</option>
))}
</select>
<select
class="filter-select"
value={addPromoId}
onChange={(e) =>
setAddPromoId(
(e.target as HTMLSelectElement).value,
)}
style="min-width: 9rem"
>
<option value="">Promo </option>
{promos.map((p) => (
<option key={p.id} value={p.id}>{p.id}</option>
))}
</select>
<input
type="number"
class="form-input"
placeholder="Coeff"
value={addCoeff}
min="0.1"
step="0.5"
onInput={(e) =>
setAddCoeff((e.target as HTMLInputElement).value)}
style="min-width: 5rem; max-width: 6rem"
/>
<button
type="button"
class="btn btn-primary"
onClick={addUeModule}
disabled={adding}
>
{adding ? "…" : "+ Ajouter"}
</button>
</div>
</div>
)
: (
<div class="panel-box">
<p class="state-empty" style="padding: 2rem 0">
Sélectionnez une UE pour voir ses modules
</p>
</div>
)}
</div>
</div>
)}
</div>
);
}