Skip to content

Commit 407cfc7

Browse files
committed
Import Perm, SIEP date choose, Front styles
1 parent 1e2afd2 commit 407cfc7

20 files changed

+530
-151
lines changed

backend/package-lock.json

+234-45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
"description": "",
2020
"dependencies": {
2121
"@electric-sql/pglite": "^0.2.17",
22+
"@types/multer": "^1.4.12",
2223
"axios": "^1.8.4",
2324
"bcryptjs": "^3.0.2",
2425
"big-integer": "^1.6.52",
2526
"cors": "^2.8.5",
26-
"crypto": "^1.0.1",
2727
"dotenv": "^16.5.0",
2828
"drizzle-kit": "^0.30.6",
2929
"drizzle-orm": "^0.41.0",
@@ -34,16 +34,20 @@
3434
"handlebars": "^4.7.8",
3535
"jsdom": "^26.1.0",
3636
"jsonwebtoken": "^9.0.2",
37+
"multer": "^1.4.5-lts.2",
3738
"nodemailer": "^6.10.1",
39+
"papaparse": "^5.5.2",
3840
"pg": "^8.14.1",
3941
"pg-promise": "^11.13.0",
4042
"postgres": "^3.4.5",
43+
"randombytes": "^2.1.0",
4144
"randomstring": "^1.3.1"
4245
},
4346
"devDependencies": {
4447
"@types/cors": "^2.8.17",
4548
"@types/express": "^5.0.1",
4649
"@types/node": "^22.14.1",
50+
"@types/papaparse": "^5.3.15",
4751
"@types/pg": "^8.11.12",
4852
"nodemon": "^3.1.9",
4953
"ts-node": "^10.9.2",

backend/src/controllers/permanence.controller.ts

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { Request, Response } from "express";
22
import * as permanence_service from "../services/permanence.service";
33
import { Ok, Error } from "../utils/responses";
44

5+
interface MulterRequest extends Request {
6+
file?: Express.Multer.File;
7+
}
8+
59
// Validation des données de permanence
610
const validatePermanenceData = (start_at: string, end_at: string) => {
711
const startDate = new Date(start_at);
@@ -295,4 +299,19 @@ export const removeUserToPermanence = async (req: Request, res: Response) => {
295299
}
296300
};
297301

302+
export const uploadPermanencesCSV = async (req: MulterRequest, res: Response) => {
303+
try {
304+
const file = req.file;
305+
if (!file) {
306+
Error(res, { msg: "Fichier CSV manquant." });
307+
}
308+
309+
await permanence_service.importPermanencesFromCSV(file.path);
310+
Ok(res,{ msg: "Importation réalisée avec succès." });
311+
} catch (error) {
312+
console.error("Erreur import CSV :", error);
313+
Error(res, { msg: "Échec de l'importation." });
314+
}
315+
};
316+
298317

backend/src/controllers/user.controller.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ export const getUsersByPermission = async (req: Request, res: Response) => {
3434

3535

3636
export const syncNewstudent = async (req: Request, res: Response) => {
37+
38+
const {date} = req.body;
39+
3740
try {
3841

3942
const token = await SIEP_Utils.getTokenUTTAPI();
40-
const newStudents = await SIEP_Utils.getNewStudentsFromUTTAPI(token);
43+
const newStudents = await SIEP_Utils.getNewStudentsFromUTTAPI(token, date);
4144
//const newStudentfiltered = newStudents.filter((student : any) => !noSyncEmails.includes(student.email));
4245

4346
newStudents.forEach( async (element: any) => {

backend/src/middlewares/user.middleware.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { Request, Response, NextFunction } from 'express';
2-
import jwt from 'jsonwebtoken';
3-
import { jwtSecret } from '../utils/secret';
42
import { Unauthorized } from '../utils/responses';
53

64
// Middleware pour vérifier le rôle

backend/src/routes/permanences.routes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import express from "express";
2+
import multer from "multer";
23
import * as permanenceController from "../controllers/permanence.controller";
34
import { checkRole } from "../middlewares/user.middleware";
45

56
const permanenceRouter = express.Router();
7+
const upload = multer({ dest: "uploads/" });
68

79
// Admin routes
810
permanenceRouter.post("/admin/permanence", checkRole("Admin"), permanenceController.createPermanence);
@@ -14,6 +16,7 @@ permanenceRouter.get("/admin/permanences", checkRole("Admin"), permanenceControl
1416
permanenceRouter.get("/admin/users", checkRole("Admin"), permanenceController.getUsersInPermanence);
1517
permanenceRouter.post("/admin/add", checkRole("Admin"), permanenceController.addUserToPermanence);
1618
permanenceRouter.post("/admin/remove", checkRole("Admin"), permanenceController.removeUserToPermanence);
19+
permanenceRouter.post("/admin/importpermanences", upload.single("file"), permanenceController.uploadPermanencesCSV);
1720

1821

1922
// Student routes

backend/src/services/auth.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '../database/db'
22
import bcrypt from 'bcryptjs';
33
import jwt from 'jsonwebtoken';
4-
import crypto from "crypto";
4+
import rdmb from "randombytes";
55
import { JSDOM } from 'jsdom';
66
import { cas_validate_url, jwtSecret, service_url } from '../utils/secret';
77
import * as userservice from './user.service';
@@ -139,7 +139,7 @@ export const completeRegistration = async(token : string, password : string) =>
139139
}
140140

141141
export const createRegistrationToken = async(userId: number) => {
142-
const token = crypto.randomBytes(32).toString("hex"); // Jeton bien sécurisé
142+
const token = rdmb.randomBytes(32).toString("hex"); // Jeton bien sécurisé
143143
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 90); // 90Jours
144144

145145
await db.insert(registrationSchema).values({

backend/src/services/permanence.service.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
import fs from "fs";
2+
import Papa from "papaparse";
13
import { and, count, eq, sql } from "drizzle-orm";
24
import { userSchema } from "../schemas/Basic/user.schema";
35
import { permanenceSchema } from "../schemas/Basic/permanence.schema";
46
import { db } from "../database/db";
57
import { userPermanenceSchema } from "../schemas/Relational/userpermanences.schema";
68

9+
type CsvPermanence = {
10+
name: string;
11+
description: string;
12+
location: string;
13+
start_at: string; // ISO string
14+
end_at: string;
15+
capacity: string;
16+
is_open: string; // 'true' or 'false'
17+
};
18+
719
// Classes d'erreurs personnalisées
820
class UnauthorizedError extends Error {}
921
class AlreadyRegisteredError extends Error {}
@@ -250,4 +262,30 @@ export const getAllPermanencesWithUsers = async () => {
250262
);
251263

252264
return results;
253-
};
265+
};
266+
267+
export const importPermanencesFromCSV = async (filePath: string): Promise<void> => {
268+
const fileContent = fs.readFileSync(filePath, "utf8");
269+
270+
const { data, errors } = Papa.parse<CsvPermanence>(fileContent, {
271+
header: true,
272+
skipEmptyLines: true,
273+
});
274+
275+
if (errors.length > 0) {
276+
console.error("CSV parsing errors:", errors);
277+
throw new Error("Erreur lors du parsing du CSV.");
278+
}
279+
280+
const parsedData = data.map((r) => ({
281+
name: r.name,
282+
description: r.description,
283+
location: r.location,
284+
start_at: new Date(r.start_at),
285+
end_at: new Date(r.end_at),
286+
capacity: parseInt(r.capacity, 10),
287+
is_open: r.is_open?.toLowerCase() === "true",
288+
}));
289+
290+
await db.insert(permanenceSchema).values(parsedData);
291+
};

backend/src/utils/siep.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ export const getTokenUTTAPI = async() => {
1818
}
1919
}
2020

21-
export const getNewStudentsFromUTTAPI = async (token: string) => {
21+
export const getNewStudentsFromUTTAPI = async (token: string, date : string) => {
2222
const allNewStudents: any[] = [];
2323
let currentPage = 1;
2424
let hasNextPage = true;
2525

2626
try {
2727
while (hasNextPage) {
28-
const response = await axios.get(`${api_utt_admis_url_ismajor}?page=${currentPage}`, {
28+
const response = await axios.get(api_utt_admis_url_ismajor+date+"?page="+currentPage, {
2929
headers: {
3030
Authorization: `Bearer ${token}`,
3131
},

frontend/src/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const App: React.FC = () => {
4040
<Route path="/admin/teams" element={<AdminRoute><AdminPageTeam /></AdminRoute>} />
4141
<Route path="/admin/factions" element={<AdminRoute><AdminPageFaction /></AdminRoute>} />
4242
<Route path="/admin/shotgun" element={<AdminRoute><AdminPageShotgun /></AdminRoute>} />
43-
<Route path="/admin/export" element={<AdminRoute><AdminPageExport /></AdminRoute>} />
43+
<Route path="/admin/export-import" element={<AdminRoute><AdminPageExport /></AdminRoute>} />
4444
<Route path="/admin/permanences" element={<AdminRoute><AdminPagePerm /></AdminRoute>} />
4545
<Route path="/admin/challenge" element={<AdminRoute><AdminPageChall /></AdminRoute>} />
4646
<Route path="/admin/email" element={<AdminRoute><AdminPageEmail /></AdminRoute>} />

frontend/src/components/Admin/adminExport.tsx

-48
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useState } from "react";
2+
import { Button } from "../ui/button";
3+
import { exportDb } from "../../services/requests/export.service";
4+
import { importPermanenceCSV } from "../../services/requests/permanence.service";
5+
6+
export const AdminExportConnect = () => {
7+
const [loading, setLoading] = useState(false);
8+
const [error, setError] = useState<string | null>(null);
9+
const [message, setMessage] = useState<string>("");
10+
11+
const handleExport = async () => {
12+
setLoading(true);
13+
try {
14+
const response = await exportDb();
15+
setMessage(response.message);
16+
} catch (error) {
17+
console.error("Erreur de connexion à Google", error);
18+
setError("Erreur lors de la tentative de connexion.");
19+
} finally {
20+
setLoading(false);
21+
}
22+
};
23+
24+
return (
25+
<div className="max-w-xl mx-auto p-8 bg-white rounded-2xl shadow-xl">
26+
<h2 className="text-3xl font-bold text-center text-gray-900 mb-6">
27+
Exporter vers Google Sheets
28+
</h2>
29+
30+
<div className="flex justify-center mb-4">
31+
<Button
32+
onClick={handleExport}
33+
disabled={loading}
34+
className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-2 px-6 rounded-xl shadow-md transition-all duration-200"
35+
>
36+
{loading ? "Chargement..." : "Exporter les données"}
37+
</Button>
38+
</div>
39+
40+
{error && (
41+
<p className="text-center text-sm text-red-500 font-medium">{error}</p>
42+
)}
43+
{message && (
44+
<p className="text-center text-sm text-green-600 font-medium">
45+
{message}
46+
</p>
47+
)}
48+
</div>
49+
);
50+
};
51+
52+
export const ImportPermCSV = () => {
53+
const [file, setFile] = useState<File | null>(null);
54+
const [message, setMessage] = useState<string>("");
55+
56+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
57+
const selectedFile = e.target.files?.[0];
58+
if (selectedFile) {
59+
setFile(selectedFile);
60+
}
61+
};
62+
63+
const handleFileUpload = async () => {
64+
if (!file) {
65+
setMessage("Veuillez sélectionner un fichier CSV.");
66+
return;
67+
}
68+
69+
const formData = new FormData();
70+
formData.append("file", file);
71+
72+
try {
73+
const response = await importPermanenceCSV(formData);
74+
setMessage(response.message);
75+
} catch (error) {
76+
console.error(error);
77+
setMessage("Erreur lors de l'import du fichier CSV.");
78+
}
79+
};
80+
81+
return (
82+
<div className="max-w-3xl mx-auto p-8 bg-white rounded-2xl shadow-xl mt-12 space-y-6">
83+
<h2 className="text-3xl font-bold text-gray-900 text-center">
84+
Importer un fichier CSV pour les permanences
85+
</h2>
86+
87+
<p className="text-center text-gray-500 text-sm">
88+
Uploadez un fichier CSV contenant les permanences à importer.
89+
</p>
90+
91+
<div className="flex flex-col items-center gap-4">
92+
<input
93+
type="file"
94+
accept=".csv"
95+
onChange={handleFileChange}
96+
className="file-input w-full max-w-md text-sm file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-blue-600 file:text-white hover:file:bg-blue-700"
97+
/>
98+
99+
<Button
100+
onClick={handleFileUpload}
101+
className="w-full max-w-md bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white py-2.5 rounded-xl font-semibold transition shadow-md"
102+
>
103+
📥 Importer le fichier
104+
</Button>
105+
</div>
106+
107+
{message && (
108+
<p
109+
className={`text-center text-sm font-medium ${
110+
message.toLowerCase().includes("succès")
111+
? "text-green-600"
112+
: "text-red-500"
113+
}`}
114+
>
115+
{message}
116+
</p>
117+
)}
118+
119+
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6 text-sm text-gray-700">
120+
<h3 className="font-semibold text-gray-800 mb-2 text-lg">
121+
📄 Exemple de fichier CSV :
122+
</h3>
123+
<pre className="bg-white p-4 rounded-lg border text-xs overflow-x-auto">
124+
{`name,description,location,start_at,end_at,capacity,is_open
125+
Permanence 1,Accueil matin,A001,2025-05-01T08:00,2025-05-01T10:00,10,false
126+
Permanence 2,Accueil après-midi,A002,2025-05-02T14:00,2025-05-02T16:00,15,false`}
127+
</pre>
128+
<p className="mt-4 text-xs text-gray-500">
129+
Le fichier doit être encodé en UTF-8 et utiliser une virgule comme séparateur. Les dates doivent être au format
130+
<code className="ml-1 font-mono bg-gray-200 px-1 rounded">
131+
aaaa-mm-jjThh:mm
132+
</code>.
133+
</p>
134+
</div>
135+
</div>
136+
);
137+
};

0 commit comments

Comments
 (0)