Merge pull request #32 from fedyna-k/PMPR-27

Pmpr 27
This commit is contained in:
Kevin FEDYNA
2025-01-25 10:49:09 +01:00
committed by GitHub
25 changed files with 968 additions and 282 deletions
+25
View File
@@ -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();
},
};
}
+11
View File
@@ -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)
);
+9 -8
View File
@@ -1,14 +1,15 @@
create table promotions (
id integer primary key autoincrement,
name text,
endyear integer,
current integer
);
create table students ( create table students (
userId text primary key, userId text primary key,
firstName text, firstName text,
lastName text, lastName text,
mail text, mail text,
promo integer, promotionId integer,
foreign key(promo) references promo(id) foreign key(promotionId) references promotions(id)
);
create table promo (
id integer,
endyear integer,
current integer
); );
+27 -20
View File
@@ -3,19 +3,21 @@
// This file is automatically updated during development when running `dev.ts`. // This file is automatically updated during development when running `dev.ts`.
import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; 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_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_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_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_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_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_index from "./routes/(apps)/notes/partials/index.tsx";
import * as $_apps_notes_partials_notes from "./routes/(apps)/notes/partials/notes.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_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_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 $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx"; import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts"; 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_AppNavigator from "./routes/(_islands)/AppNavigator.tsx";
import * as $_islands_Navbar from "./routes/(_islands)/Navbar.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_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_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_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"; import type { Manifest } from "$fresh/server.ts";
const manifest = { const manifest = {
routes: { routes: {
"./routes/(apps)/_layout.tsx": $_apps_layout, "./routes/(apps)/_layout.tsx": $_apps_layout,
"./routes/(apps)/mobility/api/insert_students.ts": "./routes/(apps)/mobility/api/insert_mobility.ts":
$_apps_mobility_api_insert_students, $_apps_mobility_api_insert_mobility,
"./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index,
"./routes/(apps)/mobility/partials/(admin)/mobility.tsx": "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx":
$_apps_mobility_partials_admin_mobility, $_apps_mobility_partials_admin_edit_mobility,
"./routes/(apps)/mobility/partials/index.tsx": "./routes/(apps)/mobility/partials/index.tsx":
$_apps_mobility_partials_index, $_apps_mobility_partials_index,
"./routes/(apps)/mobility/partials/overview.tsx": "./routes/(apps)/mobility/partials/overview.tsx":
$_apps_mobility_partials_overview, $_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/index.tsx": $_apps_notes_index,
"./routes/(apps)/notes/partials/(admin)/courses.tsx": "./routes/(apps)/notes/partials/(admin)/courses.tsx":
$_apps_notes_partials_admin_courses, $_apps_notes_partials_admin_courses,
"./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index, "./routes/(apps)/notes/partials/index.tsx": $_apps_notes_partials_index,
"./routes/(apps)/notes/partials/notes.tsx": $_apps_notes_partials_notes, "./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/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": "./routes/(apps)/students/partials/index.tsx":
$_apps_students_partials_index, $_apps_students_partials_index,
"./routes/(apps)/students/partials/overview.tsx":
$_apps_students_partials_overview,
"./routes/_404.tsx": $_404, "./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app, "./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware, "./routes/_middleware.ts": $_middleware,
@@ -71,16 +78,16 @@ const manifest = {
"./routes/(_islands)/Navbar.tsx": $_islands_Navbar, "./routes/(_islands)/Navbar.tsx": $_islands_Navbar,
"./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx": "./routes/(apps)/mobility/(_islands)/ConsultMobility.tsx":
$_apps_mobility_islands_ConsultMobility, $_apps_mobility_islands_ConsultMobility,
"./routes/(apps)/mobility/(_islands)/ConsultStudents.tsx":
$_apps_mobility_islands_ConsultStudents,
"./routes/(apps)/mobility/(_islands)/EditMobility.tsx": "./routes/(apps)/mobility/(_islands)/EditMobility.tsx":
$_apps_mobility_islands_EditMobility, $_apps_mobility_islands_EditMobility,
"./routes/(apps)/mobility/(_islands)/EditStudents.tsx":
$_apps_mobility_islands_EditStudents,
"./routes/(apps)/mobility/(_islands)/ImportFile.tsx": "./routes/(apps)/mobility/(_islands)/ImportFile.tsx":
$_apps_mobility_islands_ImportFile, $_apps_mobility_islands_ImportFile,
"./routes/(apps)/mobility/(_islands)/UploadStudents.tsx": "./routes/(apps)/students/(_islands)/ConsultStudents.tsx":
$_apps_mobility_islands_UploadStudents, $_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, baseUrl: import.meta.url,
} satisfies Manifest; } satisfies Manifest;
@@ -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<string | null>(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 <p className="error">{error}</p>;
}
if (!data?.promotions) {
return <p>No promotions found.</p>;
}
return (
<section>
<h2>Consult Mobility</h2>
{data.promotions.map((promo) => (
<div key={promo.id}>
<h3>Promotion: {promo.name}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Start Date</th>
<th>End Date</th>
<th>Weeks Count</th>
<th>Destination Country</th>
<th>Destination Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{data.students
?.filter((student) => student.promotionId === promo.id)
.map((student) => {
const mobility = data.mobilities?.find((mob) =>
mob.studentId === student.id
);
return (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{mobility?.startDate || "N/A"}</td>
<td>{mobility?.endDate || "N/A"}</td>
<td>{mobility?.weeksCount ?? "N/A"}</td>
<td>{mobility?.destinationCountry || "N/A"}</td>
<td>{mobility?.destinationName || "N/A"}</td>
<td>{mobility?.mobilityStatus || "N/A"}</td>
</tr>
);
})}
</tbody>
</table>
</div>
))}
</section>
);
}
@@ -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<Student[]>([]);
const [error, setError] = useState<string | null>(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 (
<section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>}
{students.length === 0 ? <p>No students found.</p> : (
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Promotion</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.email}</td>
<td>{student.promotion}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
);
}
@@ -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<string | null>(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 (
<section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>}
{data?.promotions.map((promo) => (
<div key={promo.id}>
<h3>Promotion: {promo.id}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{data.students
.filter((student) => student.promotionId === promo.id)
.map((student) => (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.mail}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</section>
);
}
@@ -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<string | null>(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 <p className="error">{error}</p>;
}
if (!data?.promotions) {
return <p>Loading data...</p>;
}
return (
<section>
<h2>Edit Mobility</h2>
{data.promotions.map((promo) => (
<div key={promo.id}>
<h3>Promotion: {promo.name}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Start Date</th>
<th>End Date</th>
<th>Weeks Count</th>
<th>Destination Country</th>
<th>Destination Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={student.id}>
<td>{student.id}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>
<input
type="date"
value={mobility.startDate || ""}
onChange={(e) =>
handleChange(
student.id,
"startDate",
e.target.value,
)}
/>
</td>
<td>
<input
type="date"
value={mobility.endDate || ""}
onChange={(e) =>
handleChange(student.id, "endDate", e.target.value)}
/>
</td>
<td>{mobility.weeksCount ?? "N/A"}</td>
<td>
<input
type="text"
value={mobility.destinationCountry || ""}
onChange={(e) =>
handleChange(
student.id,
"destinationCountry",
e.target.value,
)}
/>
</td>
<td>
<input
type="text"
value={mobility.destinationName || ""}
onChange={(e) =>
handleChange(
student.id,
"destinationName",
e.target.value,
)}
/>
</td>
<td>
<select
value={mobility.mobilityStatus}
onChange={(e) =>
handleChange(
student.id,
"mobilityStatus",
e.target.value,
)}
>
<option value="N/A">N/A</option>
<option value="Planned">Planned</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
<option value="Validated">Validated</option>
</select>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
))}
<button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Confirm"}
</button>
</section>
);
}
+3 -3
View File
@@ -7,10 +7,10 @@ const properties: AppProperties = {
pages: { pages: {
index: "Homepage", index: "Homepage",
overview: "Mobility overview", overview: "Mobility overview",
mobility: "Mobility management", edit_mobility: "Mobility management",
students: "Students management", consult_students_test: "Test consult students",
}, },
adminOnly: ["students"], adminOnly: ["edit_mobility", "consult_students_test"],
}; };
export default properties; export default properties;
@@ -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 });
}
},
};
@@ -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 });
}
},
};
@@ -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<State>) {
return (
<>
<h1>Test consult students</h1>
<ConsultStudents_test />
</>
);
}
export const config = getPartialsConfig();
export default makePartials(Mobility);
@@ -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<State>) {
return (
<>
<h1>Edit mobility</h1>
<EditMobility />
</>
);
}
export const config = getPartialsConfig();
export default makePartials(Mobility);
+5 -5
View File
@@ -2,12 +2,12 @@ import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } 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; // deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) {
export function Index(_props: MobilityIndexProps) { return <h2>Welcome to {context.state.session?.displayName}.</h2>;
return <p>Nothing to see here...</p>;
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
+16 -13
View File
@@ -1,17 +1,20 @@
import { Partial } from "$fresh/runtime.ts"; import ConsultMobility from "$root/routes/(apps)/mobility/(_islands)/ConsultMobility.tsx";
import { RouteConfig } from "$fresh/server.ts"; import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts";
type ModulesProps = Record<string | number | symbol, never>; // deno-lint-ignore require-await
async function Mobility(_request: Request, _context: FreshContext<State>) {
export const config: RouteConfig = {
skipAppWrapper: true,
skipInheritedLayouts: true,
};
export default function Modules(_props: ModulesProps) {
return ( return (
<Partial name="body"> <>
<a href="mobility" f-partial={"notes/partials"}>mobility</a> <h1>Edit mobility</h1>
</Partial> <ConsultMobility />
</>
); );
} }
export const config = getPartialsConfig();
export default makePartials(Mobility);
@@ -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 (
<section id="students-page">
<h1>Manage Promotions</h1>
<UploadStudents />
<hr />
<ConsultStudents />
</section>
);
}
@@ -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<string | null>(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 (
<section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>}
{data?.promotions.map((promo) => (
<div key={promo.id}>
<h3>Promotion: {promo.name}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{data.students
.filter((student) => student.promotionId === promo.id)
.map((student) => (
<tr key={student.userId}>
<td>{student.userId}</td>
<td>{student.firstName}</td>
<td>{student.lastName}</td>
<td>{student.mail}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</section>
);
}
@@ -11,7 +11,6 @@ export default function UploadStudents() {
if (input.files && input.files.length > 0) { if (input.files && input.files.length > 0) {
fileData.value = input.files[0]; fileData.value = input.files[0];
statusMessage.value = "File selected: " + input.files[0].name; statusMessage.value = "File selected: " + input.files[0].name;
console.log("File selected:", input.files[0].name);
} else { } else {
fileData.value = null; fileData.value = null;
statusMessage.value = "No file selected"; statusMessage.value = "No file selected";
@@ -19,10 +18,8 @@ export default function UploadStudents() {
}; };
const confirmUpload = () => { const confirmUpload = () => {
console.log("Confirm Upload");
if (!fileData.value) { if (!fileData.value) {
statusMessage.value = "Please select a file before confirming upload."; statusMessage.value = "Please select a file before confirming upload.";
console.error("Error: No file selected.");
return; return;
} }
@@ -30,42 +27,33 @@ export default function UploadStudents() {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async (e) => { reader.onload = async (e) => {
try { const arrayBuffer = e.target?.result as ArrayBuffer;
const arrayBuffer = e.target?.result as ArrayBuffer; const workbook = XLSX.read(arrayBuffer, { type: "array" });
const workbook = XLSX.read(arrayBuffer, { type: "array" });
for (const sheetName of workbook.SheetNames) { for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName]; const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { const data = XLSX.utils.sheet_to_json(sheet, {
header: ["Nom", "Prénom", "Mail"], header: ["Identifiant", "Nom", "Prénom", "Mail"],
range: 1, // Ignorer les en-têtes 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", { const response = await fetch("/students/api/insert_students", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ promoName: sheetName, data }), body: JSON.stringify({ promoName: sheetName, data }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(`Failed to insert data for promotion ${sheetName}`);
`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) => { reader.onerror = () => {
console.error("FileReader error:", e);
statusMessage.value = "Error reading the file."; statusMessage.value = "Error reading the file.";
}; };
+4 -2
View File
@@ -5,10 +5,12 @@ const properties: AppProperties = {
icon: "badge", icon: "badge",
pages: { pages: {
index: "Homepage", index: "Homepage",
overview: "Students overview",
upload: "Upload students", upload: "Upload students",
consult: "Consult students",
}, },
adminOnly: ["upload"], adminOnly: ["upload", "consult"],
hint: "See student information", hint: "Create students promotion and see informations",
}; };
export default properties; export default properties;
-22
View File
@@ -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",
},
});
},
};
@@ -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 });
}
},
};
@@ -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<State>) {
return (
<>
<h1>Manage Promotions</h1>
<ConsultStudents />
</>
);
}
export const config = getPartialsConfig();
export default makePartials(Students);
@@ -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<State>) {
return (
<>
<h1>Manage Promotions</h1>
<UploadStudents />
</>
);
}
export const config = getPartialsConfig();
export default makePartials(Students);
@@ -11,7 +11,7 @@ export const config: RouteConfig = {
export default function Modules(_props: ModulesProps) { export default function Modules(_props: ModulesProps) {
return ( return (
<Partial name="body"> <Partial name="body">
<a href="mobility" f-partial={"notes/partials"}>mobility</a> <a href="students" f-partial={"notes/partials"}>students</a>
</Partial> </Partial>
); );
} }