feat: made stuff
This commit is contained in:
@@ -56,6 +56,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
|
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
|
||||||
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
|
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
|
||||||
|
sed 's/--> statement-breakpoint/;/g' databases/migrations/0003_add_session2_and_malus.sql | \
|
||||||
|
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
|
||||||
|
sed 's/--> statement-breakpoint/;/g' databases/migrations/0004_add_stages_and_mobilites.sql | \
|
||||||
|
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install --ignore-scripts && deno install
|
run: npm install --ignore-scripts && deno install
|
||||||
|
|||||||
@@ -27,6 +27,32 @@ export default function makeSlug(basePath: string): Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For multi-segment slugs (e.g. "overview/12345"), try
|
||||||
|
// partials/<dir>/[param].tsx and inject the param into context.params
|
||||||
|
if (!page && slug.includes("/")) {
|
||||||
|
const idx = slug.indexOf("/");
|
||||||
|
const dir = slug.slice(0, idx);
|
||||||
|
const param = slug.slice(idx + 1);
|
||||||
|
|
||||||
|
// Discover the dynamic segment name from the file system
|
||||||
|
try {
|
||||||
|
const entries: string[] = [];
|
||||||
|
for await (const entry of Deno.readDir(`${basePath}/partials/${dir}`)) {
|
||||||
|
if (entry.isFile) entries.push(entry.name);
|
||||||
|
}
|
||||||
|
const dynFile = entries.find((n) =>
|
||||||
|
n.startsWith("[") && n.endsWith("].tsx")
|
||||||
|
);
|
||||||
|
if (dynFile) {
|
||||||
|
const paramName = dynFile.slice(1, -5); // "[numEtud].tsx" → "numEtud"
|
||||||
|
context.params[paramName] = param;
|
||||||
|
page = (await import(`${basePath}/partials/${dir}/${dynFile}`)).Page;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// directory doesn't exist or no dynamic file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return context.renderNotFound();
|
return context.renderNotFound();
|
||||||
}
|
}
|
||||||
|
|||||||
+1536
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,7 @@ export default function AdminEnseignements() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<h2 class="page-title">Assignations Enseignant → Module / Promo</h2>
|
<h2 class="page-title">Assignations Enseignant → ECUE / Promo</h2>
|
||||||
|
|
||||||
{error && <p class="state-error">{error}</p>}
|
{error && <p class="state-error">{error}</p>}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export default function AdminEnseignements() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFilterModule((e.target as HTMLSelectElement).value)}
|
setFilterModule((e.target as HTMLSelectElement).value)}
|
||||||
>
|
>
|
||||||
<option value="">Module ▾</option>
|
<option value="">ECUE ▾</option>
|
||||||
{modules.map((m) => (
|
{modules.map((m) => (
|
||||||
<option key={m.id} value={m.id}>{m.id} – {m.nom}</option>
|
<option key={m.id} value={m.id}>{m.id} – {m.nom}</option>
|
||||||
))}
|
))}
|
||||||
@@ -194,7 +194,7 @@ export default function AdminEnseignements() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Module</label>
|
<label>ECUE</label>
|
||||||
<select
|
<select
|
||||||
class="filter-select"
|
class="filter-select"
|
||||||
value={addModule}
|
value={addModule}
|
||||||
@@ -202,7 +202,7 @@ export default function AdminEnseignements() {
|
|||||||
setAddModule((e.target as HTMLSelectElement).value)}
|
setAddModule((e.target as HTMLSelectElement).value)}
|
||||||
style="min-width: 0; width: 100%"
|
style="min-width: 0; width: 100%"
|
||||||
>
|
>
|
||||||
<option value="">Module...</option>
|
<option value="">ECUE...</option>
|
||||||
{modules.map((m) => (
|
{modules.map((m) => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.id} -- {m.nom}
|
{m.id} -- {m.nom}
|
||||||
@@ -251,7 +251,7 @@ export default function AdminEnseignements() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Promo</th>
|
<th>Promo</th>
|
||||||
<th>Module</th>
|
<th>ECUE</th>
|
||||||
<th>Enseignant (User.id)</th>
|
<th>Enseignant (User.id)</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -319,7 +319,7 @@ export default function AdminEnseignements() {
|
|||||||
|
|
||||||
<div class="info-note">
|
<div class="info-note">
|
||||||
<p>
|
<p>
|
||||||
Un même module peut être enseigné par plusieurs utilisateurs sur une
|
Un même ECUE peut être enseigné par plusieurs utilisateurs sur une
|
||||||
même promo.
|
même promo.
|
||||||
</p>
|
</p>
|
||||||
<p class="info-note-dim">
|
<p class="info-note-dim">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function AdminModules() {
|
|||||||
fetch("/admin/api/enseignements"),
|
fetch("/admin/api/enseignements"),
|
||||||
fetch("/admin/api/users"),
|
fetch("/admin/api/users"),
|
||||||
]);
|
]);
|
||||||
if (!mRes.ok) throw new Error("Impossible de charger les modules");
|
if (!mRes.ok) throw new Error("Impossible de charger les ECUEs");
|
||||||
setModules(await mRes.json());
|
setModules(await mRes.json());
|
||||||
if (eRes.ok) setEnseignements(await eRes.json());
|
if (eRes.ok) setEnseignements(await eRes.json());
|
||||||
if (uRes.ok) setUsers(await uRes.json());
|
if (uRes.ok) setUsers(await uRes.json());
|
||||||
@@ -61,7 +61,7 @@ export default function AdminModules() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModule(id: string) {
|
async function deleteModule(id: string) {
|
||||||
if (!confirm(`Supprimer le module ${id} ?`)) return;
|
if (!confirm(`Supprimer l'ECUE ${id} ?`)) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/admin/api/modules/${encodeURIComponent(id)}`,
|
`/admin/api/modules/${encodeURIComponent(id)}`,
|
||||||
@@ -102,7 +102,7 @@ export default function AdminModules() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<h2 class="page-title">Gestion des Modules</h2>
|
<h2 class="page-title">Gestion des ECUEs</h2>
|
||||||
|
|
||||||
{error && <p class="state-error">{error}</p>}
|
{error && <p class="state-error">{error}</p>}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export default function AdminModules() {
|
|||||||
}}
|
}}
|
||||||
style="margin-left: auto"
|
style="margin-left: auto"
|
||||||
>
|
>
|
||||||
+ Ajouter module
|
+ Ajouter ECUE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export default function AdminModules() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>id (code)</th>
|
<th>id (code)</th>
|
||||||
<th>Nom du module</th>
|
<th>Nom de l'ECUE</th>
|
||||||
<th>Enseignants assignes</th>
|
<th>Enseignants assignes</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -144,7 +144,7 @@ export default function AdminModules() {
|
|||||||
? (
|
? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={4} class="state-empty">
|
<td colspan={4} class="state-empty">
|
||||||
Aucun module enregistré
|
Aucun ECUE enregistré
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -218,13 +218,13 @@ export default function AdminModules() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Nouveau module */}
|
{/* Nouvel ECUE */}
|
||||||
<div
|
<div
|
||||||
id="new-module-section"
|
id="new-module-section"
|
||||||
class="edit-section"
|
class="edit-section"
|
||||||
style="margin-top: 1.5rem"
|
style="margin-top: 1.5rem"
|
||||||
>
|
>
|
||||||
<p class="edit-section-title">Nouveau module</p>
|
<p class="edit-section-title">Nouvel ECUE</p>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
@@ -235,7 +235,7 @@ export default function AdminModules() {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder="Nom du module"
|
placeholder="Nom de l'ECUE"
|
||||||
value={newNom}
|
value={newNom}
|
||||||
onInput={(e) => setNewNom((e.target as HTMLInputElement).value)}
|
onInput={(e) => setNewNom((e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function AdminUEs() {
|
|||||||
idUE: number,
|
idUE: number,
|
||||||
idPromo: string,
|
idPromo: string,
|
||||||
) {
|
) {
|
||||||
if (!confirm("Supprimer ce module de la UE ?")) return;
|
if (!confirm("Supprimer cet ECUE de la UE ?")) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/admin/api/ue-modules/${encodeURIComponent(idModule)}/${idUE}/${
|
`/admin/api/ue-modules/${encodeURIComponent(idModule)}/${idUE}/${
|
||||||
@@ -121,7 +121,7 @@ export default function AdminUEs() {
|
|||||||
|
|
||||||
async function addUeModule() {
|
async function addUeModule() {
|
||||||
if (!selectedUe || !addModuleId || !addPromoId) {
|
if (!selectedUe || !addModuleId || !addPromoId) {
|
||||||
setAddError("Module et Promo sont requis");
|
setAddError("ECUE et Promo sont requis");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const coeff = parseFloat(addCoeff);
|
const coeff = parseFloat(addCoeff);
|
||||||
@@ -203,7 +203,7 @@ export default function AdminUEs() {
|
|||||||
class="col-dim"
|
class="col-dim"
|
||||||
style="font-size: 0.78rem; margin: -0.5rem 0 1rem"
|
style="font-size: 0.78rem; margin: -0.5rem 0 1rem"
|
||||||
>
|
>
|
||||||
UE = Unité d'Enseignement regroupant plusieurs modules
|
UE = Unité d'Enseignement regroupant plusieurs ECUEs
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && <p class="state-error">{error}</p>}
|
{error && <p class="state-error">{error}</p>}
|
||||||
@@ -314,13 +314,13 @@ export default function AdminUEs() {
|
|||||||
<div class="panel-box">
|
<div class="panel-box">
|
||||||
<p class="panel-box-title">{selectedUe.nom}</p>
|
<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">
|
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
|
||||||
Modules assignés (UE_Module)
|
ECUEs assignés (UE_Module)
|
||||||
</p>
|
</p>
|
||||||
<div class="data-table-wrap" style="margin-bottom: 1rem">
|
<div class="data-table-wrap" style="margin-bottom: 1rem">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Module</th>
|
<th>ECUE</th>
|
||||||
<th>Promo</th>
|
<th>Promo</th>
|
||||||
<th>Coeff</th>
|
<th>Coeff</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@@ -331,7 +331,7 @@ export default function AdminUEs() {
|
|||||||
? (
|
? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={4} class="state-empty">
|
<td colspan={4} class="state-empty">
|
||||||
Aucun module assigné
|
Aucun ECUE assigné
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -441,7 +441,7 @@ export default function AdminUEs() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
|
<p style="font-size: 0.78rem; font-weight: var(--font-weight-bold); margin: 0 0 0.5rem">
|
||||||
Ajouter un module à cette UE
|
Ajouter un ECUE à cette UE
|
||||||
</p>
|
</p>
|
||||||
{addError && (
|
{addError && (
|
||||||
<p class="state-error" style="padding: 0.3rem 0.5rem">
|
<p class="state-error" style="padding: 0.3rem 0.5rem">
|
||||||
@@ -458,7 +458,7 @@ export default function AdminUEs() {
|
|||||||
)}
|
)}
|
||||||
style="min-width: 12rem"
|
style="min-width: 12rem"
|
||||||
>
|
>
|
||||||
<option value="">Module ▾</option>
|
<option value="">ECUE ▾</option>
|
||||||
{modules.map((m) => (
|
{modules.map((m) => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.id} – {m.nom}
|
{m.id} – {m.nom}
|
||||||
@@ -504,7 +504,7 @@ export default function AdminUEs() {
|
|||||||
: (
|
: (
|
||||||
<div class="panel-box">
|
<div class="panel-box">
|
||||||
<p class="state-empty" style="padding: 2rem 0">
|
<p class="state-empty" style="padding: 2rem 0">
|
||||||
Sélectionnez une UE pour voir ses modules
|
Sélectionnez une UE pour voir ses ECUEs
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
fetch("/admin/api/users"),
|
fetch("/admin/api/users"),
|
||||||
fetch("/students/api/promotions"),
|
fetch("/students/api/promotions"),
|
||||||
]);
|
]);
|
||||||
if (!mRes.ok) throw new Error("Module introuvable");
|
if (!mRes.ok) throw new Error("ECUE introuvable");
|
||||||
const m: Module = await mRes.json();
|
const m: Module = await mRes.json();
|
||||||
setMod(m);
|
setMod(m);
|
||||||
setNom(m.nom);
|
setNom(m.nom);
|
||||||
@@ -70,7 +70,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
if (!res.ok) throw new Error("Modification échouée");
|
if (!res.ok) throw new Error("Modification échouée");
|
||||||
const updated: Module = await res.json();
|
const updated: Module = await res.json();
|
||||||
setMod(updated);
|
setMod(updated);
|
||||||
setSaveMsg("Module enregistré.");
|
setSaveMsg("ECUE enregistré.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Erreur");
|
setError(e instanceof Error ? e.message : "Erreur");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -79,7 +79,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModule() {
|
async function deleteModule() {
|
||||||
if (!confirm(`Supprimer définitivement le module ${moduleId} ?`)) return;
|
if (!confirm(`Supprimer définitivement l'ECUE ${moduleId} ?`)) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/admin/api/modules/${encodeURIComponent(moduleId)}`,
|
`/admin/api/modules/${encodeURIComponent(moduleId)}`,
|
||||||
@@ -173,7 +173,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
class="page-title"
|
class="page-title"
|
||||||
style="border-bottom: none; margin-bottom: 0.5rem"
|
style="border-bottom: none; margin-bottom: 0.5rem"
|
||||||
>
|
>
|
||||||
Module -- {mod.id}
|
ECUE -- {mod.id}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="info-bar">
|
<div class="info-bar">
|
||||||
@@ -202,7 +202,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Nom du module</label>
|
<label>Nom de l'ECUE</label>
|
||||||
<input
|
<input
|
||||||
class="form-input"
|
class="form-input"
|
||||||
value={nom}
|
value={nom}
|
||||||
@@ -224,7 +224,7 @@ export default function EditModule({ moduleId }: Props) {
|
|||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
onClick={deleteModule}
|
onClick={deleteModule}
|
||||||
>
|
>
|
||||||
Supprimer le module
|
Supprimer l'ECUE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ export default function ImportMaquette() {
|
|||||||
globalThis.open("/templates/modele_maquette.xlsx", "_blank");
|
globalThis.open("/templates/modele_maquette.xlsx", "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadExport() {
|
function _downloadExport() {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/admin/api/ues").then((r) => r.json()),
|
fetch("/admin/api/ues").then((r) => r.json()),
|
||||||
fetch("/admin/api/ue-modules").then((r) => r.json()),
|
fetch("/admin/api/ue-modules").then((r) => r.json()),
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ function validatedWeeks(mobs: Mobilite[]): number {
|
|||||||
.reduce((sum, m) => sum + m.duree, 0);
|
.reduce((sum, m) => sum + m.duree, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MobilityOverview() {
|
export default function MobilityOverview(
|
||||||
|
{ initialNumEtud }: { initialNumEtud?: number } = {},
|
||||||
|
) {
|
||||||
const [students, setStudents] = useState<Student[]>([]);
|
const [students, setStudents] = useState<Student[]>([]);
|
||||||
const [promos, setPromos] = useState<Promotion[]>([]);
|
const [promos, setPromos] = useState<Promotion[]>([]);
|
||||||
const [mobilites, setMobilites] = useState<Mobilite[]>([]);
|
const [mobilites, setMobilites] = useState<Mobilite[]>([]);
|
||||||
@@ -105,6 +107,12 @@ export default function MobilityOverview() {
|
|||||||
setStagesMap(
|
setStagesMap(
|
||||||
Object.fromEntries((stData as Stage[]).map((s) => [s.id, s])),
|
Object.fromEntries((stData as Stage[]).map((s) => [s.id, s])),
|
||||||
);
|
);
|
||||||
|
if (initialNumEtud) {
|
||||||
|
const s = (sData as Student[]).find((s) =>
|
||||||
|
s.numEtud === initialNumEtud
|
||||||
|
);
|
||||||
|
if (s) setDetailStudent(s);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Erreur");
|
setError(e instanceof Error ? e.message : "Erreur");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -116,6 +124,18 @@ export default function MobilityOverview() {
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function openStudent(s: Student) {
|
||||||
|
setDetailStudent(s);
|
||||||
|
history.pushState(null, "", `/mobility/overview/${s.numEtud}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStudent() {
|
||||||
|
setDetailStudent(null);
|
||||||
|
setEditingMob(null);
|
||||||
|
setShowAddForm(false);
|
||||||
|
history.pushState(null, "", "/mobility/overview");
|
||||||
|
}
|
||||||
|
|
||||||
// If in detail view, render that
|
// If in detail view, render that
|
||||||
if (detailStudent) {
|
if (detailStudent) {
|
||||||
return (
|
return (
|
||||||
@@ -128,11 +148,7 @@ export default function MobilityOverview() {
|
|||||||
setEditingMob={setEditingMob}
|
setEditingMob={setEditingMob}
|
||||||
showAddForm={showAddForm}
|
showAddForm={showAddForm}
|
||||||
setShowAddForm={setShowAddForm}
|
setShowAddForm={setShowAddForm}
|
||||||
onBack={() => {
|
onBack={closeStudent}
|
||||||
setDetailStudent(null);
|
|
||||||
setEditingMob(null);
|
|
||||||
setShowAddForm(false);
|
|
||||||
}}
|
|
||||||
onReload={load}
|
onReload={load}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -207,14 +223,14 @@ export default function MobilityOverview() {
|
|||||||
<ListView
|
<ListView
|
||||||
students={filtered}
|
students={filtered}
|
||||||
mobsByStudent={mobsByStudent}
|
mobsByStudent={mobsByStudent}
|
||||||
onConsult={(s) => setDetailStudent(s)}
|
onConsult={(s) => openStudent(s)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<KanbanView
|
<KanbanView
|
||||||
students={filtered}
|
students={filtered}
|
||||||
mobsByStudent={mobsByStudent}
|
mobsByStudent={mobsByStudent}
|
||||||
onConsult={(s) => setDetailStudent(s)}
|
onConsult={(s) => openStudent(s)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -637,6 +653,9 @@ function DetailView(
|
|||||||
numEtud={student.numEtud}
|
numEtud={student.numEtud}
|
||||||
ecoles={ecoles}
|
ecoles={ecoles}
|
||||||
paysList={paysList}
|
paysList={paysList}
|
||||||
|
availableStages={Object.values(stagesMap)
|
||||||
|
.filter((s) => s.numEtud === student.numEtud)
|
||||||
|
.filter((s) => !mobilites.some((m) => m.idStage === s.id))}
|
||||||
onCancel={() => setShowAddForm(false)}
|
onCancel={() => setShowAddForm(false)}
|
||||||
onSave={async () => {
|
onSave={async () => {
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
@@ -774,10 +793,11 @@ function MobEditForm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MobAddForm(
|
function MobAddForm(
|
||||||
{ numEtud, ecoles, paysList, onCancel, onSave }: {
|
{ numEtud, ecoles, paysList, availableStages, onCancel, onSave }: {
|
||||||
numEtud: number;
|
numEtud: number;
|
||||||
ecoles: string[];
|
ecoles: string[];
|
||||||
paysList: string[];
|
paysList: string[];
|
||||||
|
availableStages: Stage[];
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSave: () => Promise<void>;
|
onSave: () => Promise<void>;
|
||||||
},
|
},
|
||||||
@@ -786,8 +806,19 @@ function MobAddForm(
|
|||||||
const [ecole, setEcole] = useState("");
|
const [ecole, setEcole] = useState("");
|
||||||
const [pays, setPays] = useState("");
|
const [pays, setPays] = useState("");
|
||||||
const [status, setStatus] = useState("contracts_received");
|
const [status, setStatus] = useState("contracts_received");
|
||||||
|
const [selectedStageId, setSelectedStageId] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const isStageLinked = selectedStageId !== "";
|
||||||
|
|
||||||
|
function onStageChange(value: string) {
|
||||||
|
setSelectedStageId(value);
|
||||||
|
if (value) {
|
||||||
|
const stage = availableStages.find((s) => s.id === Number(value));
|
||||||
|
if (stage) setDuree(String(stage.duree));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -797,9 +828,10 @@ function MobAddForm(
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
numEtud,
|
numEtud,
|
||||||
duree: parseInt(duree),
|
duree: parseInt(duree),
|
||||||
ecole: ecole || null,
|
ecole: isStageLinked ? null : (ecole || null),
|
||||||
pays: pays || null,
|
pays: isStageLinked ? null : (pays || null),
|
||||||
status,
|
status: isStageLinked ? "validated" : status,
|
||||||
|
idStage: isStageLinked ? Number(selectedStageId) : null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("Erreur");
|
if (!res.ok) throw new Error("Erreur");
|
||||||
@@ -815,6 +847,24 @@ function MobAddForm(
|
|||||||
<div class="edit-section" style={{ marginBottom: "1rem" }}>
|
<div class="edit-section" style={{ marginBottom: "1rem" }}>
|
||||||
<p class="edit-section-title">Nouvelle mobilité</p>
|
<p class="edit-section-title">Nouvelle mobilité</p>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
{availableStages.length > 0 && (
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Lier à un stage</label>
|
||||||
|
<select
|
||||||
|
class="filter-select"
|
||||||
|
value={selectedStageId}
|
||||||
|
onChange={(e) =>
|
||||||
|
onStageChange((e.target as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
<option value="">— Mobilité d'étude —</option>
|
||||||
|
{availableStages.map((s) => (
|
||||||
|
<option key={s.id} value={String(s.id)}>
|
||||||
|
{s.nomEntreprise} ({s.duree} sem.)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Durée (semaines)</label>
|
<label>Durée (semaines)</label>
|
||||||
<input
|
<input
|
||||||
@@ -825,6 +875,8 @@ function MobAddForm(
|
|||||||
onInput={(e) => setDuree((e.target as HTMLInputElement).value)}
|
onInput={(e) => setDuree((e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{!isStageLinked && (
|
||||||
|
<>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>École</label>
|
<label>École</label>
|
||||||
<input
|
<input
|
||||||
@@ -854,14 +906,28 @@ function MobAddForm(
|
|||||||
<select
|
<select
|
||||||
class="filter-select"
|
class="filter-select"
|
||||||
value={status}
|
value={status}
|
||||||
onChange={(e) => setStatus((e.target as HTMLSelectElement).value)}
|
onChange={(e) =>
|
||||||
|
setStatus((e.target as HTMLSelectElement).value)}
|
||||||
>
|
>
|
||||||
{STATUS_ORDER.map((s) => (
|
{STATUS_ORDER.map((s) => (
|
||||||
<option key={s} value={s}>{STATUS_LABELS[s]}</option>
|
<option key={s} value={s}>{STATUS_LABELS[s]}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isStageLinked && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
opacity: 0.7,
|
||||||
|
margin: "0.4rem 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mobilité liée à un stage — status automatiquement « Validé »
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}>
|
<div style={{ display: "flex", gap: "0.5rem", marginTop: "0.5rem" }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
getPartialsConfig,
|
||||||
|
makePartials,
|
||||||
|
} from "$root/defaults/makePartials.tsx";
|
||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { State } from "$root/defaults/interfaces.ts";
|
||||||
|
import MobilityOverview from "../../(_islands)/MobilityOverview.tsx";
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async function Overview(
|
||||||
|
_request: Request,
|
||||||
|
context: FreshContext<State>,
|
||||||
|
) {
|
||||||
|
const numEtud = Number(context.params.numEtud);
|
||||||
|
return <MobilityOverview initialNumEtud={numEtud} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Overview as Page };
|
||||||
|
export const config = getPartialsConfig();
|
||||||
|
export default makePartials(Overview);
|
||||||
@@ -268,7 +268,7 @@ export default function ImportNotes() {
|
|||||||
globalThis.open("/templates/modele_notes.xlsx", "_blank");
|
globalThis.open("/templates/modele_notes.xlsx", "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadExport() {
|
function _downloadExport() {
|
||||||
// Export notes from the API in the same format
|
// Export notes from the API in the same format
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/students/api/students").then((r) => r.json()),
|
fetch("/students/api/students").then((r) => r.json()),
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ type Stage = {
|
|||||||
|
|
||||||
const REQUIRED_WEEKS = 40;
|
const REQUIRED_WEEKS = 40;
|
||||||
|
|
||||||
export default function StagesOverview() {
|
export default function StagesOverview(
|
||||||
|
{ initialNumEtud }: { initialNumEtud?: number } = {},
|
||||||
|
) {
|
||||||
const [students, setStudents] = useState<Student[]>([]);
|
const [students, setStudents] = useState<Student[]>([]);
|
||||||
const [promos, setPromos] = useState<Promotion[]>([]);
|
const [promos, setPromos] = useState<Promotion[]>([]);
|
||||||
const [stagesList, setStagesList] = useState<Stage[]>([]);
|
const [stagesList, setStagesList] = useState<Stage[]>([]);
|
||||||
@@ -48,6 +50,12 @@ export default function StagesOverview() {
|
|||||||
setStudents(sData);
|
setStudents(sData);
|
||||||
setPromos(pData);
|
setPromos(pData);
|
||||||
setStagesList(stData);
|
setStagesList(stData);
|
||||||
|
if (initialNumEtud) {
|
||||||
|
const found = (sData as Student[]).find((s) =>
|
||||||
|
s.numEtud === initialNumEtud
|
||||||
|
);
|
||||||
|
if (found) setDetailStudent(found);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Erreur");
|
setError(e instanceof Error ? e.message : "Erreur");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -59,6 +67,18 @@ export default function StagesOverview() {
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function openStudent(s: Student) {
|
||||||
|
setDetailStudent(s);
|
||||||
|
history.pushState(null, "", `/stages/overview/${s.numEtud}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStudent() {
|
||||||
|
setDetailStudent(null);
|
||||||
|
setEditingStage(null);
|
||||||
|
setShowAddForm(false);
|
||||||
|
history.pushState(null, "", "/stages/overview");
|
||||||
|
}
|
||||||
|
|
||||||
if (detailStudent) {
|
if (detailStudent) {
|
||||||
return (
|
return (
|
||||||
<DetailView
|
<DetailView
|
||||||
@@ -69,11 +89,7 @@ export default function StagesOverview() {
|
|||||||
setEditingStage={setEditingStage}
|
setEditingStage={setEditingStage}
|
||||||
showAddForm={showAddForm}
|
showAddForm={showAddForm}
|
||||||
setShowAddForm={setShowAddForm}
|
setShowAddForm={setShowAddForm}
|
||||||
onBack={() => {
|
onBack={closeStudent}
|
||||||
setDetailStudent(null);
|
|
||||||
setEditingStage(null);
|
|
||||||
setShowAddForm(false);
|
|
||||||
}}
|
|
||||||
onReload={load}
|
onReload={load}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -129,7 +145,7 @@ export default function StagesOverview() {
|
|||||||
<ListView
|
<ListView
|
||||||
students={filtered}
|
students={filtered}
|
||||||
stagesByStudent={stagesByStudent}
|
stagesByStudent={stagesByStudent}
|
||||||
onConsult={(s) => setDetailStudent(s)}
|
onConsult={(s) => openStudent(s)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
getPartialsConfig,
|
||||||
|
makePartials,
|
||||||
|
} from "$root/defaults/makePartials.tsx";
|
||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { State } from "$root/defaults/interfaces.ts";
|
||||||
|
import StagesOverview from "../../(_islands)/StagesOverview.tsx";
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async function Overview(
|
||||||
|
_request: Request,
|
||||||
|
context: FreshContext<State>,
|
||||||
|
) {
|
||||||
|
const numEtud = Number(context.params.numEtud);
|
||||||
|
return <StagesOverview initialNumEtud={numEtud} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Overview as Page };
|
||||||
|
export const config = getPartialsConfig();
|
||||||
|
export default makePartials(Overview);
|
||||||
@@ -8,6 +8,8 @@ type Student = {
|
|||||||
};
|
};
|
||||||
type Promo = { id: string; annee: string };
|
type Promo = { id: string; annee: string };
|
||||||
type Module = { id: string; nom: string };
|
type Module = { id: string; nom: string };
|
||||||
|
type Mobilite = { id: number; duree: number; status: string };
|
||||||
|
type Stage = { id: number; duree: number };
|
||||||
|
|
||||||
type Props = { numEtud: number };
|
type Props = { numEtud: number };
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ export default function EditStudents({ numEtud }: Props) {
|
|||||||
const [student, setStudent] = useState<Student | null>(null);
|
const [student, setStudent] = useState<Student | null>(null);
|
||||||
const [promos, setPromos] = useState<Promo[]>([]);
|
const [promos, setPromos] = useState<Promo[]>([]);
|
||||||
const [_modules, setModules] = useState<Module[]>([]);
|
const [_modules, setModules] = useState<Module[]>([]);
|
||||||
|
const [mobWeeks, setMobWeeks] = useState(0);
|
||||||
|
const [stageWeeks, setStageWeeks] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [saveMsg, setSaveMsg] = useState<string | null>(null);
|
const [saveMsg, setSaveMsg] = useState<string | null>(null);
|
||||||
@@ -38,10 +42,12 @@ export default function EditStudents({ numEtud }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const [sRes, pRes, mRes] = await Promise.all([
|
const [sRes, pRes, mRes, mobRes, stRes] = await Promise.all([
|
||||||
fetch(`/students/api/students/${numEtud}`),
|
fetch(`/students/api/students/${numEtud}`),
|
||||||
fetch("/students/api/promotions"),
|
fetch("/students/api/promotions"),
|
||||||
fetch("/admin/api/modules"),
|
fetch("/notes/api/modules"),
|
||||||
|
fetch(`/mobility/api/mobilites?numEtud=${numEtud}`),
|
||||||
|
fetch(`/stages/api/stages?numEtud=${numEtud}`),
|
||||||
]);
|
]);
|
||||||
if (!sRes.ok) throw new Error("Élève introuvable");
|
if (!sRes.ok) throw new Error("Élève introuvable");
|
||||||
const s: Student = await sRes.json();
|
const s: Student = await sRes.json();
|
||||||
@@ -51,6 +57,19 @@ export default function EditStudents({ numEtud }: Props) {
|
|||||||
setIdPromo(s.idPromo);
|
setIdPromo(s.idPromo);
|
||||||
if (pRes.ok) setPromos(await pRes.json());
|
if (pRes.ok) setPromos(await pRes.json());
|
||||||
if (mRes.ok) setModules(await mRes.json());
|
if (mRes.ok) setModules(await mRes.json());
|
||||||
|
if (mobRes.ok) {
|
||||||
|
const mobs: Mobilite[] = await mobRes.json();
|
||||||
|
setMobWeeks(
|
||||||
|
mobs.filter((m) => m.status === "validated").reduce(
|
||||||
|
(s, m) => s + m.duree,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stRes.ok) {
|
||||||
|
const stages: Stage[] = await stRes.json();
|
||||||
|
setStageWeeks(stages.reduce((s, st) => s + st.duree, 0));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Erreur");
|
setError(e instanceof Error ? e.message : "Erreur");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -207,30 +226,69 @@ export default function EditStudents({ numEtud }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section 2: Spécialisations */}
|
{/* Section 2: Notes */}
|
||||||
<div class="edit-section">
|
<div class="edit-section">
|
||||||
<p class="edit-section-title">Spécialisations</p>
|
<p class="edit-section-title">Notes</p>
|
||||||
<p
|
|
||||||
class="state-empty"
|
|
||||||
style="padding: 1rem 0; text-align: left"
|
|
||||||
>
|
|
||||||
Fonctionnalité non disponible (endpoint non implémenté).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section 3: Notes lecture seule */}
|
|
||||||
<div class="edit-section">
|
|
||||||
<p class="edit-section-title">Notes (lecture seule)</p>
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
|
||||||
<span class="col-dim" style="font-size: 0.82rem">
|
<span class="col-dim" style="font-size: 0.82rem">
|
||||||
Voir le récap complet des notes et moyennes de cet étudiant →
|
Récap complet des notes et moyennes
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
href={`/notes/recap/${numEtud}`}
|
href={`/notes/recap/${numEtud}`}
|
||||||
f-client-nav={false}
|
f-client-nav={false}
|
||||||
>
|
>
|
||||||
Récap notes
|
Voir les notes
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 3: Mobilités */}
|
||||||
|
<div class="edit-section">
|
||||||
|
<p class="edit-section-title">Mobilités</p>
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
|
||||||
|
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontWeight: "var(--font-weight-bold)",
|
||||||
|
color: mobWeeks >= 12 ? "#22c55e" : "#dc2626",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mobWeeks}/12 semaines validées
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary"
|
||||||
|
href={`/mobility/overview/${numEtud}`}
|
||||||
|
f-client-nav={false}
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 4: Stages */}
|
||||||
|
<div class="edit-section">
|
||||||
|
<p class="edit-section-title">Stages</p>
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap">
|
||||||
|
<span style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontWeight: "var(--font-weight-bold)",
|
||||||
|
color: stageWeeks >= 40 ? "#22c55e" : "#dc2626",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stageWeeks}/40 semaines
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary"
|
||||||
|
href={`/stages/overview/${numEtud}`}
|
||||||
|
f-client-nav={false}
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-2
@@ -40,6 +40,12 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field .filter-select {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-input:focus,
|
.filter-input:focus,
|
||||||
@@ -368,7 +374,9 @@
|
|||||||
color: light-dark(var(--light-foreground), var(--dark-foreground));
|
color: light-dark(var(--light-foreground), var(--dark-foreground));
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
min-width: 12rem;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
@@ -799,7 +807,7 @@
|
|||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
|
||||||
gap: 0.75rem 1rem;
|
gap: 0.75rem 1rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -1,15 +1,15 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var t = localStorage.getItem("theme");
|
const t = localStorage.getItem("theme");
|
||||||
if (t) document.documentElement.style.colorScheme = t;
|
if (t) document.documentElement.style.colorScheme = t;
|
||||||
|
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
var btn = e.target.closest("#theme-toggle");
|
const btn = e.target.closest("#theme-toggle");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var cs = getComputedStyle(document.documentElement).colorScheme;
|
const cs = getComputedStyle(document.documentElement).colorScheme;
|
||||||
var isDark = cs === "dark" ||
|
const isDark = cs === "dark" ||
|
||||||
(!cs || cs === "light dark") &&
|
(!cs || cs === "light dark") &&
|
||||||
matchMedia("(prefers-color-scheme:dark)").matches;
|
matchMedia("(prefers-color-scheme:dark)").matches;
|
||||||
var next = isDark ? "light" : "dark";
|
const next = isDark ? "light" : "dark";
|
||||||
document.documentElement.style.colorScheme = next;
|
document.documentElement.style.colorScheme = next;
|
||||||
localStorage.setItem("theme", next);
|
localStorage.setItem("theme", next);
|
||||||
btn.querySelector("span").textContent = next === "dark"
|
btn.querySelector("span").textContent = next === "dark"
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
var btn = document.getElementById("theme-toggle");
|
const btn = document.getElementById("theme-toggle");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var cs = getComputedStyle(document.documentElement).colorScheme;
|
const cs = getComputedStyle(document.documentElement).colorScheme;
|
||||||
var isDark = cs === "dark" ||
|
const isDark = cs === "dark" ||
|
||||||
(!cs || cs === "light dark") &&
|
(!cs || cs === "light dark") &&
|
||||||
matchMedia("(prefers-color-scheme:dark)").matches;
|
matchMedia("(prefers-color-scheme:dark)").matches;
|
||||||
btn.querySelector("span").textContent = isDark ? "light_mode" : "dark_mode";
|
btn.querySelector("span").textContent = isDark ? "light_mode" : "dark_mode";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Deno.test({
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
name: "e2e modules: GET /modules returns empty for non-employee",
|
name: "e2e modules: GET /modules returns all for non-employee",
|
||||||
async fn() {
|
async fn() {
|
||||||
await truncateAll();
|
await truncateAll();
|
||||||
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
|
await seedModules([{ id: "MATH101", nom: "Mathématiques" }]);
|
||||||
@@ -44,7 +44,7 @@ Deno.test({
|
|||||||
);
|
);
|
||||||
assertEquals(res.status, 200);
|
assertEquals(res.status, 200);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
assertEquals(body.length, 0);
|
assertEquals(body.length, 1);
|
||||||
},
|
},
|
||||||
sanitizeResources: false,
|
sanitizeResources: false,
|
||||||
sanitizeOps: false,
|
sanitizeOps: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user