diff --git a/databases/connect.ts b/databases/connect.ts new file mode 100644 index 0000000..1a6bbdc --- /dev/null +++ b/databases/connect.ts @@ -0,0 +1,25 @@ +import { Database } from "@db/sqlite"; + +interface DatabaseConnection extends Disposable { + database: Database; +} + +/** + * Connect to the given database and attach `students.db`. + * @param database The database name. + * @returns The database connection as disposable. + */ +export default function connect(database: string): DatabaseConnection { + const connection = new Database(`databases/data/${database}.db`, { + create: false, + }); + + connection.run("attach database 'databases/data/students.db' as students"); + + return { + database: connection, + [Symbol.dispose]: () => { + connection.close(); + }, + }; +} diff --git a/databases/init/mobility.sql b/databases/init/mobility.sql index e69de29..fe503d4 100644 --- a/databases/init/mobility.sql +++ b/databases/init/mobility.sql @@ -0,0 +1,11 @@ +CREATE TABLE mobility ( + id integer primary key autoincrement, + studentId text, + startDate date, + endDate date, + weeksCount integer, + destinationCountry text, + destinationName text, + mobilityStatus text default 'N/A', + foreign key (studentId) references students(userId) +); \ No newline at end of file diff --git a/databases/init/students.sql b/databases/init/students.sql index b503221..4dbc7e8 100644 --- a/databases/init/students.sql +++ b/databases/init/students.sql @@ -1,14 +1,15 @@ +create table promotions ( + id integer primary key autoincrement, + name text, + endyear integer, + current integer +); + create table students ( userId text primary key, firstName text, lastName text, mail text, - promo integer, - foreign key(promo) references promo(id) -); - -create table promo ( - id integer, - endyear integer, - current integer + promotionId integer, + foreign key(promotionId) references promotions(id) ); \ No newline at end of file diff --git a/fresh.gen.ts b/fresh.gen.ts index 9d9666d..2b0c7d3 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -3,19 +3,21 @@ // This file is automatically updated during development when running `dev.ts`. import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; -import * as $_apps_mobility_api_insert_students from "./routes/(apps)/mobility/api/insert_students.ts"; +import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.ts"; import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx"; -import * as $_apps_mobility_partials_admin_mobility from "./routes/(apps)/mobility/partials/(admin)/mobility.tsx"; +import * as $_apps_mobility_partials_admin_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx"; import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx"; import * as $_apps_mobility_partials_overview from "./routes/(apps)/mobility/partials/overview.tsx"; -import * as $_apps_mobility_partials_students from "./routes/(apps)/mobility/partials/students.tsx"; import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx"; import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx"; 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_example from "./routes/(apps)/students/api/example.ts"; +import * as $_apps_students_api_insert_students from "./routes/(apps)/students/api/insert_students.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_partials_overview from "./routes/(apps)/students/partials/overview.tsx"; import * as $_404 from "./routes/_404.tsx"; import * as $_app from "./routes/_app.tsx"; import * as $_middleware from "./routes/_middleware.ts"; @@ -27,36 +29,41 @@ import * as $logout from "./routes/logout.tsx"; import * as $_islands_AppNavigator from "./routes/(_islands)/AppNavigator.tsx"; import * as $_islands_Navbar from "./routes/(_islands)/Navbar.tsx"; import * as $_apps_mobility_islands_ConsultMobility from "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx"; -import * as $_apps_mobility_islands_ConsultStudents from "./routes/(apps)/mobility/(_islands)/ConsultStudents.tsx"; import * as $_apps_mobility_islands_EditMobility from "./routes/(apps)/mobility/(_islands)/EditMobility.tsx"; -import * as $_apps_mobility_islands_EditStudents from "./routes/(apps)/mobility/(_islands)/EditStudents.tsx"; import * as $_apps_mobility_islands_ImportFile from "./routes/(apps)/mobility/(_islands)/ImportFile.tsx"; -import * as $_apps_mobility_islands_UploadStudents from "./routes/(apps)/mobility/(_islands)/UploadStudents.tsx"; +import * as $_apps_students_islands_ConsultStudents from "./routes/(apps)/students/(_islands)/ConsultStudents.tsx"; +import * as $_apps_students_islands_EditStudents from "./routes/(apps)/students/(_islands)/EditStudents.tsx"; +import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx"; import type { Manifest } from "$fresh/server.ts"; const manifest = { routes: { "./routes/(apps)/_layout.tsx": $_apps_layout, - "./routes/(apps)/mobility/api/insert_students.ts": - $_apps_mobility_api_insert_students, + "./routes/(apps)/mobility/api/insert_mobility.ts": + $_apps_mobility_api_insert_mobility, "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, - "./routes/(apps)/mobility/partials/(admin)/mobility.tsx": - $_apps_mobility_partials_admin_mobility, + "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx": + $_apps_mobility_partials_admin_edit_mobility, "./routes/(apps)/mobility/partials/index.tsx": $_apps_mobility_partials_index, "./routes/(apps)/mobility/partials/overview.tsx": $_apps_mobility_partials_overview, - "./routes/(apps)/mobility/partials/students.tsx": - $_apps_mobility_partials_students, "./routes/(apps)/notes/index.tsx": $_apps_notes_index, "./routes/(apps)/notes/partials/(admin)/courses.tsx": $_apps_notes_partials_admin_courses, "./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index, "./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes, - "./routes/(apps)/students/api/example.ts": $_apps_students_api_example, + "./routes/(apps)/students/api/insert_students.ts": + $_apps_students_api_insert_students, "./routes/(apps)/students/index.tsx": $_apps_students_index, + "./routes/(apps)/students/partials/(admin)/consult.tsx": + $_apps_students_partials_admin_consult, + "./routes/(apps)/students/partials/(admin)/upload.tsx": + $_apps_students_partials_admin_upload, "./routes/(apps)/students/partials/index.tsx": $_apps_students_partials_index, + "./routes/(apps)/students/partials/overview.tsx": + $_apps_students_partials_overview, "./routes/_404.tsx": $_404, "./routes/_app.tsx": $_app, "./routes/_middleware.ts": $_middleware, @@ -71,16 +78,16 @@ const manifest = { "./routes/(_islands)/Navbar.tsx": $_islands_Navbar, "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx": $_apps_mobility_islands_ConsultMobility, - "./routes/(apps)/mobility/(_islands)/ConsultStudents.tsx": - $_apps_mobility_islands_ConsultStudents, "./routes/(apps)/mobility/(_islands)/EditMobility.tsx": $_apps_mobility_islands_EditMobility, - "./routes/(apps)/mobility/(_islands)/EditStudents.tsx": - $_apps_mobility_islands_EditStudents, "./routes/(apps)/mobility/(_islands)/ImportFile.tsx": $_apps_mobility_islands_ImportFile, - "./routes/(apps)/mobility/(_islands)/UploadStudents.tsx": - $_apps_mobility_islands_UploadStudents, + "./routes/(apps)/students/(_islands)/ConsultStudents.tsx": + $_apps_students_islands_ConsultStudents, + "./routes/(apps)/students/(_islands)/EditStudents.tsx": + $_apps_students_islands_EditStudents, + "./routes/(apps)/students/(_islands)/UploadStudents.tsx": + $_apps_students_islands_UploadStudents, }, baseUrl: import.meta.url, } satisfies Manifest; diff --git a/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx b/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx index e69de29..7befa63 100644 --- a/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx +++ b/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "preact/hooks"; + +interface Promotion { + id: number; + name: string; +} + +interface Student { + id: string; + firstName: string; + lastName: string; + promotionId: number; +} + +interface Mobility { + id: number; + studentId: string; + startDate: string | null; + endDate: string | null; + weeksCount: number | null; + destinationCountry: string | null; + destinationName: string | null; + mobilityStatus: string; +} + +export default function ConsultMobility() { + const [data, setData] = useState< + | { + promotions?: Promotion[]; + students?: Student[]; + mobilities?: Mobility[]; + } + | null + >(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + console.log("ConsultMobility: Fetching data from API..."); + try { + const response = await fetch("/mobility/api/insert_mobility"); + console.log("ConsultMobility: API response status:", response.status); + + if (!response.ok) { + throw new Error(`Error fetching data: ${response.statusText}`); + } + + const result = await response.json(); + console.log("ConsultMobility: Data fetched successfully:", result); + setData(result); + } catch (err) { + console.error("ConsultMobility: Error fetching data:", err); + setError("Failed to load mobility data. Please try again later."); + } + }; + + fetchData(); + }, []); + + if (error) { + return

