feat : fixed some page not being as described in the figma

This commit is contained in:
2026-04-27 11:21:32 +02:00
parent 56019ad372
commit 733259e317
13 changed files with 1757 additions and 235 deletions
+288 -65
View File
@@ -1,19 +1,54 @@
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 [newNom, setNewNom] = useState("");
const [creating, setCreating] = useState(false);
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 res = await fetch("/notes/api/ues");
if (!res.ok) throw new Error("Impossible de charger les UEs");
setUes(await res.json());
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 {
@@ -26,28 +61,37 @@ export default function AdminUEs() {
}, []);
async function createUE() {
if (!newNom.trim()) return;
setCreating(true);
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: newNom.trim() }),
body: JSON.stringify({ nom: newUeNom.trim() }),
});
if (!res.ok) throw new Error("Création échouée");
setNewNom("");
setNewUeNom("");
await load();
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur");
} finally {
setCreating(false);
setCreatingUe(false);
}
}
async function deleteUE(id: number) {
if (!confirm("Supprimer cette UE ?")) return;
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/ues/${id}`, { method: "DELETE" });
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) {
@@ -55,68 +99,247 @@ export default function AdminUEs() {
}
}
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>}
<div class="form-row">
<input
class="form-input"
placeholder="Nom de la nouvelle UE"
value={newNom}
onInput={(e) => setNewNom((e.target as HTMLInputElement).value)}
onKeyDown={(e) => e.key === "Enter" && createUE()}
/>
<button
type="button"
class="btn btn-primary"
onClick={createUE}
disabled={creating}
>
+ Ajouter
</button>
</div>
{loading
? <p class="state-loading">Chargement</p>
: (
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{ues.length === 0
? (
<tr>
<td colspan={3} class="state-empty">
Aucune UE enregistrée
</td>
</tr>
)
: ues.map((ue) => (
<tr key={ue.id}>
<td class="col-dim">{ue.id}</td>
<td>{ue.nom}</td>
<td>
<button
type="button"
class="btn btn-sm btn-danger"
onClick={() => deleteUE(ue.id)}
>
🗑
</button>
</td>
</tr>
<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>
))}
</tbody>
</table>
{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>