diff --git a/deno.json b/deno.json
index 45c5870..c7f729b 100644
--- a/deno.json
+++ b/deno.json
@@ -35,7 +35,8 @@
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/",
- "$root/": "./"
+ "$root/": "./",
+ "$apps/": "./routes/(apps)/"
},
"compilerOptions": {
"jsx": "react-jsx",
diff --git a/fresh.gen.ts b/fresh.gen.ts
index 3061dcd..eeb5302 100644
--- a/fresh.gen.ts
+++ b/fresh.gen.ts
@@ -14,11 +14,11 @@ import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/part
import * as $_apps_notes_partials_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.tsx";
import * as $_apps_students_api_students from "./routes/(apps)/students/api/students.ts";
-import * as $_apps_students_api_types_d from "./routes/(apps)/students/api/types.d.ts";
import * as $_apps_students_index from "./routes/(apps)/students/index.tsx";
import * as $_apps_students_partials_admin_consult from "./routes/(apps)/students/partials/(admin)/consult.tsx";
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
+import * as $_apps_students_types_d from "./routes/(apps)/students/types.d.ts";
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts";
@@ -56,7 +56,6 @@ const manifest = {
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes,
"./routes/(apps)/students/api/students.ts": $_apps_students_api_students,
- "./routes/(apps)/students/api/types.d.ts": $_apps_students_api_types_d,
"./routes/(apps)/students/index.tsx": $_apps_students_index,
"./routes/(apps)/students/partials/(admin)/consult.tsx":
$_apps_students_partials_admin_consult,
@@ -64,6 +63,7 @@ const manifest = {
$_apps_students_partials_admin_upload,
"./routes/(apps)/students/partials/index.tsx":
$_apps_students_partials_index,
+ "./routes/(apps)/students/types.d.ts": $_apps_students_types_d,
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware,
diff --git a/routes/(apps)/students/(_components)/Promotion.tsx b/routes/(apps)/students/(_components)/Promotion.tsx
new file mode 100644
index 0000000..985b637
--- /dev/null
+++ b/routes/(apps)/students/(_components)/Promotion.tsx
@@ -0,0 +1,30 @@
+import Student from "$root/routes/(apps)/students/(_components)/Student.tsx";
+
+type PromotionProps = { students: Student[]; promo: Promotion };
+
+export default function Promotion(props: PromotionProps) {
+ if (!props.promo) {
+ return
Unable to find user in database.
;
+ }
+
+ return (
+
+
Promotion {props.promo.endyear}
+
+
+
+ | ID |
+ First Name |
+ Last Name |
+ Email |
+
+
+
+ {props.students
+ .filter((student) => student.promotionId === props.promo.id)
+ .map((student) => )}
+
+
+
+ );
+}
diff --git a/routes/(apps)/students/(_components)/SelfPortrait.tsx b/routes/(apps)/students/(_components)/SelfPortrait.tsx
new file mode 100644
index 0000000..a505298
--- /dev/null
+++ b/routes/(apps)/students/(_components)/SelfPortrait.tsx
@@ -0,0 +1,31 @@
+import { CasContent } from "$root/defaults/interfaces.ts";
+
+type SelfPortraitProps = { self: CasContent };
+
+const regex =
+ /^(?\d{4})(?\d{2})(?\d{2})(?\d{2})(?\d{2})(?\d{2})Z$/;
+
+export default function SelfPortrait(props: SelfPortraitProps) {
+ const { year, month, date, hours, minutes, seconds } = props.self
+ .amuDateValidation.match(regex)!.groups!;
+
+ const validationIsoDate =
+ `${year}-${month}-${date}T${hours}:${minutes}:${seconds}Z`;
+
+ const validationDate = new Date(validationIsoDate);
+
+ return (
+
+
Identity
+
{props.self.supannCivilite} {props.self.displayName}
+
Student number
+
{props.self.uid}
+
amU mail
+
{props.self.mail}
+
First amU registration
+
{validationDate.toLocaleString()}
+
amU class code
+
{props.self.supannEtuEtape}
+
+ );
+}
diff --git a/routes/(apps)/students/(_components)/Student.tsx b/routes/(apps)/students/(_components)/Student.tsx
new file mode 100644
index 0000000..7da0e84
--- /dev/null
+++ b/routes/(apps)/students/(_components)/Student.tsx
@@ -0,0 +1,13 @@
+type StudentProps = { student: Student; promo?: number };
+
+export default function Student(props: StudentProps) {
+ return (
+
+ | {props.student.userId} |
+ {props.student.firstName} |
+ {props.student.lastName} |
+ {props.student.mail} |
+ {props.promo && {props.promo} | }
+
+ );
+}
diff --git a/routes/(apps)/students/(_islands)/ConsultStudents.tsx b/routes/(apps)/students/(_islands)/ConsultStudents.tsx
index 835754f..f67036b 100644
--- a/routes/(apps)/students/(_islands)/ConsultStudents.tsx
+++ b/routes/(apps)/students/(_islands)/ConsultStudents.tsx
@@ -1,75 +1,45 @@
import { useEffect, useState } from "preact/hooks";
+import Promotion from "$root/routes/(apps)/students/(_components)/Promotion.tsx";
-interface Promotion {
- id: number;
- name: string;
-}
+type SingleUserResponse = { promo: Promotion; student: Student };
+type ManyUsersResponse = { promos: Promotion[]; students: Student[] };
-interface Student {
- userId: string;
- firstName: string;
- lastName: string;
- mail: string;
- promotionId: number;
-}
+type APIResponse = SingleUserResponse | ManyUsersResponse;
export default function ConsultStudents() {
- const [data, setData] = useState<
- { promotions: Promotion[]; students: Student[] } | null
- >(null);
+ const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
- try {
- const response = await fetch("/students/api/students");
- if (!response.ok) {
- throw new Error(`Error fetching data: ${response.statusText}`);
- }
-
- const result = await response.json();
- console.log("Fetched data:", result);
- setData(result);
- } catch (err) {
- console.error("Error fetching data:", err);
+ const response = await fetch("/students/api/students");
+ if (!response.ok) {
setError("Failed to load data. Please try again later.");
}
+
+ const result: APIResponse = await response.json();
+ setData(result);
};
fetchData();
}, []);
return (
-
- Consult Students
+ <>
{error && {error}
}
- {data?.promotions.map((promo) => (
-
-
Promotion: {promo.name}
-
-
-
- | ID |
- First Name |
- Last Name |
- Email |
-
-
-
- {data.students
- .filter((student) => student.promotionId === promo.id)
- .map((student) => (
-
- | {student.userId} |
- {student.firstName} |
- {student.lastName} |
- {student.mail} |
-
- ))}
-
-
-
- ))}
-
+ {data && ((Object.hasOwn(data, "student"))
+ ? (
+
+ )
+ : (data as ManyUsersResponse).promos.map((promo) => (
+
+ )))}
+ >
);
}
diff --git a/routes/(apps)/students/(_islands)/UploadStudents.tsx b/routes/(apps)/students/(_islands)/UploadStudents.tsx
index 6ff1d5a..1ae11d7 100644
--- a/routes/(apps)/students/(_islands)/UploadStudents.tsx
+++ b/routes/(apps)/students/(_islands)/UploadStudents.tsx
@@ -1,75 +1,111 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts"
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
-import { useSignal } from "@preact/signals";
+import { Signal, useSignal } from "@preact/signals";
-export default function UploadStudents() {
- const statusMessage = useSignal("");
- const fileData = useSignal(null);
-
- const handleFileChange = (event: Event) => {
+/**
+ * Create a new handler for file change that displays
+ * messages in statusMessage and gets file data in fileData.
+ * @param statusMessage The status message signal.
+ * @param fileData The file data signal.
+ * @returns The file change handler.
+ */
+function getFileChangeHandler(
+ statusMessage: Signal,
+ fileData: Signal,
+): (event: Event) => void {
+ /**
+ * Handle file change.
+ * @param event The file change event.
+ */
+ return (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
fileData.value = input.files[0];
- statusMessage.value = "File selected: " + input.files[0].name;
+ statusMessage.value = `File selected: ${input.files[0].name}`;
} else {
fileData.value = null;
statusMessage.value = "No file selected";
}
};
+}
- const confirmUpload = () => {
+/**
+ * Create a new handler that sends data file to server.
+ * @param statusMessage The status message signal.
+ * @param fileData The file data signal.
+ * @returns The file confirmation handler.
+ */
+function getUploadConfirmationFunction(
+ statusMessage: Signal,
+ fileData: Signal,
+): () => void {
+ /**
+ * Add students to database.
+ * @returns Confirm upload of students.
+ */
+ return () => {
if (!fileData.value) {
statusMessage.value = "Please select a file before confirming upload.";
return;
}
- try {
- const reader = new FileReader();
+ const reader = new FileReader();
- reader.onload = async (e) => {
- const arrayBuffer = e.target?.result as ArrayBuffer;
- const workbook = XLSX.read(arrayBuffer, { type: "array" });
+ /**
+ * Send all data to the server.
+ * @param event The finished progress event.
+ */
+ reader.onload = async (event: ProgressEvent) => {
+ const arrayBuffer = event.target!.result as ArrayBuffer;
+ const workbook = XLSX.read(arrayBuffer, { type: "array" });
+ let allOK = true;
- for (const sheetName of workbook.SheetNames) {
- const sheet = workbook.Sheets[sheetName];
- const data = XLSX.utils.sheet_to_json(sheet, {
- header: ["Identifiant", "Nom", "Prénom", "Mail"],
- range: 1, // Ignorer les en-têtes
- });
+ for (const sheetName of workbook.SheetNames) {
+ const sheet = workbook.Sheets[sheetName];
+ const data = XLSX.utils.sheet_to_json(sheet, {
+ header: ["userId", "lastName", "firstName", "mail"],
+ range: 1,
+ });
- console.log(`Data from sheet ${sheetName}:`, data);
+ const response = await fetch("/students/api/students", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ promoName: sheetName, data }),
+ });
- const response = await fetch("/students/api/students", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ promoName: sheetName, data }),
- });
-
- if (!response.ok) {
- throw new Error(`Failed to insert data for promotion ${sheetName}`);
- }
+ if (!response.ok) {
+ allOK = false;
}
+ }
- statusMessage.value = "Data uploaded and inserted successfully!";
- };
+ statusMessage.value = allOK
+ ? "Failed to insert all data."
+ : "Data uploaded and inserted successfully!";
+ };
- reader.onerror = () => {
- statusMessage.value = "Error reading the file.";
- };
+ /**
+ * Display error message if any.
+ */
+ reader.onerror = () => {
+ statusMessage.value = "Error reading the file.";
+ };
- reader.readAsArrayBuffer(fileData.value);
- } catch (error) {
- console.error("Error uploading file:", error);
- statusMessage.value = "An unexpected error occurred during upload.";
- }
+ reader.readAsArrayBuffer(fileData.value);
};
+}
+
+export default function UploadStudents() {
+ const statusMessage = useSignal("");
+ const fileData = useSignal(null);
+
+ const handleFileChange = getFileChangeHandler(statusMessage, fileData);
+ const confirmUpload = getUploadConfirmationFunction(statusMessage, fileData);
return (
-
-
Upload Students
+ <>
{statusMessage.value}
-
+ >
);
}
diff --git a/routes/(apps)/students/api/students.ts b/routes/(apps)/students/api/students.ts
index eb1dd2b..157299f 100644
--- a/routes/(apps)/students/api/students.ts
+++ b/routes/(apps)/students/api/students.ts
@@ -32,31 +32,50 @@ function getItself(
/**
* Gets itself from the database.
- * @param database The database connection
+ * @param database The database connexion
* @param userId The user ID.
* @returns Itself from the database.
*/
function getAll(
database: Database,
- userId: string,
-): { student: Student | null; promo: Promotion | null } {
- const studentQuery = "select * from students where userId = ?";
- const student: Student | undefined = database.prepare(studentQuery).get(
- userId,
- );
+): { students: Student[]; promos: Promotion[] } {
+ const studentsQuery = `
+ select userId, firstName, lastName, mail, promotionId
+ from students inner join promotions
+ on students.promotionId = promotions.id
+ where promotions.current < 6`;
+ const students: Student[] = database.prepare(studentsQuery).all();
- if (!student) {
- return { student: null, promo: null };
- }
+ const promosQuery = "select * from promotions where promotions.current < 6";
+ const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
- const promoQuery = "select * from promotions where id = ?";
- const promo: Promotion | undefined = database.prepare(promoQuery).get(
- student.promotionId,
- );
-
- return { student, promo: promo ?? null };
+ return { students, promos };
}
+/**
+ * Add users to the database.
+ * @param database The database connexion
+ * @param students The students to add
+ * @param promoId The promotion id.
+ */
+function addStudents(database: Database, students: Student[], promoId: string) {
+ const query = `
+ INSERT INTO students
+ (userId, firstName, lastName, mail, promotionId)
+ VALUES (?, ?, ?, ?, ?)`;
+
+ const statement = database.prepare(query);
+
+ for (const student of students) {
+ statement.run(
+ student.userId,
+ student.firstName,
+ student.lastName,
+ student.mail,
+ promoId,
+ );
+ }
+}
export const handler: Handlers = {
/**
@@ -75,9 +94,7 @@ export const handler: Handlers = {
if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response(
- JSON.stringify({
- student: getItself(database, context.state.session.uid),
- }),
+ JSON.stringify(getItself(database, context.state.session.uid)),
{
headers: {
"content-type": "application/json",
@@ -86,70 +103,49 @@ export const handler: Handlers = {
);
}
- const promotions = database.prepare("select id, name from promotions")
- .all();
-
- const students = database.prepare(
- "select userId, firstName, lastName, mail, promotionId from students",
- ).all();
-
return new Response(
- JSON.stringify({ promotions, students }),
+ JSON.stringify(getAll(database)),
{
- status: 200,
- headers: { "Content-Type": "application/json" },
+ headers: {
+ "content-type": "application/json",
+ },
},
);
},
+ /**
+ * Add students in the database.
+ * @param request The HTTP request.
+ * @param _context The Fresh context.
+ * @returns HTTP 201 on successful insert.
+ */
+ async POST(
+ request: Request,
+ _context: FreshContext,
+ ): Promise {
+ const { students, promo }: { students: Student[]; promo: string } =
+ await request.json();
- async POST(request) {
- console.log("API /students/api/insert_students called");
-
- try {
- const body = await request.json();
- const { data, promoName } = body;
-
- console.log("Received data:", { promoName, data });
-
- if (!promoName || !Array.isArray(data)) {
- throw new Error("Invalid request body");
- }
-
- using connection = connect("students");
-
- connection.database.prepare(
- "INSERT OR IGNORE INTO promotions (name) VALUES (?)",
- ).run(promoName);
-
- const promoIdRow: { id: number } = connection.database
- .prepare("SELECT id FROM promotions WHERE name = ?")
- .get(promoName)!;
- const promoId = promoIdRow.id;
-
- console.log(`Promotion ID for "${promoName}":`, promoId);
-
- const insertQuery = connection.database.prepare(
- `INSERT INTO students
- (userId, firstName, lastName, mail, promotionId)
- VALUES (?, ?, ?, ?, ?)`,
- );
-
- for (const student of data) {
- console.log("Inserting student:", student);
- insertQuery.run(
- student.Identifiant,
- student.Nom,
- student["Prénom"],
- student.Mail,
- promoId,
- );
- }
-
- console.log("All data inserted successfully");
- return new Response("Data inserted successfully", { status: 201 });
- } catch (error) {
- console.error("Error inserting data:", error);
- return new Response("Failed to insert data", { status: 500 });
+ if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
+ return new Response(null, { status: 400 });
}
+
+ using connection = connect("students");
+ const database = connection.database;
+
+ const { endyear, current } = promo.match(
+ /^(?\d{4})-(?\d)A$/,
+ )?.groups!;
+
+ database.prepare(
+ "insert or ignore into promotions (endyear, current) values (?, ?)",
+ ).run(endyear, current);
+
+ const { id: promoId }: { id: string } = database
+ .prepare("select id from promotions where endyear = ? and current = ?")
+ .get(endyear, current)!;
+
+ addStudents(database, students, promoId);
+
+ return new Response(null, { status: 201 });
},
};
diff --git a/routes/(apps)/students/partials/(admin)/consult.tsx b/routes/(apps)/students/partials/(admin)/consult.tsx
index c46c2a5..b685c5c 100644
--- a/routes/(apps)/students/partials/(admin)/consult.tsx
+++ b/routes/(apps)/students/partials/(admin)/consult.tsx
@@ -1,4 +1,4 @@
-import ConsultStudents from "$root/routes/(apps)/students/(_islands)/ConsultStudents.tsx";
+import ConsultStudents from "../../(_islands)/ConsultStudents.tsx";
import {
getPartialsConfig,
makePartials,
@@ -10,7 +10,7 @@ import { State } from "$root/defaults/interfaces.ts";
async function Students(_request: Request, _context: FreshContext) {
return (
<>
- Manage Promotions
+ Consult students
>
);
diff --git a/routes/(apps)/students/partials/(admin)/upload.tsx b/routes/(apps)/students/partials/(admin)/upload.tsx
index d64608a..2f36f6d 100644
--- a/routes/(apps)/students/partials/(admin)/upload.tsx
+++ b/routes/(apps)/students/partials/(admin)/upload.tsx
@@ -1,4 +1,4 @@
-import UploadStudents from "$root/routes/(apps)/students/(_islands)/UploadStudents.tsx";
+import UploadStudents from "../../(_islands)/UploadStudents.tsx";
import {
getPartialsConfig,
makePartials,
@@ -10,7 +10,7 @@ import { State } from "$root/defaults/interfaces.ts";
async function Students(_request: Request, _context: FreshContext) {
return (
<>
- Manage Promotions
+ Upload Students
>
);
diff --git a/routes/(apps)/students/partials/index.tsx b/routes/(apps)/students/partials/index.tsx
index a54dff7..78931b5 100644
--- a/routes/(apps)/students/partials/index.tsx
+++ b/routes/(apps)/students/partials/index.tsx
@@ -4,10 +4,17 @@ import {
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts";
+import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrait.tsx";
// deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext) {
- return Welcome to {context.state.session?.displayName}.
;
+ return (
+ <>
+ Welcome {context.state.session?.givenName}!
+ Your amU identity
+
+ >
+ );
}
export const config = getPartialsConfig();
diff --git a/routes/(apps)/students/api/types.d.ts b/routes/(apps)/students/types.d.ts
similarity index 100%
rename from routes/(apps)/students/api/types.d.ts
rename to routes/(apps)/students/types.d.ts
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 8b253b8..60d03a9 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -27,6 +27,7 @@ export default async function App(
+
diff --git a/static/styles/app.css b/static/styles/app.css
index 8ddee03..8395076 100644
--- a/static/styles/app.css
+++ b/static/styles/app.css
@@ -3,7 +3,7 @@
padding: 1em 0;
display: grid;
grid-template-columns: auto 1fr;
- gap: 1em;
+ gap: 4em;
}
#app > #app-body {
@@ -16,7 +16,7 @@
}
#app > nav > a {
- padding: 0.25em 0.5em;
+ padding: 0.5em 4em 0.5em 1em;
color: light-dark(var(--light-foreground), var(--dark-foreground));
}
@@ -57,5 +57,10 @@
#app {
grid-template-rows: auto 1fr;
grid-template-columns: none;
+ gap: 1em;
+ }
+
+ #app > nav > a {
+ padding: 0.5em 1em;
}
}
diff --git a/static/styles/students.css b/static/styles/students.css
new file mode 100644
index 0000000..d7398bd
--- /dev/null
+++ b/static/styles/students.css
@@ -0,0 +1,15 @@
+#self-portrait {
+ display: grid;
+ gap: 1em;
+ grid-template-columns: auto 1fr;
+}
+
+#self-portrait > div:nth-child(2n+1) {
+ font-weight: var(--font-weight-bold);
+}
+
+@media screen and (max-width: 1024px) {
+ #self-portrait {
+ grid-template-columns: 1fr;
+ }
+}