{error}

; + } + + if (!data?.promotions) { + return

No promotions found.

; + } + + return ( +
+

Consult Mobility

+ {data.promotions.map((promo) => ( +
+

Promotion: {promo.name}

+ + + + + + + + + + + + + + + + {data.students + ?.filter((student) => student.promotionId === promo.id) + .map((student) => { + const mobility = data.mobilities?.find((mob) => + mob.studentId === student.id + ); + return ( + + + + + + + + + + + + ); + })} + +
IDFirst NameLast NameStart DateEnd DateWeeks CountDestination CountryDestination NameStatus
{student.id}{student.firstName}{student.lastName}{mobility?.startDate || "N/A"}{mobility?.endDate || "N/A"}{mobility?.weeksCount ?? "N/A"}{mobility?.destinationCountry || "N/A"}{mobility?.destinationName || "N/A"}{mobility?.mobilityStatus || "N/A"}
+
+ ))} +
+ ); +} diff --git a/routes/(apps)/mobility/(_islands)/ConsultStudents.tsx b/routes/(apps)/mobility/(_islands)/ConsultStudents.tsx deleted file mode 100644 index 4ca3459..0000000 --- a/routes/(apps)/mobility/(_islands)/ConsultStudents.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; - -interface Student { - id: number; - firstName: string; - lastName: string; - email: string; - promotion: string; -} - -export default function ConsultStudents() { - const [students, setStudents] = useState([]); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchStudents = async () => { - try { - const response = await fetch("/mobility/api/insert_students"); - - if (!response.ok) { - throw new Error(`Error fetching students: ${response.statusText}`); - } - - const data: Student[] = await response.json(); - setStudents(data); - } catch (err) { - console.error("Error fetching students:", err); - setError("Failed to load students. Please try again later."); - } - }; - - fetchStudents(); - }, []); - - return ( -
-

Consult Students

- {error &&

{error}

} - {students.length === 0 ?

No students found.

: ( - - - - - - - - - - - - {students.map((student) => ( - - - - - - - - ))} - -
IDFirst NameLast NameEmailPromotion
{student.id}{student.firstName}{student.lastName}{student.email}{student.promotion}
- )} -
- ); -} diff --git a/routes/(apps)/mobility/(_islands)/ConsultStudents_test.tsx b/routes/(apps)/mobility/(_islands)/ConsultStudents_test.tsx new file mode 100644 index 0000000..3e008bc --- /dev/null +++ b/routes/(apps)/mobility/(_islands)/ConsultStudents_test.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "preact/hooks"; + +interface Promotion { + id: number; + name: string; +} + +interface Student { + id: number; + firstName: string; + lastName: string; + mail: string; + promotionId: number; + promotionName: string; +} + +export default function ConsultStudents_test() { + const [data, setData] = useState< + { promotions: Promotion[]; students: Student[] } | null + >(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/students/api/insert_students"); + if (!response.ok) { + throw new Error(`Error fetching data: ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error("Error fetching data:", err); + setError("Failed to load data. Please try again later."); + } + }; + + fetchData(); + }, []); + + return ( +
+

Consult Students

+ {error &&

{error}

} + {data?.promotions.map((promo) => ( +
+

Promotion: {promo.id}

+ + + + + + + + + + + {data.students + .filter((student) => student.promotionId === promo.id) + .map((student) => ( + + + + + + + ))} + +
IDFirst NameLast NameEmail
{student.id}{student.firstName}{student.lastName}{student.mail}
+
+ ))} +
+ ); +} diff --git a/routes/(apps)/mobility/(_islands)/EditMobility.tsx b/routes/(apps)/mobility/(_islands)/EditMobility.tsx index e69de29..ef3a485 100644 --- a/routes/(apps)/mobility/(_islands)/EditMobility.tsx +++ b/routes/(apps)/mobility/(_islands)/EditMobility.tsx @@ -0,0 +1,248 @@ +import { useEffect, useState } from "preact/hooks"; + +interface Student { + id: string; + firstName: string; + lastName: string; + promotionId: number; +} + +interface Promotion { + id: number; + name: string; +} + +interface Mobility { + id: number | null; + studentId: string; + startDate: string | null; + endDate: string | null; + weeksCount: number | null; + destinationCountry: string | null; + destinationName: string | null; + mobilityStatus: string; +} + +export default function EditMobility() { + const [data, setData] = useState< + | { + promotions?: Promotion[]; + students?: Student[]; + mobilities?: Mobility[]; + } + | null + >(null); + const [error, setError] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + const fetchData = async () => { + console.log("EditMobility: Fetching data from API..."); + try { + const response = await fetch("/mobility/api/insert_mobility"); + console.log("EditMobility: API response status:", response.status); + + if (!response.ok) { + throw new Error(`Error fetching data: ${response.statusText}`); + } + + const result = await response.json(); + console.log("EditMobility: Data fetched successfully:", result); + setData(result); + } catch (err) { + console.error("EditMobility: Error fetching data:", err); + setError("Failed to load mobility data. Please try again later."); + } + }; + + fetchData(); + }, []); + + const handleChange = ( + studentId: string, + field: keyof Mobility, + value: string | number | null, + ) => { + if (!data) return; + + setData((prevData) => { + if (!prevData) return null; + + const updatedMobilities = prevData.mobilities?.map((mobility) => { + if (mobility.studentId === studentId) { + const updatedMobility = { ...mobility, [field]: value }; + + if (field === "startDate" || field === "endDate") { + const startDate = new Date(updatedMobility.startDate || ""); + const endDate = new Date(updatedMobility.endDate || ""); + if (startDate && endDate && startDate <= endDate) { + const weeks = Math.ceil( + (endDate.getTime() - startDate.getTime()) / + (7 * 24 * 60 * 60 * 1000), + ); + updatedMobility.weeksCount = weeks; + } else { + updatedMobility.weeksCount = null; + } + } + + return updatedMobility; + } + return mobility; + }) || []; + + return { ...prevData, mobilities: updatedMobilities }; + }); + }; + + const handleSave = async () => { + setIsSaving(true); + + try { + const response = await fetch("/mobility/api/insert_mobility", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data: data?.mobilities }), + }); + + console.log("EditMobility: Save response status:", response.status); + + if (response.ok) { + alert("Data saved successfully!"); + globalThis.location.reload(); + } else { + throw new Error(`Failed to save data: ${response.statusText}`); + } + } catch (error) { + console.error("EditMobility: Error saving data:", error); + alert("An error occurred while saving data."); + } finally { + setIsSaving(false); + } + }; + + if (error) { + return

{error}

; + } + + if (!data?.promotions) { + return

Loading data...

; + } + + return ( +
+

Edit Mobility

+ {data.promotions.map((promo) => ( +
+

Promotion: {promo.name}

+ + + + + + + + + + + + + + + + {data.students + ?.filter((student) => student.promotionId === promo.id) + .map((student) => { + const mobility = data.mobilities?.find((mob) => + mob.studentId === student.id + ) || { + id: null, + studentId: student.id, + startDate: null, + endDate: null, + weeksCount: null, + destinationCountry: null, + destinationName: null, + mobilityStatus: "N/A", + }; + + return ( + + + + + + + + + + + + ); + })} + +
IDFirst NameLast NameStart DateEnd DateWeeks CountDestination CountryDestination NameStatus
{student.id}{student.firstName}{student.lastName} + + handleChange( + student.id, + "startDate", + e.target.value, + )} + /> + + + handleChange(student.id, "endDate", e.target.value)} + /> + {mobility.weeksCount ?? "N/A"} + + handleChange( + student.id, + "destinationCountry", + e.target.value, + )} + /> + + + handleChange( + student.id, + "destinationName", + e.target.value, + )} + /> + + +
+
+ ))} + +
+ ); +} diff --git a/routes/(apps)/mobility/(_props)/props.ts b/routes/(apps)/mobility/(_props)/props.ts index b324ead..75a91b4 100644 --- a/routes/(apps)/mobility/(_props)/props.ts +++ b/routes/(apps)/mobility/(_props)/props.ts @@ -7,10 +7,10 @@ const properties: AppProperties = { pages: { index: "Homepage", overview: "Mobility overview", - mobility: "Mobility management", - students: "Students management", + edit_mobility: "Mobility management", + consult_students_test: "Test consult students", }, - adminOnly: ["students"], + adminOnly: ["edit_mobility", "consult_students_test"], }; export default properties; diff --git a/routes/(apps)/mobility/api/insert_mobility.ts b/routes/(apps)/mobility/api/insert_mobility.ts new file mode 100644 index 0000000..90f228f --- /dev/null +++ b/routes/(apps)/mobility/api/insert_mobility.ts @@ -0,0 +1,168 @@ +import { Handlers } from "$fresh/server.ts"; +import { Database } from "@db/sqlite"; + +export const handler: Handlers = { + // deno-lint-ignore require-await + async GET() { + try { + console.log("Connecting to mobility database..."); + const connection = new Database("databases/data/mobility.db", { + create: false, + }); + connection.run( + "ATTACH DATABASE 'databases/data/students.db' AS students", + ); + console.log("Connected to databases."); + + const students = connection.prepare( + `SELECT + students.userId AS id, + students.firstName, + students.lastName, + students.promotionId AS promotionId, + promotions.name AS promotionName + FROM students.students + LEFT JOIN students.promotions ON students.promotionId = promotions.id`, + ).all(); + + const mobilities = connection.prepare( + `SELECT + mobility.id, + mobility.studentId, + mobility.startDate, + mobility.endDate, + mobility.weeksCount, + mobility.destinationCountry, + mobility.destinationName, + mobility.mobilityStatus + FROM mobility`, + ).all(); + + const promotions = connection.prepare( + `SELECT id, name FROM students.promotions`, + ).all(); + + connection.close(); + + return new Response( + JSON.stringify({ mobilities, students, promotions }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error fetching mobility data:", error); + return new Response("Failed to fetch data", { status: 500 }); + } + }, + + async POST(request) { + console.log("API /mobility/api/insert_mobility POST called"); + + try { + const body = await request.json(); + const { data } = body; + + if (!Array.isArray(data)) { + throw new Error("Invalid request body"); + } + console.log("Connecting to mobility database..."); + const connection = new Database("databases/data/mobility.db", { + create: false, + }); + + console.log("Attaching students database..."); + connection.run( + "ATTACH DATABASE 'databases/data/students.db' AS students", + ); + console.log("Students database attached successfully."); + + const insertQuery = connection.prepare( + `INSERT INTO mobility ( + id, studentId, startDate, endDate, weeksCount, destinationCountry, destinationName, mobilityStatus + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + startDate = excluded.startDate, + endDate = excluded.endDate, + weeksCount = excluded.weeksCount, + destinationCountry = excluded.destinationCountry, + destinationName = excluded.destinationName, + mobilityStatus = excluded.mobilityStatus`, + ); + + for (const mobility of data) { + const { + id, + studentId, + startDate, + endDate, + weeksCount, + destinationCountry, + destinationName, + mobilityStatus = "N/A", + } = mobility; + + console.log("Processing mobility data:", mobility); + + const studentExists = connection + .prepare( + `SELECT COUNT(*) AS count FROM students.students WHERE userId = ?`, + ) + .get(studentId); + + console.log(`Student ${studentId} exists:`, studentExists.count > 0); + + if (studentExists.count === 0) { + console.warn(`Skipping mobility for unknown studentId: ${studentId}`); + continue; + } + + let calculatedWeeksCount = weeksCount; + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + if (start <= end) { + calculatedWeeksCount = Math.ceil( + (end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000), + ); + } else { + calculatedWeeksCount = null; + } + } + + console.log("Executing SQL insert/update query for:", { + id, + studentId, + startDate, + endDate, + calculatedWeeksCount, + destinationCountry, + destinationName, + mobilityStatus, + }); + + insertQuery.run( + id, + studentId, + startDate, + endDate, + calculatedWeeksCount, + destinationCountry, + destinationName, + mobilityStatus, + ); + } + + connection.close(); + console.log("Mobility data inserted/updated successfully."); + return new Response("Data inserted/updated successfully", { + status: 200, + }); + } catch (error) { + console.error("Error inserting mobility data:", error); + return new Response("Failed to insert/update data", { status: 500 }); + } + }, +}; diff --git a/routes/(apps)/mobility/api/insert_students.ts b/routes/(apps)/mobility/api/insert_students.ts deleted file mode 100644 index e8af2fe..0000000 --- a/routes/(apps)/mobility/api/insert_students.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; -import { Database } from "@db/sqlite"; - -export const handler: Handlers = { - // deno-lint-ignore require-await - async GET(_request, _context) { - try { - const db = new Database("databases/data/mobility.db"); - - db.prepare( - ` - CREATE TABLE IF NOT EXISTS students ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - firstName TEXT NOT NULL, - lastName TEXT NOT NULL, - email TEXT NOT NULL, - promotion TEXT NOT NULL - ); - `, - ).run(); - - const rows = db.prepare( - "SELECT id, firstName, lastName, email, promotion FROM students", - ).all(); - - db.close(); - - return new Response(JSON.stringify(rows), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } catch (error) { - console.error("Error fetching students:", error); - return new Response("Failed to fetch students", { status: 500 }); - } - }, - - async POST(request) { - console.log("API /mobility/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"); - } - - const db = new Database("databases/data/mobility.db"); - - console.log("Database opened successfully"); - - db.prepare( - ` - CREATE TABLE IF NOT EXISTS students ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - firstName TEXT NOT NULL, - lastName TEXT NOT NULL, - email TEXT NOT NULL, - promotion TEXT NOT NULL - ); - `, - ).run(); - - console.log("Table ensured successfully"); - - const insertQuery = db.prepare( - "INSERT INTO students (firstName, lastName, email, promotion) VALUES (?, ?, ?, ?)", - ); - - for (const student of data) { - console.log("Inserting student:", student); - insertQuery.run( - student.Nom, - student["Prénom"], - student.Mail, - promoName, - ); - } - - console.log("All students inserted successfully"); - db.close(); - - return new Response("Students inserted successfully", { status: 201 }); - } catch (error) { - console.error("Error inserting students:", error); - return new Response("Failed to insert students", { status: 500 }); - } - }, -}; diff --git a/routes/(apps)/mobility/partials/(admin)/consult_students_test.tsx b/routes/(apps)/mobility/partials/(admin)/consult_students_test.tsx new file mode 100644 index 0000000..8727e5d --- /dev/null +++ b/routes/(apps)/mobility/partials/(admin)/consult_students_test.tsx @@ -0,0 +1,21 @@ +import ConsultStudents_test from "$root/routes/(apps)/mobility/(_islands)/ConsultStudents_test.tsx"; +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; +//import EditStudents from "../(_islands)/EditStudents.tsx"; + +// deno-lint-ignore require-await +async function Mobility(_request: Request, _context: FreshContext) { + return ( + <> +

Test consult students

+ + + ); +} + +export const config = getPartialsConfig(); +export default makePartials(Mobility); diff --git a/routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx b/routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx new file mode 100644 index 0000000..88e75bf --- /dev/null +++ b/routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx @@ -0,0 +1,20 @@ +import EditMobility from "$root/routes/(apps)/mobility/(_islands)/EditMobility.tsx"; +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; + +// deno-lint-ignore require-await +async function Mobility(_request: Request, _context: FreshContext) { + return ( + <> +

