Compare commits

..

16 Commits

Author SHA1 Message Date
Clayzxr d79cd11b41 Init download API (not working) 2025-01-27 16:47:12 +01:00
Clayzxr 793a43ef87 Fixing bug while editing mobility 2025-01-27 16:12:43 +01:00
Clayzxr 8889dc6758 Init download file (not working yet) 2025-01-27 16:04:01 +01:00
Clayzxr 42102c150d Init file manager for Mobility 2025-01-27 16:00:07 +01:00
Clayzxr 1f4ec66a2c Minor fix 2025-01-27 14:57:08 +01:00
Clayzxr c9cb423ae2 Select promotion in EditMobility 2025-01-27 13:41:55 +01:00
Clayzxr a50bfbe975 Select promotion in ConsultMobility 2025-01-27 13:38:06 +01:00
Clayzxr e14efebf1c types.d 2025-01-27 13:25:43 +01:00
Clayzxr ea6b3d1f48 Fixing weeks count 2025-01-27 12:37:40 +01:00
Clayzxr c3d33317b4 Renamed file 2025-01-27 12:05:27 +01:00
Clayzxr 286f84f5a6 Remove console log 2025-01-27 11:16:45 +01:00
Clayzxr 37d2753c56 Working EditMobility 2025-01-27 11:11:44 +01:00
Clayzxr 9d828069a5 Bug fix 2025-01-27 10:56:51 +01:00
Clayzxr 299f820339 Minor fix 2025-01-27 09:49:12 +01:00
Clayzxr 874716c39d Using connect.ts to attach databases 2025-01-27 09:45:50 +01:00
Clayzxr b7e9df71f3 Init PMPR-34 2025-01-27 09:32:22 +01:00
43 changed files with 793 additions and 1138 deletions
-27
View File
@@ -1,27 +0,0 @@
name: "Build and push image"
on:
push:
branches:
- main
jobs:
deploy:
name: "Build Docker image"
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: registry.docker.polytech.djalim.fr
username: ${{ secrets.registry_login }}
password: ${{ secrets.registry_pass }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: registry.docker.polytech.djalim.fr/polympr:latest
+1 -1
View File
@@ -3,7 +3,7 @@ FROM denoland/deno:alpine
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN deno cache main.ts --allow-import RUN deno cache main.ts --allow-import flag
RUN deno task build RUN deno task build
USER deno USER deno
-20
View File
@@ -1,20 +0,0 @@
# Contributing
Thank you for your interest in contributing to our project! We appreciate your
help in making this project better. To get started with contributing, please
refer to our
[Contributing Guide](https://github.com/fedyna-k/PolyMPR/wiki/Contributing) on
the project's wiki.
The Contributing Guide provides detailed information on how to:
- Set up your development environment
- Submit issues and feature requests
- Fork the repository and create pull requests
- Follow our coding standards and guidelines
- Report bugs and suggest improvements
If you have any questions or need further assistance, feel free to reach out to
us by opening an issue or contacting the maintainers directly.
Happy coding! 💻✨
+1 -1
View File
@@ -7,5 +7,5 @@ CREATE TABLE mobility (
destinationCountry text, destinationCountry text,
destinationName text, destinationName text,
mobilityStatus text default 'N/A', mobilityStatus text default 'N/A',
foreign key (studentId) references students(userId) attestationFile blob
); );
+1
View File
@@ -1,5 +1,6 @@
create table promotions ( create table promotions (
id integer primary key autoincrement, id integer primary key autoincrement,
name text,
endyear integer, endyear integer,
current integer current integer
); );
+1 -2
View File
@@ -1,10 +1,9 @@
import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser"; import { type RegularTagNode, type TextNode } from "@melvdouc/xml-parser";
import { AsyncRoute } from "$fresh/src/server/types.ts"; import { AsyncRoute } from "$fresh/src/server/types.ts";
export interface AuthenticatedState { interface AuthenticatedState {
isAuthenticated: true; isAuthenticated: true;
session: CasContent; session: CasContent;
availablePages: Record<string, string>;
} }
interface UnauthenticatedState { interface UnauthenticatedState {
+1 -3
View File
@@ -29,14 +29,12 @@
"@popov/jwt": "jsr:@popov/jwt@^1.0.1", "@popov/jwt": "jsr:@popov/jwt@^1.0.1",
"@psych/sheet": "jsr:@psych/sheet@^1.0.6", "@psych/sheet": "jsr:@psych/sheet@^1.0.6",
"@std/cli": "jsr:@std/cli@^1.0.10", "@std/cli": "jsr:@std/cli@^1.0.10",
"@std/dotenv": "jsr:@std/dotenv@^0.225.3",
"preact": "https://esm.sh/preact@10.22.0", "preact": "https://esm.sh/preact@10.22.0",
"preact/": "https://esm.sh/preact@10.22.0/", "preact/": "https://esm.sh/preact@10.22.0/",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/", "$std/": "https://deno.land/std@0.216.0/",
"$root/": "./", "$root/": "./"
"$apps/": "./routes/(apps)/"
}, },
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
-2
View File
@@ -1,2 +0,0 @@
#Local mode, set to true to access admin pages with any users
LOCAL=false
-2
View File
@@ -1,8 +1,6 @@
import { defineConfig } from "$fresh/server.ts"; import { defineConfig } from "$fresh/server.ts";
import ensureDatabases from "$root/databases/ensure.ts"; import ensureDatabases from "$root/databases/ensure.ts";
import { load } from "@std/dotenv";
await load({ envPath: "./.env", export: true });
await ensureDatabases(); await ensureDatabases();
export default defineConfig({ export default defineConfig({
server: { server: {
+12 -11
View File
@@ -3,22 +3,23 @@
// 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_middleware from "./routes/(apps)/_middleware.ts"; import * as $_apps_mobility_api_download from "./routes/(apps)/mobility/api/download.ts";
import * as $_apps_mobility_api_insert_mobility from "./routes/(apps)/mobility/api/insert_mobility.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_edit_mobility from "./routes/(apps)/mobility/partials/(admin)/edit_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_types_d from "./routes/(apps)/mobility/types.d.ts";
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_students from "./routes/(apps)/students/api/students.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_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_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_types_d from "./routes/(apps)/students/types.d.ts"; 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";
@@ -31,7 +32,6 @@ 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_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_ImportFile from "./routes/(apps)/mobility/(_islands)/ImportFile.tsx";
import * as $_apps_students_islands_ConsultStudents from "./routes/(apps)/students/(_islands)/ConsultStudents.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_EditStudents from "./routes/(apps)/students/(_islands)/EditStudents.tsx";
import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx"; import * as $_apps_students_islands_UploadStudents from "./routes/(apps)/students/(_islands)/UploadStudents.tsx";
@@ -40,8 +40,8 @@ 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)/_middleware.ts": $_apps_middleware, "./routes/(apps)/mobility/api/download.ts": $_apps_mobility_api_download,
"./routes/(apps)/mobility/api/insert_mobility.ts": "./routes/(apps)/mobility/api/insert-mobility.ts":
$_apps_mobility_api_insert_mobility, $_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)/edit_mobility.tsx": "./routes/(apps)/mobility/partials/(admin)/edit_mobility.tsx":
@@ -50,12 +50,14 @@ const manifest = {
$_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/types.d.ts": $_apps_mobility_types_d,
"./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/students.ts": $_apps_students_api_students, "./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": "./routes/(apps)/students/partials/(admin)/consult.tsx":
$_apps_students_partials_admin_consult, $_apps_students_partials_admin_consult,
@@ -63,7 +65,8 @@ const manifest = {
$_apps_students_partials_admin_upload, $_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/types.d.ts": $_apps_students_types_d, "./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,
@@ -80,8 +83,6 @@ const manifest = {
$_apps_mobility_islands_ConsultMobility, $_apps_mobility_islands_ConsultMobility,
"./routes/(apps)/mobility/(_islands)/EditMobility.tsx": "./routes/(apps)/mobility/(_islands)/EditMobility.tsx":
$_apps_mobility_islands_EditMobility, $_apps_mobility_islands_EditMobility,
"./routes/(apps)/mobility/(_islands)/ImportFile.tsx":
$_apps_mobility_islands_ImportFile,
"./routes/(apps)/students/(_islands)/ConsultStudents.tsx": "./routes/(apps)/students/(_islands)/ConsultStudents.tsx":
$_apps_students_islands_ConsultStudents, $_apps_students_islands_ConsultStudents,
"./routes/(apps)/students/(_islands)/EditStudents.tsx": "./routes/(apps)/students/(_islands)/EditStudents.tsx":
-18
View File
@@ -1,18 +0,0 @@
Copyright 2025 - PolyMPR team @ Polytech Marseille
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2 -87
View File
@@ -1,89 +1,4 @@
# ✨ PolyMPR ✨ # ✨ PolyMPR ✨
**PolyMPR** (Poly Management Platform for Resources) is a modern, modular The ✨ Poly Module de Pilotage des Ressources is the ultimate tool to handle
framework built on **Deno** and **Fresh**, designed to help organizations various HR task in the INFO department.
transition their HR systems to the cloud. With its **modulith architecture**,
PolyMPR simplifies the development, deployment, and maintenance of HR
applications, making it the perfect choice for teams looking to modernize their
workflows. 🌐
## Features ✨
- **Modular Design**: Easily add, remove, or update features without disrupting
the entire system. 🧩
- **Cloud-Native**: Built for the cloud, enabling seamless integration with
cloud services (amU DataCenter). ☁️
- **Deno-Powered**: Utilizes Deno's secure runtime for TypeScript. 🦕
- **Fresh Framework**: Delivers fast, edge-ready web applications with minimal
overhead. ⚡
- **HR-Focused**: Tailored to meet the unique needs of INFO's HR. 👩‍💼👨‍💼
## Getting Started 🛠️
### Prerequisites
- **Deno**: Install Deno by following the
[official guide](https://deno.land/#installation).
- **Docker** (optional): Install Docker for containerized deployments. Follow
the [Docker installation guide](https://docs.docker.com/get-docker/).
### Installation
1. Clone the PolyMPR repository:
```bash
git clone https://github.com/fedyna-k/PolyMPR.git
cd PolyMPR
```
2. Start the application:
```bash
deno task start
```
3. Access the application at `https://localhost`.
For detailed installation instructions, check out the
[Installation Guide](./wiki/installation).
## Modules Overview 🧩
PolyMPR comes with a variety of modules to streamline HR processes.
To learn how to create a module, visit the [Module Overview](./wiki/modules).
## CLI Documentation 📄
The **PolyMPR CLI** simplifies development tasks. Here are some common commands:
- Create a new module:
```bash
pmpr module create <module-name-kebab-case>
```
For detailed CLI usage, check out the [CLI Documentation](./wiki/cli).
## Contributing 🤝
We welcome contributions from the community! Whether you're fixing bugs, adding
features, or improving documentation, your help is appreciated. Heres how to
get started:
1. Create a new issue.
2. Create a new branch for your changes:
```bash
git checkout -b PMPR-:ISSUE_ID:
```
3. Commit your changes and push them to your branch.
4. Submit a pull request.
For more details, read the [Contributing Guide](./contributing).
## Community and Support 🌟
Join the PolyMPR community to connect with other users and developers:
- **GitHub Discussions**: Ask questions and share ideas. 💬
- **Issue Tracker**: Report bugs or request features. 🐛
## License 📜
PolyMPR is open-source and released under the **MIT License**. Feel free to use,
modify, and distribute it as per the license terms.
+7 -4
View File
@@ -1,19 +1,22 @@
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts"; import { Partial } from "$fresh/runtime.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { State } from "$root/defaults/interfaces.ts";
import { AppProperties } from "$root/defaults/interfaces.ts";
import Navbar from "$root/routes/(_islands)/Navbar.tsx"; import Navbar from "$root/routes/(_islands)/Navbar.tsx";
// deno-lint-ignore require-await
export default async function AppLayout( export default async function AppLayout(
request: Request, request: Request,
context: FreshContext<AuthenticatedState>, context: FreshContext<State>,
) { ) {
const pathname = new URL(request.url).pathname; const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1]; const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
return ( return (
<section id="app"> <section id="app">
<Navbar currentApp={currentApp} pages={context.state.availablePages} /> <Navbar currentApp={currentApp} pages={properties.pages} />
<section id="app-body"> <section id="app-body">
<Partial name="body"> <Partial name="body">
<context.Component /> <context.Component />
-36
View File
@@ -1,36 +0,0 @@
import { FreshContext, MiddlewareHandler } from "$fresh/server.ts";
import {
AppProperties,
AuthenticatedState,
} from "$root/defaults/interfaces.ts";
export const handler: MiddlewareHandler<AuthenticatedState>[] = [
/**
* Get all available pages for current user.
* @param request The HTTP incomming request.
* @param context The Fresh context object with custom `AuthenticatedState`.
* @returns The response from the next middleware.
*/
async function getAllAvailablePages(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const pathname = new URL(request.url).pathname;
const currentApp = pathname.split("/")[1];
const properties: AppProperties = (await import(
`./${currentApp}/(_props)/props.ts`
)).default;
context.state.availablePages = properties.pages;
if (
context.state.session.eduPersonPrimaryAffiliation == "student" &&
Deno.env.get("LOCAL") != "true"
) {
properties.adminOnly.forEach((page) =>
delete context.state.availablePages[page]
);
}
return await context.next();
},
];
@@ -1,75 +1,100 @@
import { useEffect, useState } from "preact/hooks"; 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() { export default function ConsultMobility() {
const [data, setData] = useState< const [mobilityData, setMobilityData] = useState<MobilityData[]>([]);
| { const [promotions, setPromotions] = useState<Promotion[]>([]);
promotions?: Promotion[]; const [selectedPromotion, setSelectedPromotion] = useState<number | "all">("all");
students?: Student[];
mobilities?: Mobility[];
}
| null
>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
console.log("ConsultMobility: Fetching data from API...");
try { try {
const response = await fetch("/mobility/api/insert_mobility"); console.log("ConsultMobility: Fetching data from API...");
console.log("ConsultMobility: API response status:", response.status); const response = await fetch("/mobility/api/insert-mobility");
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching data: ${response.statusText}`); throw new Error(`Error fetching data: ${response.statusText}`);
} }
const result = await response.json(); const result = await response.json();
console.log("ConsultMobility: Data fetched successfully:", result); console.log("ConsultMobility: Data fetched successfully:", result);
setData(result);
setPromotions(result.promotions);
const mergedData = result.students.map((student: any) => {
const existingMobility = result.mobilities.find(
(mobility: any) => mobility.studentId === student.id
);
return {
id: existingMobility ? existingMobility.id : null,
studentId: student.id,
firstName: student.firstName,
lastName: student.lastName,
startDate: existingMobility?.startDate || null,
endDate: existingMobility?.endDate || null,
weeksCount: existingMobility?.weeksCount || null,
destinationCountry: existingMobility?.destinationCountry || null,
destinationName: existingMobility?.destinationName || null,
mobilityStatus: existingMobility?.mobilityStatus || "N/A",
promotionId: student.promotionId,
promotionName: student.promotionName,
attestationFile: existingMobility?.attestationFile || null,
};
});
setMobilityData(mergedData);
} catch (err) { } catch (err) {
console.error("ConsultMobility: Error fetching data:", err); console.error("ConsultMobility: Error fetching data:", err);
setError("Failed to load mobility data. Please try again later."); setError("Failed to load data. Please try again later.");
} }
}; };
fetchData(); fetchData();
}, []); }, []);
if (error) { const filteredData =
return <p className="error">{error}</p>; selectedPromotion === "all"
? mobilityData
: mobilityData.filter((entry) => entry.promotionId === selectedPromotion);
const downloadFile = (id: number | null) => {
if (!id) {
alert("No file available for download.");
return;
} }
if (!data?.promotions) { const downloadUrl = `/mobility/api/download/${id}`;
return <p>No promotions found.</p>; window.open(downloadUrl, "_blank");
} };
return ( return (
<section> <section>
<h2>Consult Mobility</h2> <h2>Consult Mobility</h2>
{data.promotions.map((promo) => ( {error && <p className="error">{error}</p>}
<div>
<label htmlFor="promotionSelect">Select Promotion: </label>
<select
id="promotionSelect"
value={selectedPromotion}
onChange={(e) =>
setSelectedPromotion(
e.target.value === "all" ? "all" : Number(e.target.value)
)
}
>
<option value="all">All Promotions</option>
{promotions.map((promo) => (
<option key={promo.id} value={promo.id}>
{promo.name}
</option>
))}
</select>
</div>
{promotions.map((promo) => (
<div key={promo.id}> <div key={promo.id}>
{selectedPromotion === "all" || selectedPromotion === promo.id ? (
<>
<h3>Promotion: {promo.name}</h3> <h3>Promotion: {promo.name}</h3>
<table> <table>
<thead> <thead>
@@ -83,31 +108,40 @@ export default function ConsultMobility() {
<th>Destination Country</th> <th>Destination Country</th>
<th>Destination Name</th> <th>Destination Name</th>
<th>Status</th> <th>Status</th>
<th>Attestation File</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.students {filteredData
?.filter((student) => student.promotionId === promo.id) .filter((entry) => entry.promotionId === promo.id)
.map((student) => { .map((entry) => (
const mobility = data.mobilities?.find((mob) => <tr key={entry.studentId}>
mob.studentId === student.id <td>{entry.studentId}</td>
); <td>{entry.firstName}</td>
return ( <td>{entry.lastName}</td>
<tr key={student.id}> <td>{entry.startDate || "N/A"}</td>
<td>{student.id}</td> <td>{entry.endDate || "N/A"}</td>
<td>{student.firstName}</td> <td>{entry.weeksCount || "0"}</td>
<td>{student.lastName}</td> <td>{entry.destinationCountry || "N/A"}</td>
<td>{mobility?.startDate || "N/A"}</td> <td>{entry.destinationName || "N/A"}</td>
<td>{mobility?.endDate || "N/A"}</td> <td>{entry.mobilityStatus}</td>
<td>{mobility?.weeksCount ?? "N/A"}</td> <td>
<td>{mobility?.destinationCountry || "N/A"}</td> {entry.attestationFile ? (
<td>{mobility?.destinationName || "N/A"}</td> <button
<td>{mobility?.mobilityStatus || "N/A"}</td> onClick={() => downloadFile(entry.id)}
>
Download
</button>
) : (
"No file"
)}
</td>
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</>
) : null}
</div> </div>
))} ))}
</section> </section>
@@ -1,75 +0,0 @@
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>
);
}
+197 -149
View File
@@ -1,117 +1,97 @@
import { useEffect, useState } from "preact/hooks"; 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() { export default function EditMobility() {
const [data, setData] = useState< const [mobilityData, setMobilityData] = useState<MobilityData[]>([]);
| { const [promotions, setPromotions] = useState<Promotion[]>([]);
promotions?: Promotion[]; const [selectedPromotion, setSelectedPromotion] = useState<number | "all">("all");
students?: Student[];
mobilities?: Mobility[];
}
| null
>(null);
const [error, setError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
useEffect(() => { useEffect(() => {
const fetchData = async () => { async function fetchMobilityData() {
console.log("EditMobility: Fetching data from API..."); console.log("EditMobility: Fetching data from API...");
try { const response = await fetch("/mobility/api/insert-mobility");
const response = await fetch("/mobility/api/insert_mobility"); const data = await response.json();
console.log("EditMobility: API response status:", response.status); console.log("EditMobility: Data fetched successfully:", data);
if (!response.ok) { setPromotions(data.promotions);
throw new Error(`Error fetching data: ${response.statusText}`);
}
const result = await response.json(); const initializedData = data.students.map((student: any) => {
console.log("EditMobility: Data fetched successfully:", result); const existingMobility = data.mobilities.find(
setData(result); (mobility: any) => mobility.studentId === student.id
} catch (err) { );
console.error("EditMobility: Error fetching data:", err); return {
setError("Failed to load mobility data. Please try again later."); id: existingMobility ? existingMobility.id : null,
} studentId: student.id,
firstName: student.firstName,
lastName: student.lastName,
startDate: existingMobility?.startDate || null,
endDate: existingMobility?.endDate || null,
weeksCount: existingMobility?.weeksCount || null,
destinationCountry: existingMobility?.destinationCountry || null,
destinationName: existingMobility?.destinationName || null,
mobilityStatus: existingMobility?.mobilityStatus || "N/A",
attestationFile: existingMobility?.attestationFile || null,
promotionId: student.promotionId,
promotionName: student.promotionName,
}; };
});
setMobilityData(initializedData);
}
fetchData(); fetchMobilityData();
}, []); }, []);
const handleChange = ( const handleFileChange = (studentId: string, file: File | null) => {
studentId: string, if (file && file.type !== "application/pdf") {
field: keyof Mobility, alert("Only PDF files are allowed.");
value: string | number | null, return;
) => { }
if (!data) return;
setData((prevData) => { setMobilityData((prev) =>
if (!prevData) return null; prev.map((entry) =>
entry.studentId === studentId ? { ...entry, attestationFile: file } : entry
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 () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
try { try {
const response = await fetch("/mobility/api/insert_mobility", { console.log("EditMobility: Sending data to API...");
method: "POST",
headers: { "Content-Type": "application/json" }, const formData = new FormData();
body: JSON.stringify({ data: data?.mobilities }),
mobilityData.forEach((entry) => {
formData.append(
"data",
JSON.stringify({
id: entry.id,
studentId: entry.studentId,
startDate: entry.startDate,
endDate: entry.endDate,
destinationCountry: entry.destinationCountry,
destinationName: entry.destinationName,
mobilityStatus: entry.mobilityStatus,
})
);
if (entry.attestationFile instanceof File) {
formData.append(`file_${entry.studentId}`, entry.attestationFile);
}
}); });
console.log("EditMobility: Save response status:", response.status); const response = await fetch("/mobility/api/insert-mobility", {
method: "POST",
body: formData,
});
if (response.ok) { if (response.ok) {
alert("Data saved successfully!"); alert("Data saved successfully!");
globalThis.location.reload(); console.log("EditMobility: Save response status:", response.status);
} else { } else {
throw new Error(`Failed to save data: ${response.statusText}`); alert("Failed to save data.");
console.error("EditMobility: Save response status:", response.status);
} }
} catch (error) { } catch (error) {
console.error("EditMobility: Error saving data:", error); console.error("EditMobility: Error saving data:", error);
@@ -121,20 +101,49 @@ export default function EditMobility() {
} }
}; };
if (error) { const filteredData =
return <p className="error">{error}</p>; selectedPromotion === "all"
} ? mobilityData
: mobilityData.filter((entry) => entry.promotionId === selectedPromotion);
if (!data?.promotions) { const groupedData = promotions.map((promo) => ({
return <p>Loading data...</p>; promotion: promo.name,
} students: filteredData.filter((entry) => entry.promotionId === promo.id),
}));
const handleDownload = (id: number) => {
window.open(`/mobility/api/download/${id}`, "_blank");
};
return ( return (
<section> <div>
<h2>Edit Mobility</h2> <h2>Edit Mobility</h2>
{data.promotions.map((promo) => (
<div key={promo.id}> <div>
<h3>Promotion: {promo.name}</h3> <label htmlFor="promotionSelect">Select Promotion: </label>
<select
id="promotionSelect"
value={selectedPromotion}
onChange={(e) =>
setSelectedPromotion(
e.target.value === "all" ? "all" : Number(e.target.value)
)
}
>
<option value="all">All Promotions</option>
{promotions.map((promo) => (
<option key={promo.id} value={promo.id}>
{promo.name}
</option>
))}
</select>
</div>
{groupedData.map((group) => (
<div key={group.promotion}>
{group.students.length > 0 && (
<>
<h3>Promotion: {group.promotion}</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -147,84 +156,88 @@ export default function EditMobility() {
<th>Destination Country</th> <th>Destination Country</th>
<th>Destination Name</th> <th>Destination Name</th>
<th>Status</th> <th>Status</th>
<th>Attestation File</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.students {group.students.map((entry) => (
?.filter((student) => student.promotionId === promo.id) <tr key={entry.studentId}>
.map((student) => { <td>{entry.studentId}</td>
const mobility = data.mobilities?.find((mob) => <td>{entry.firstName}</td>
mob.studentId === student.id <td>{entry.lastName}</td>
) || {
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> <td>
<input <input
type="date" type="date"
value={mobility.startDate || ""} value={entry.startDate || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"startDate", data.studentId === entry.studentId
e.target.value, ? { ...data, startDate: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<input <input
type="date" type="date"
value={mobility.endDate || ""} value={entry.endDate || ""}
onChange={(e) => onChange={(e) =>
handleChange(student.id, "endDate", e.target.value)} setMobilityData((prev) =>
prev.map((data) =>
data.studentId === entry.studentId
? { ...data, endDate: e.target.value }
: data
)
)
}
/> />
</td> </td>
<td>{mobility.weeksCount ?? "N/A"}</td> <td>{entry.weeksCount || "0"}</td>
<td> <td>
<input <input
type="text" type="text"
value={mobility.destinationCountry || ""} value={entry.destinationCountry || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"destinationCountry", data.studentId === entry.studentId
e.target.value, ? { ...data, destinationCountry: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<input <input
type="text" type="text"
value={mobility.destinationName || ""} value={entry.destinationName || ""}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"destinationName", data.studentId === entry.studentId
e.target.value, ? { ...data, destinationName: e.target.value }
)} : data
)
)
}
/> />
</td> </td>
<td> <td>
<select <select
value={mobility.mobilityStatus} value={entry.mobilityStatus}
onChange={(e) => onChange={(e) =>
handleChange( setMobilityData((prev) =>
student.id, prev.map((data) =>
"mobilityStatus", data.studentId === entry.studentId
e.target.value, ? { ...data, mobilityStatus: e.target.value }
)} : data
)
)
}
> >
<option value="N/A">N/A</option> <option value="N/A">N/A</option>
<option value="Planned">Planned</option> <option value="Planned">Planned</option>
@@ -233,16 +246,51 @@ export default function EditMobility() {
<option value="Validated">Validated</option> <option value="Validated">Validated</option>
</select> </select>
</td> </td>
<td>
{entry.attestationFile ? (
<>
<button onClick={() => handleDownload(entry.id!)}>
Download
</button>
<button
onClick={() =>
setMobilityData((prev) =>
prev.map((data) =>
data.studentId === entry.studentId
? { ...data, attestationFile: null }
: data
)
)
}
>
Delete
</button>
</>
) : (
<input
type="file"
accept=".pdf"
onChange={(e) =>
handleFileChange(
entry.studentId,
e.target.files?.[0] || null
)
}
/>
)}
</td>
</tr> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</>
)}
</div> </div>
))} ))}
<button type="button" onClick={handleSave} disabled={isSaving}>
<button onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Confirm"} {isSaving ? "Saving..." : "Confirm"}
</button> </button>
</section> </div>
); );
} }
+1 -2
View File
@@ -8,9 +8,8 @@ const properties: AppProperties = {
index: "Homepage", index: "Homepage",
overview: "Mobility overview", overview: "Mobility overview",
edit_mobility: "Mobility management", edit_mobility: "Mobility management",
consult_students_test: "Test consult students",
}, },
adminOnly: ["edit_mobility", "consult_students_test"], adminOnly: ["edit_mobility"],
}; };
export default properties; export default properties;
+43
View File
@@ -0,0 +1,43 @@
import { Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
async GET(request) {
try {
console.log("API /mobility/api/download/:id GET called");
const url = new URL(request.url);
const id = url.pathname.split("/").pop();
if (!id) {
return new Response("Invalid request: Missing ID", { status: 400 });
}
console.log("Connecting to mobility database...");
using connection = connect("mobility");
console.log("Connected to databases.");
const query = connection.database.prepare(
`SELECT attestationFile FROM mobility WHERE id = ?`
);
const result = query.get(id);
if (!result || !result.attestationFile) {
return new Response("No file found for the given ID", { status: 404 });
}
const fileBuffer = result.attestationFile;
return new Response(fileBuffer, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="attestation_${id}.pdf"`,
},
});
} catch (error) {
console.error("Error fetching file:", error);
return new Response("Failed to fetch file", { status: 500 });
}
},
};
@@ -0,0 +1,131 @@
import { Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() {
try {
using connection = connect("mobility");
const mobilities = connection.database.prepare(
`SELECT
mobility.id,
mobility.studentId,
mobility.startDate,
mobility.endDate,
mobility.weeksCount,
mobility.destinationCountry,
mobility.destinationName,
mobility.mobilityStatus,
mobility.attestationFile -- Inclure le fichier
FROM mobility`
).all();
const students = connection.database.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 promotions = connection.database.prepare(
`SELECT id, name FROM students.promotions`
).all();
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 formData = await request.formData();
const dataEntries = formData.getAll("data").map((item) => JSON.parse(item as string));
console.log("Parsed data entries:", dataEntries);
const fileMap: Record<string, Uint8Array> = {};
for (const [key, value] of formData.entries()) {
if (key.startsWith("file_") && value instanceof File) {
const studentId = key.split("_")[1];
const file = value as File;
fileMap[studentId] = new Uint8Array(await file.arrayBuffer());
console.log(`File processed for studentId ${studentId}`);
}
}
using connection = connect("mobility");
const insertQuery = connection.database.prepare(
`INSERT INTO mobility (
id, studentId, startDate, endDate, weeksCount, destinationCountry, destinationName, mobilityStatus, attestationFile
)
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,
attestationFile = excluded.attestationFile`
);
for (const mobility of dataEntries) {
const {
id = null,
studentId,
startDate,
endDate,
destinationCountry,
destinationName,
mobilityStatus = "N/A",
} = mobility;
let calculatedWeeksCount = null;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (start <= end) {
const differenceInDays = Math.ceil(
(end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)
);
calculatedWeeksCount = Math.floor(differenceInDays / 7);
}
}
const attestationFile = fileMap[studentId] || null;
console.log(`Inserting/Updating mobility for studentId: ${studentId}`);
insertQuery.run(
id,
studentId,
startDate,
endDate,
calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
attestationFile
);
}
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,168 +0,0 @@
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,21 +0,0 @@
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);
+1 -1
View File
@@ -10,7 +10,7 @@ import { State } from "$root/routes/_middleware.ts";
async function Mobility(_request: Request, _context: FreshContext<State>) { async function Mobility(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h1>Edit mobility</h1> <h1>Mobility overview</h1>
<ConsultMobility /> <ConsultMobility />
</> </>
); );
+21
View File
@@ -0,0 +1,21 @@
interface Promotion {
id: number;
name: string;
}
interface MobilityData {
id: number | null;
studentId: string;
firstName: string;
lastName: string;
startDate: string | null;
endDate: string | null;
weeksCount: number | null;
destinationCountry: string | null;
destinationName: string | null;
mobilityStatus: string;
promotionId: number;
promotionName: string;
//attestationFile: File | null;
}
@@ -1,30 +0,0 @@
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 <p>Unable to find user in database.</p>;
}
return (
<div key={props.promo.id}>
<h3>Promotion {props.promo.endyear}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{props.students
.filter((student) => student.promotionId === props.promo.id)
.map((student) => <Student key={student.id} student={student} />)}
</tbody>
</table>
</div>
);
}
@@ -1,31 +0,0 @@
import { CasContent } from "$root/defaults/interfaces.ts";
type SelfPortraitProps = { self: CasContent };
const regex =
/^(?<year>\d{4})(?<month>\d{2})(?<date>\d{2})(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\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 (
<div id="self-portrait">
<div>Identity</div>
<div>{props.self.supannCivilite} {props.self.displayName}</div>
<div>Student number</div>
<div>{props.self.uid}</div>
<div>amU mail</div>
<div>{props.self.mail}</div>
<div>First amU registration</div>
<div>{validationDate.toLocaleString()}</div>
<div>amU class code</div>
<div>{props.self.supannEtuEtape}</div>
</div>
);
}
@@ -1,13 +0,0 @@
type StudentProps = { student: Student; promo?: number };
export default function Student(props: StudentProps) {
return (
<tr key={props.student.userId}>
<td>{props.student.userId}</td>
<td>{props.student.firstName}</td>
<td>{props.student.lastName}</td>
<td>{props.student.mail}</td>
{props.promo && <td>{props.promo}</td>}
</tr>
);
}
@@ -1,45 +1,75 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import Promotion from "$root/routes/(apps)/students/(_components)/Promotion.tsx";
type SingleUserResponse = { promo: Promotion; student: Student }; interface Promotion {
type ManyUsersResponse = { promos: Promotion[]; students: Student[] }; id: number;
name: string;
}
type APIResponse = SingleUserResponse | ManyUsersResponse; interface Student {
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
export default function ConsultStudents() { export default function ConsultStudents() {
const [data, setData] = useState<APIResponse | null>(null); const [data, setData] = useState<
{ promotions: Promotion[]; students: Student[] } | null
>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const response = await fetch("/students/api/students"); try {
const response = await fetch("/students/api/insert_students");
if (!response.ok) { if (!response.ok) {
setError("Failed to load data. Please try again later."); throw new Error(`Error fetching data: ${response.statusText}`);
} }
const result: APIResponse = await response.json(); const result = await response.json();
console.log("Fetched data:", result);
setData(result); setData(result);
} catch (err) {
console.error("Error fetching data:", err);
setError("Failed to load data. Please try again later.");
}
}; };
fetchData(); fetchData();
}, []); }, []);
return ( return (
<> <section>
<h2>Consult Students</h2>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
{data && ((Object.hasOwn(data, "student")) {data?.promotions.map((promo) => (
? ( <div key={promo.id}>
<Promotion <h3>Promotion: {promo.name}</h3>
students={[(data as SingleUserResponse).student]} <table>
promo={(data as SingleUserResponse).promo} <thead>
/> <tr>
) <th>ID</th>
: (data as ManyUsersResponse).promos.map((promo) => ( <th>First Name</th>
<Promotion <th>Last Name</th>
students={(data as ManyUsersResponse).students} <th>Email</th>
promo={promo} </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>
); );
} }
@@ -1,111 +1,75 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.3/package/types/index.d.ts" // @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 * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { Signal, useSignal } from "@preact/signals"; import { useSignal } from "@preact/signals";
/** export default function UploadStudents() {
* Create a new handler for file change that displays const statusMessage = useSignal<string>("");
* messages in statusMessage and gets file data in fileData. const fileData = useSignal<File | null>(null);
* @param statusMessage The status message signal.
* @param fileData The file data signal. const handleFileChange = (event: Event) => {
* @returns The file change handler.
*/
function getFileChangeHandler(
statusMessage: Signal<string>,
fileData: Signal<File | null>,
): (event: Event) => void {
/**
* Handle file change.
* @param event The file change event.
*/
return (event: Event) => {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
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;
} else { } else {
fileData.value = null; fileData.value = null;
statusMessage.value = "No file selected"; 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<string>,
fileData: Signal<File | null>,
): () => void {
/**
* Add students to database.
* @returns Confirm upload of students.
*/
return () => {
if (!fileData.value) { if (!fileData.value) {
statusMessage.value = "Please select a file before confirming upload."; statusMessage.value = "Please select a file before confirming upload.";
return; return;
} }
try {
const reader = new FileReader(); const reader = new FileReader();
/** reader.onload = async (e) => {
* Send all data to the server. const arrayBuffer = e.target?.result as ArrayBuffer;
* @param event The finished progress event.
*/
reader.onload = async (event: ProgressEvent<FileReader>) => {
const arrayBuffer = event.target!.result as ArrayBuffer;
const workbook = XLSX.read(arrayBuffer, { type: "array" }); const workbook = XLSX.read(arrayBuffer, { type: "array" });
let allOK = true;
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: ["userId", "lastName", "firstName", "mail"], header: ["Identifiant", "Nom", "Prénom", "Mail"],
range: 1, range: 1, // Ignorer les en-têtes
}); });
const response = await fetch("/students/api/students", { console.log(`Data from sheet ${sheetName}:`, data);
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) {
allOK = false; throw new Error(`Failed to insert data for promotion ${sheetName}`);
} }
} }
statusMessage.value = allOK statusMessage.value = "Data uploaded and inserted successfully!";
? "Failed to insert all data."
: "Data uploaded and inserted successfully!";
}; };
/**
* Display error message if any.
*/
reader.onerror = () => { reader.onerror = () => {
statusMessage.value = "Error reading the file."; statusMessage.value = "Error reading the file.";
}; };
reader.readAsArrayBuffer(fileData.value); reader.readAsArrayBuffer(fileData.value);
} catch (error) {
console.error("Error uploading file:", error);
statusMessage.value = "An unexpected error occurred during upload.";
}
}; };
}
export default function UploadStudents() {
const statusMessage = useSignal<string>("");
const fileData = useSignal<File | null>(null);
const handleFileChange = getFileChangeHandler(statusMessage, fileData);
const confirmUpload = getUploadConfirmationFunction(statusMessage, fileData);
return ( return (
<> <div>
<h2>Upload Students</h2>
<input type="file" accept=".xlsx, .xls" onChange={handleFileChange} /> <input type="file" accept=".xlsx, .xls" onChange={handleFileChange} />
<button type="button" onClick={confirmUpload}>Confirm Upload</button> <button onClick={confirmUpload}>Confirm Upload</button>
<p>{statusMessage.value}</p> <p>{statusMessage.value}</p>
</> </div>
); );
} }
+1
View File
@@ -5,6 +5,7 @@ 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", consult: "Consult students",
}, },
@@ -0,0 +1,83 @@
import { Handlers } from "$fresh/server.ts";
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 });
}
},
};
-151
View File
@@ -1,151 +0,0 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { Database } from "@db/sqlite";
/**
* Gets itself from the database.
* @param database The database connection
* @param userId The user ID.
* @returns Itself from the database.
*/
function getItself(
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,
);
if (!student) {
return { student: null, promo: null };
}
const promoQuery = "select * from promotions where id = ?";
const promo: Promotion | undefined = database.prepare(promoQuery).get(
student.promotionId,
);
return { student, promo: promo ?? null };
}
/**
* Gets itself from the database.
* @param database The database connexion
* @param userId The user ID.
* @returns Itself from the database.
*/
function getAll(
database: Database,
): { 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();
const promosQuery = "select * from promotions where promotions.current < 6";
const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
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<null, AuthenticatedState> = {
/**
* The students the user can see.
* @param _request The HTTP request.
* @param _context The context with authenticated state.
* @returns All students our user can see.
*/
// deno-lint-ignore require-await
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
using connection = connect("students");
const database = connection.database;
if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response(
JSON.stringify(getItself(database, context.state.session.uid)),
{
headers: {
"content-type": "application/json",
},
},
);
}
return new Response(
JSON.stringify(getAll(database)),
{
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<AuthenticatedState>,
): Promise<Response> {
const { students, promo }: { students: Student[]; promo: string } =
await request.json();
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(
/^(?<endyear>\d{4})-(?<current>\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 });
},
};
@@ -1,16 +1,17 @@
import ConsultStudents from "../../(_islands)/ConsultStudents.tsx"; import ConsultStudents from "$root/routes/(apps)/students/(_islands)/ConsultStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h2>Consult students</h2> <h1>Manage Promotions</h1>
<ConsultStudents /> <ConsultStudents />
</> </>
); );
@@ -1,16 +1,17 @@
import UploadStudents from "../../(_islands)/UploadStudents.tsx"; import UploadStudents from "$root/routes/(apps)/students/(_islands)/UploadStudents.tsx";
import { import {
getPartialsConfig, getPartialsConfig,
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
//import EditStudents from "../(_islands)/EditStudents.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
async function Students(_request: Request, _context: FreshContext<State>) { async function Students(_request: Request, _context: FreshContext<State>) {
return ( return (
<> <>
<h2>Upload Students</h2> <h1>Manage Promotions</h1>
<UploadStudents /> <UploadStudents />
</> </>
); );
+2 -9
View File
@@ -3,18 +3,11 @@ import {
makePartials, makePartials,
} from "$root/defaults/makePartials.tsx"; } from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts"; import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/defaults/interfaces.ts"; import { State } from "$root/routes/_middleware.ts";
import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrait.tsx";
// deno-lint-ignore require-await // deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) { export async function Index(_request: Request, context: FreshContext<State>) {
return ( return <h2>Welcome to {context.state.session?.displayName}.</h2>;
<>
<h2>Welcome {context.state.session?.givenName}!</h2>
<h3>Your amU identity</h3>
<SelfPortrait self={context.state.session!} />
</>
);
} }
export const config = getPartialsConfig(); export const config = getPartialsConfig();
@@ -0,0 +1,17 @@
import { Partial } from "$fresh/runtime.ts";
import { RouteConfig } from "$fresh/server.ts";
type ModulesProps = Record<string | number | symbol, never>;
export const config: RouteConfig = {
skipAppWrapper: true,
skipInheritedLayouts: true,
};
export default function Modules(_props: ModulesProps) {
return (
<Partial name="body">
<a href="students" f-partial={"notes/partials"}>students</a>
</Partial>
);
}
-13
View File
@@ -1,13 +0,0 @@
interface Student {
userId: string;
firstName: string;
lastName: string;
mail: string;
promotionId: number;
}
interface Promotion {
id: number;
endyear: number;
current: number;
}
-1
View File
@@ -27,7 +27,6 @@ export default async function App(
<link rel="stylesheet" href="/styles/main.css" /> <link rel="stylesheet" href="/styles/main.css" />
<link rel="stylesheet" href="/styles/app.css" /> <link rel="stylesheet" href="/styles/app.css" />
<link rel="stylesheet" href="styles/app-cards.css" /> <link rel="stylesheet" href="styles/app-cards.css" />
<link rel="stylesheet" href="styles/students.css" />
</head> </head>
<body f-client-nav> <body f-client-nav>
<Header link={link} /> <Header link={link} />
+2 -7
View File
@@ -3,7 +3,7 @@
padding: 1em 0; padding: 1em 0;
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 4em; gap: 1em;
} }
#app > #app-body { #app > #app-body {
@@ -16,7 +16,7 @@
} }
#app > nav > a { #app > nav > a {
padding: 0.5em 4em 0.5em 1em; padding: 0.25em 0.5em;
color: light-dark(var(--light-foreground), var(--dark-foreground)); color: light-dark(var(--light-foreground), var(--dark-foreground));
} }
@@ -57,10 +57,5 @@
#app { #app {
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: none; grid-template-columns: none;
gap: 1em;
}
#app > nav > a {
padding: 0.5em 1em;
} }
} }
-15
View File
@@ -1,15 +0,0 @@
#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;
}
}
View File