Edit mobility

+ + + ); +} + +export const config = getPartialsConfig(); +export default makePartials(Mobility); diff --git a/routes/(apps)/mobility/partials/index.tsx b/routes/(apps)/mobility/partials/index.tsx index a5cffdb..2971e0e 100644 --- a/routes/(apps)/mobility/partials/index.tsx +++ b/routes/(apps)/mobility/partials/index.tsx @@ -2,12 +2,12 @@ import { getPartialsConfig, makePartials, } from "$root/defaults/makePartials.tsx"; -import { EmptyObject } from "$root/defaults/interfaces.ts"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; -type MobilityIndexProps = EmptyObject; - -export function Index(_props: MobilityIndexProps) { - return

Nothing to see here...

; +// deno-lint-ignore require-await +export async function Index(_request: Request, context: FreshContext) { + return

Welcome to {context.state.session?.displayName}.

; } export const config = getPartialsConfig(); diff --git a/routes/(apps)/mobility/partials/overview.tsx b/routes/(apps)/mobility/partials/overview.tsx index a73c859..c8775a6 100644 --- a/routes/(apps)/mobility/partials/overview.tsx +++ b/routes/(apps)/mobility/partials/overview.tsx @@ -1,17 +1,20 @@ -import { Partial } from "$fresh/runtime.ts"; -import { RouteConfig } from "$fresh/server.ts"; +import ConsultMobility from "$root/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx"; +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; -type ModulesProps = Record; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default function Modules(_props: ModulesProps) { +// deno-lint-ignore require-await +async function Mobility(_request: Request, _context: FreshContext) { return ( - - mobility - + <> +

Edit mobility

+ + ); } + +export const config = getPartialsConfig(); +export default makePartials(Mobility); diff --git a/routes/(apps)/mobility/partials/students.tsx b/routes/(apps)/mobility/partials/students.tsx deleted file mode 100644 index f3c7cb9..0000000 --- a/routes/(apps)/mobility/partials/students.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import UploadStudents from "../(_islands)/UploadStudents.tsx"; -import ConsultStudents from "../(_islands)/ConsultStudents.tsx"; -//import EditStudents from "../(_islands)/EditStudents.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: false, - skipInheritedLayouts: false, -}; - -export default function Students() { - return ( -
-

Manage Promotions

- -
- -
- ); -} diff --git a/routes/(apps)/students/(_islands)/ConsultStudents.tsx b/routes/(apps)/students/(_islands)/ConsultStudents.tsx new file mode 100644 index 0000000..1f82580 --- /dev/null +++ b/routes/(apps)/students/(_islands)/ConsultStudents.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "preact/hooks"; + +interface Promotion { + id: number; + name: string; +} + +interface Student { + userId: string; + firstName: string; + lastName: string; + mail: string; + promotionId: number; +} + +export default function ConsultStudents() { + const [data, setData] = useState< + { promotions: Promotion[]; students: Student[] } | null + >(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/students/api/insert_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); + setError("Failed to load data. Please try again later."); + } + }; + + fetchData(); + }, []); + + return ( +
+

Consult Students

+ {error &&

{error}

} + {data?.promotions.map((promo) => ( +
+

Promotion: {promo.name}

+ + + + + + + + + + + {data.students + .filter((student) => student.promotionId === promo.id) + .map((student) => ( + + + + + + + ))} + +
IDFirst NameLast NameEmail
{student.userId}{student.firstName}{student.lastName}{student.mail}
+
+ ))} +
+ ); +} diff --git a/routes/(apps)/mobility/(_islands)/EditStudents.tsx b/routes/(apps)/students/(_islands)/EditStudents.tsx similarity index 100% rename from routes/(apps)/mobility/(_islands)/EditStudents.tsx rename to routes/(apps)/students/(_islands)/EditStudents.tsx diff --git a/routes/(apps)/mobility/(_islands)/UploadStudents.tsx b/routes/(apps)/students/(_islands)/UploadStudents.tsx similarity index 51% rename from routes/(apps)/mobility/(_islands)/UploadStudents.tsx rename to routes/(apps)/students/(_islands)/UploadStudents.tsx index 763cdc4..244e4d2 100644 --- a/routes/(apps)/mobility/(_islands)/UploadStudents.tsx +++ b/routes/(apps)/students/(_islands)/UploadStudents.tsx @@ -11,7 +11,6 @@ export default function UploadStudents() { if (input.files && input.files.length > 0) { fileData.value = input.files[0]; statusMessage.value = "File selected: " + input.files[0].name; - console.log("File selected:", input.files[0].name); } else { fileData.value = null; statusMessage.value = "No file selected"; @@ -19,10 +18,8 @@ export default function UploadStudents() { }; const confirmUpload = () => { - console.log("Confirm Upload"); if (!fileData.value) { statusMessage.value = "Please select a file before confirming upload."; - console.error("Error: No file selected."); return; } @@ -30,42 +27,33 @@ export default function UploadStudents() { const reader = new FileReader(); reader.onload = async (e) => { - try { - const arrayBuffer = e.target?.result as ArrayBuffer; - const workbook = XLSX.read(arrayBuffer, { type: "array" }); + const arrayBuffer = e.target?.result as ArrayBuffer; + const workbook = XLSX.read(arrayBuffer, { type: "array" }); - for (const sheetName of workbook.SheetNames) { - const sheet = workbook.Sheets[sheetName]; - const data = XLSX.utils.sheet_to_json(sheet, { - header: ["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: ["Identifiant", "Nom", "Prénom", "Mail"], + range: 1, // Ignorer les en-têtes + }); - console.log(`Data from sheet ${sheetName}:`, data); + console.log(`Data from sheet ${sheetName}:`, data); - const response = await fetch("/mobility/api/insert_students", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ promoName: sheetName, data }), - }); + const response = await fetch("/students/api/insert_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) { + throw new Error(`Failed to insert data for promotion ${sheetName}`); } - - statusMessage.value = "Data uploaded and inserted successfully!"; - } catch (error) { - console.error("Error processing the file:", error); - statusMessage.value = - "Error processing the file. Please check its format."; } + + statusMessage.value = "Data uploaded and inserted successfully!"; }; - reader.onerror = (e) => { - console.error("FileReader error:", e); + reader.onerror = () => { statusMessage.value = "Error reading the file."; }; diff --git a/routes/(apps)/students/(_props)/props.ts b/routes/(apps)/students/(_props)/props.ts index 0a7a522..93a50a5 100644 --- a/routes/(apps)/students/(_props)/props.ts +++ b/routes/(apps)/students/(_props)/props.ts @@ -5,10 +5,12 @@ const properties: AppProperties = { icon: "badge", pages: { index: "Homepage", + overview: "Students overview", upload: "Upload students", + consult: "Consult students", }, - adminOnly: ["upload"], - hint: "See student information", + adminOnly: ["upload", "consult"], + hint: "Create students promotion and see informations", }; export default properties; diff --git a/routes/(apps)/students/api/example.ts b/routes/(apps)/students/api/example.ts deleted file mode 100644 index 9f04cd1..0000000 --- a/routes/(apps)/students/api/example.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - async POST(request, context) { - if (request.headers.get("content-type") != "application/json") { - return new Response(null, { - status: 400, - }); - } - - const responseBody = { - requestBody: await request.json(), - context, - }; - - return new Response(JSON.stringify(responseBody), { - headers: { - "content-type": "application/json", - }, - }); - }, -}; diff --git a/routes/(apps)/students/api/insert_students.ts b/routes/(apps)/students/api/insert_students.ts new file mode 100644 index 0000000..f1c5020 --- /dev/null +++ b/routes/(apps)/students/api/insert_students.ts @@ -0,0 +1,84 @@ +import { Handlers } from "$fresh/server.ts"; +// import { Database } from "@db/sqlite"; +import connect from "$root/databases/connect.ts"; + +export const handler: Handlers = { + // deno-lint-ignore require-await + async GET() { + try { + using connection = connect("students"); + + const promotions = connection.database.prepare( + "select id, name from promotions", + ).all(); + + const students = connection.database + .prepare( + `select userId, firstName, lastName, mail, promotionId from students`, + ) + .all(); + + return new Response( + JSON.stringify({ promotions, students }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Error fetching data:", error); + return new Response("Failed to fetch data", { status: 500 }); + } + }, + + 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 }); + } + }, +}; diff --git a/routes/(apps)/students/partials/(admin)/consult.tsx b/routes/(apps)/students/partials/(admin)/consult.tsx new file mode 100644 index 0000000..e506a9b --- /dev/null +++ b/routes/(apps)/students/partials/(admin)/consult.tsx @@ -0,0 +1,21 @@ +import ConsultStudents from "$root/routes/(apps)/students/(_islands)/ConsultStudents.tsx"; +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; +//import EditStudents from "../(_islands)/EditStudents.tsx"; + +// deno-lint-ignore require-await +async function Students(_request: Request, _context: FreshContext) { + return ( + <> +

Manage Promotions

+ + + ); +} + +export const config = getPartialsConfig(); +export default makePartials(Students); diff --git a/routes/(apps)/students/partials/(admin)/upload.tsx b/routes/(apps)/students/partials/(admin)/upload.tsx new file mode 100644 index 0000000..f1b9ef8 --- /dev/null +++ b/routes/(apps)/students/partials/(admin)/upload.tsx @@ -0,0 +1,21 @@ +import UploadStudents from "$root/routes/(apps)/students/(_islands)/UploadStudents.tsx"; +import { + getPartialsConfig, + makePartials, +} from "$root/defaults/makePartials.tsx"; +import { FreshContext } from "$fresh/server.ts"; +import { State } from "$root/routes/_middleware.ts"; +//import EditStudents from "../(_islands)/EditStudents.tsx"; + +// deno-lint-ignore require-await +async function Students(_request: Request, _context: FreshContext) { + return ( + <> +

Manage Promotions

+ + + ); +} + +export const config = getPartialsConfig(); +export default makePartials(Students); diff --git a/routes/(apps)/mobility/partials/(admin)/mobility.tsx b/routes/(apps)/students/partials/overview.tsx similarity index 85% rename from routes/(apps)/mobility/partials/(admin)/mobility.tsx rename to routes/(apps)/students/partials/overview.tsx index a73c859..ceac77c 100644 --- a/routes/(apps)/mobility/partials/(admin)/mobility.tsx +++ b/routes/(apps)/students/partials/overview.tsx @@ -11,7 +11,7 @@ export const config: RouteConfig = { export default function Modules(_props: ModulesProps) { return ( - mobility + students ); }