results = res.result();
+ JsonArray jsonArray = new JsonArray();
+
+ results.forEach(row -> {
+ JsonObject json = new JsonObject()
+ .put("id", row.getInteger("id"))
+ .put("object_id", row.getInteger("object_id"))
+ .put("requested_by", row.getInteger("requested_by"))
+ .put("request_date", row.getLocalDateTime("request_date").toString()); // Assurez-vous que le type correspond
+ jsonArray.add(json);
+ });
+
+ context.response()
+ .putHeader("Content-Type", "application/json")
+ .end(jsonArray.encode());
+ } else {
+ res.cause().printStackTrace();
+ context.response().setStatusCode(500).end(new JsonObject().put("error", "Erreur lors de la récupération des demandes").encode());
+ }
+ });
+ }
+}
diff --git a/z1/Back-end/src/main/java/com/example/starter/SetUser.java b/z1/Back-end/src/main/java/com/example/starter/SetUser.java
new file mode 100644
index 0000000..47aa996
--- /dev/null
+++ b/z1/Back-end/src/main/java/com/example/starter/SetUser.java
@@ -0,0 +1,196 @@
+package com.example.starter;
+
+import at.favre.lib.crypto.bcrypt.BCrypt;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.sqlclient.Tuple;
+
+public class SetUser {
+ private DatabaseService databaseService;
+
+ public SetUser(DatabaseService ddbs) {
+ this.databaseService = ddbs;
+ }
+
+ public void updateUserPoints(Integer userId, Integer points) {
+ databaseService.pool
+ .preparedQuery("UPDATE users SET points=points+? WHERE id=?")
+ .execute(Tuple.of(points, userId))
+ .onFailure(e -> {
+ System.err.println("Erreur de mise à jour des points :" + e.getMessage());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() > 0) {
+ System.out.println("Points de l'utilisateur mis à jour avec succès");
+ } else {
+ System.out.println("Utilisateur non trouvé pour la mise à jour des points");
+ }
+ });
+ }
+
+ public void changeUserPassword(RoutingContext context) {
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Corps de la requête manquant").encode());
+ return;
+ }
+ Integer id = body.getInteger("id");
+ String oldPassword = body.getString("oldPassword");
+ String newPassword = body.getString("newPassword");
+
+ databaseService.pool
+ .preparedQuery("SELECT password FROM users WHERE id=?")
+ .execute(Tuple.of(id))
+ .onFailure(e -> {
+ System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject().put("error", "Erreur de récupération de la BDD").encode());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() == 0) {
+ context.response()
+ .setStatusCode(404)
+ .end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
+ return;
+ }
+
+ String currentPassword = rows.iterator().next().getString("password");
+ BCrypt.Result verification = BCrypt.verifyer().verify(oldPassword.toCharArray(), currentPassword);
+
+ if (!verification.verified) {
+ context.response()
+ .setStatusCode(401)
+ .end(new JsonObject().put("error", "Ancien mot de passe incorrect").encode());
+ return;
+ }
+ String hashedPassword = BCrypt.withDefaults().hashToString(12, newPassword.toCharArray());
+
+ databaseService.pool
+ .preparedQuery("UPDATE users SET password=? WHERE id=?")
+ .execute(Tuple.of(hashedPassword, id))
+ .onFailure(e -> {
+ System.err.println("Erreur lors de la mise à jour du mot de passe :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject()
+ .put("error", "Erreur lors de la mise à jour du mot de passe")
+ .encode());
+ })
+ .onSuccess(updateRows -> {
+ context.response()
+ .putHeader("content-type", "application/json: charset=UTF-8")
+ .end(new JsonObject().put("success", "Le mot de passe a bien été mis à jour")
+ .encode());
+ });
+ });
+ }
+
+ public void updateUserProfile(RoutingContext context) {
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Corps de la requête manquant").encode());
+ return;
+ }
+ Integer id = body.getInteger("id");
+ String name = body.getString("name");
+ String surname = body.getString("surname");
+ String pseudo = body.getString("pseudo");
+
+ databaseService.pool
+ .preparedQuery("UPDATE users SET name=?, surname=?, pseudo=? WHERE id=?")
+ .execute(Tuple.of(name, surname,pseudo, id))
+ .onFailure(e -> {
+ System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject().put("error", "Erreur de récupération de la BDD").encode());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() == 0) {
+ context.response()
+ .setStatusCode(404)
+ .end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
+ return;
+ }
+ context.response()
+ .putHeader("content-type", "application/json: charset=UTF-8")
+ .end(new JsonObject()
+ .put("success", "Les informations de l'utilisateur ont bien été mises à jour")
+ .encode());
+ return;
+ });
+ }
+
+ public void setUserPoints(RoutingContext context) {
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Corps de la requête manquant").encode());
+ return;
+ }
+ Integer id = body.getInteger("id");
+ Integer points = body.getInteger("points");
+ databaseService.pool
+ .preparedQuery(
+ "UPDATE users SET points=? WHERE id=?")
+ .execute(Tuple.of(points, id))
+ .onFailure(e -> {
+ System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() == 0) {
+ context.response()
+ .setStatusCode(404)
+ .end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
+ return;
+ }
+ context.response()
+ .putHeader("content-type", "application/json: charset=UTF-8")
+ .end(new JsonObject().put("success", "Les points de l'utilisateur ont bien été mis à jour")
+ .encode());
+ return;
+ });
+ }
+
+ public void deleteUser(RoutingContext context) {
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Corps de la requête manquant").encode());
+ return;
+ }
+ Integer id = body.getInteger("id");
+ databaseService.pool
+ .preparedQuery("DELETE FROM users WHERE id=?")
+ .execute(Tuple.of(id))
+ .onFailure(e -> {
+ System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject().put("error", "Erreur de récupération de la BDD").encode());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() == 0) {
+ context.response()
+ .setStatusCode(404)
+ .end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
+ return;
+ }
+ context.response()
+ .putHeader("content-type", "application/json: charset=UTF-8")
+ .end(new JsonObject().put("success", "L'utilisateur à bien été supprimé").encode());
+ return;
+ });
+
+ }
+}
diff --git a/z1/Back-end/src/main/java/com/example/starter/SetWeatherData.java b/z1/Back-end/src/main/java/com/example/starter/SetWeatherData.java
new file mode 100644
index 0000000..233611f
--- /dev/null
+++ b/z1/Back-end/src/main/java/com/example/starter/SetWeatherData.java
@@ -0,0 +1,72 @@
+package com.example.starter;
+
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.sqlclient.Tuple;
+
+public class SetWeatherData {
+ private DatabaseService databaseService;
+ private SetUser setUser;
+
+
+ public SetWeatherData(DatabaseService ddbs) {
+ this.databaseService = ddbs;
+ }
+ public void setUserHandler(SetUser setUser) {
+ this.setUser = setUser;
+ }
+ public void setRangeData(RoutingContext context) {
+ JsonObject body = context.body().asJsonObject();
+ if (body == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Corps de la requête manquant").encode());
+ return;
+ }
+ String id = body.getString("id");
+ Double min = body.getDouble("min");
+ Double max = body.getDouble("max");
+ String type = body.getString("type");
+
+ if (id == null || min == null || max == null || type == null) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Paramètres manquants ou invalides").encode());
+ return;
+ }
+ if (!type.matches("^[a-zA-Z_]+$")) {
+ context.response()
+ .setStatusCode(400)
+ .end(new JsonObject().put("error", "Type invalide").encode());
+ return;
+ }
+ String query = String.format("UPDATE range_data SET %s_min=?, %s_max=? WHERE station_id=?", type, type);
+ Integer idUser = body.getInteger("idUser");
+ databaseService.pool
+ .preparedQuery(
+ query)
+ .execute(Tuple.of(min, max, Integer.parseInt(id)))
+ .onFailure(e -> {
+ System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
+ context.response()
+ .setStatusCode(500)
+ .end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
+ })
+ .onSuccess(rows -> {
+ if (rows.rowCount() == 0) {
+ context.response()
+ .setStatusCode(404)
+ .end(new JsonObject().put("error", "Objet non trouvé").encode());
+ return;
+ }
+ if (idUser != null) {
+ setUser.updateUserPoints(idUser, 1);
+ }
+ context.response()
+ .putHeader("content-type", "application/json: charset=UTF-8")
+ .end(new JsonObject().put("success", "Les limites ont bien été mis à jour").encode());
+ return;
+ });
+ }
+
+}
diff --git a/z1/Back-end/src/test/java/com/example/starter/TestMainVerticle.java b/z1/Back-end/src/test/java/com/example/starter/TestMainVerticle.java
new file mode 100644
index 0000000..333630b
--- /dev/null
+++ b/z1/Back-end/src/test/java/com/example/starter/TestMainVerticle.java
@@ -0,0 +1,22 @@
+package com.example.starter;
+
+import io.vertx.core.Vertx;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(VertxExtension.class)
+public class TestMainVerticle {
+
+ @BeforeEach
+ void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
+ vertx.deployVerticle(new MainVerticle()).onComplete(testContext.succeeding(id -> testContext.completeNow()));
+ }
+
+ @Test
+ void verticle_deployed(Vertx vertx, VertxTestContext testContext) throws Throwable {
+ testContext.completeNow();
+ }
+}
diff --git a/z1/Front-end/.gitignore b/z1/Front-end/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/z1/Front-end/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/z1/Front-end/Dockerfile b/z1/Front-end/Dockerfile
new file mode 100644
index 0000000..d741d86
--- /dev/null
+++ b/z1/Front-end/Dockerfile
@@ -0,0 +1,25 @@
+# Stage 1: Build the React application
+FROM node:18-alpine AS build
+WORKDIR /app
+
+COPY package.json package-lock.json* ./
+RUN npm install
+
+COPY . .
+RUN npm run build
+
+# Stage 2: Serve the application using Nginx
+FROM nginx:alpine
+
+# Remove default Nginx static assets
+RUN rm -rf /usr/share/nginx/html/*
+
+# Copy built app from the first stage
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# Replace the default Nginx configuration
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/z1/Front-end/eslint.config.js b/z1/Front-end/eslint.config.js
new file mode 100644
index 0000000..5cc400d
--- /dev/null
+++ b/z1/Front-end/eslint.config.js
@@ -0,0 +1,27 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+
+export default [
+ { ignores: ['dist'] },
+ js.configs.recommended,
+ {
+ files: ['**/*.{js,jsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ }
+];
\ No newline at end of file
diff --git a/z1/Front-end/index.html b/z1/Front-end/index.html
new file mode 100644
index 0000000..9b72c45
--- /dev/null
+++ b/z1/Front-end/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Projet Dev Web
+
+
+
+
+
+
\ No newline at end of file
diff --git a/z1/Front-end/nginx.conf b/z1/Front-end/nginx.conf
new file mode 100644
index 0000000..34f1760
--- /dev/null
+++ b/z1/Front-end/nginx.conf
@@ -0,0 +1,12 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+
+ index index.html;
+
+ # Fallback to index.html for Single Page Applications handling client-side routing
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/z1/Front-end/package.json b/z1/Front-end/package.json
new file mode 100644
index 0000000..6c314e3
--- /dev/null
+++ b/z1/Front-end/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "vite-react-javascript-starter",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/material": "^7.0.1",
+ "axios": "^1.8.4",
+ "i18next": "^26.0.1",
+ "i18next-browser-languagedetector": "^8.2.1",
+ "jwt-decode": "^4.0.0",
+ "lucide-react": "^0.427.0",
+ "react": "^18.3.1",
+ "react-charts": "^3.0.0-beta.57",
+ "react-circle-progress-bar": "^0.1.4",
+ "react-dom": "^18.3.1",
+ "react-i18next": "^17.0.1",
+ "react-router-dom": "^7.4.0",
+ "recharts": "^2.15.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.9.1",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.18",
+ "eslint": "^9.23.0",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.11",
+ "globals": "^15.9.0",
+ "postcss": "^8.4.35",
+ "tailwindcss": "^3.4.17",
+ "vite": "^5.4.2"
+ },
+ "description": "Bienvenue dans le projet **DevWeb** ! Ce projet utilise **Vite** et **React** pour créer une application web moderne et performante.",
+ "main": "eslint.config.js",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Charles40130/Projet-Dev-Web-Ing1.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/Charles40130/Projet-Dev-Web-Ing1/issues"
+ },
+ "homepage": "https://github.com/Charles40130/Projet-Dev-Web-Ing1#readme"
+}
diff --git a/z1/Front-end/postcss.config.js b/z1/Front-end/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/z1/Front-end/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/z1/Front-end/public/img/Est.png b/z1/Front-end/public/img/Est.png
new file mode 100644
index 0000000..96a8101
Binary files /dev/null and b/z1/Front-end/public/img/Est.png differ
diff --git a/z1/Front-end/public/img/Nord-Est.png b/z1/Front-end/public/img/Nord-Est.png
new file mode 100644
index 0000000..108fc9a
Binary files /dev/null and b/z1/Front-end/public/img/Nord-Est.png differ
diff --git a/z1/Front-end/public/img/Nord-Ouest.png b/z1/Front-end/public/img/Nord-Ouest.png
new file mode 100644
index 0000000..6d58631
Binary files /dev/null and b/z1/Front-end/public/img/Nord-Ouest.png differ
diff --git a/z1/Front-end/public/img/Nord.png b/z1/Front-end/public/img/Nord.png
new file mode 100644
index 0000000..8c47efa
Binary files /dev/null and b/z1/Front-end/public/img/Nord.png differ
diff --git a/z1/Front-end/public/img/NotreMission.png b/z1/Front-end/public/img/NotreMission.png
new file mode 100644
index 0000000..f51ebc9
Binary files /dev/null and b/z1/Front-end/public/img/NotreMission.png differ
diff --git a/z1/Front-end/public/img/Ouest.png b/z1/Front-end/public/img/Ouest.png
new file mode 100644
index 0000000..7b544a9
Binary files /dev/null and b/z1/Front-end/public/img/Ouest.png differ
diff --git a/z1/Front-end/public/img/Sud-Est.png b/z1/Front-end/public/img/Sud-Est.png
new file mode 100644
index 0000000..39d0632
Binary files /dev/null and b/z1/Front-end/public/img/Sud-Est.png differ
diff --git a/z1/Front-end/public/img/Sud-Ouest.png b/z1/Front-end/public/img/Sud-Ouest.png
new file mode 100644
index 0000000..2840f64
Binary files /dev/null and b/z1/Front-end/public/img/Sud-Ouest.png differ
diff --git a/z1/Front-end/public/img/Sud.png b/z1/Front-end/public/img/Sud.png
new file mode 100644
index 0000000..d3e5bd7
Binary files /dev/null and b/z1/Front-end/public/img/Sud.png differ
diff --git a/z1/Front-end/public/img/cloud-sun-rain.svg b/z1/Front-end/public/img/cloud-sun-rain.svg
new file mode 100644
index 0000000..9892ac6
--- /dev/null
+++ b/z1/Front-end/public/img/cloud-sun-rain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/z1/Front-end/public/img/fr-alert.webp b/z1/Front-end/public/img/fr-alert.webp
new file mode 100644
index 0000000..d1ab54b
Binary files /dev/null and b/z1/Front-end/public/img/fr-alert.webp differ
diff --git a/z1/Front-end/public/img/gestioniot.png b/z1/Front-end/public/img/gestioniot.png
new file mode 100644
index 0000000..d2661ab
Binary files /dev/null and b/z1/Front-end/public/img/gestioniot.png differ
diff --git a/z1/Front-end/public/img/iotmeteo.jpg b/z1/Front-end/public/img/iotmeteo.jpg
new file mode 100644
index 0000000..19a2683
Binary files /dev/null and b/z1/Front-end/public/img/iotmeteo.jpg differ
diff --git a/z1/Front-end/public/img/precisionfiable.jpg b/z1/Front-end/public/img/precisionfiable.jpg
new file mode 100644
index 0000000..b861940
Binary files /dev/null and b/z1/Front-end/public/img/precisionfiable.jpg differ
diff --git a/z1/Front-end/public/img/snow.jpg b/z1/Front-end/public/img/snow.jpg
new file mode 100644
index 0000000..a9e9abd
Binary files /dev/null and b/z1/Front-end/public/img/snow.jpg differ
diff --git a/z1/Front-end/public/img/surveillancemeteo.webp b/z1/Front-end/public/img/surveillancemeteo.webp
new file mode 100644
index 0000000..976fac5
Binary files /dev/null and b/z1/Front-end/public/img/surveillancemeteo.webp differ
diff --git a/z1/Front-end/public/img/surveillancetempsreel.jpg b/z1/Front-end/public/img/surveillancetempsreel.jpg
new file mode 100644
index 0000000..07405a2
Binary files /dev/null and b/z1/Front-end/public/img/surveillancetempsreel.jpg differ
diff --git a/z1/Front-end/src/App.jsx b/z1/Front-end/src/App.jsx
new file mode 100644
index 0000000..fbf874c
--- /dev/null
+++ b/z1/Front-end/src/App.jsx
@@ -0,0 +1,54 @@
+import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
+import { AuthProvider } from "./AuthContext.jsx";
+import Home from "./pages/Home.jsx";
+import About from "./pages/About.jsx";
+import Gestion from "./pages/Gestion/Gestion.jsx";
+import Header from "./components/Header.jsx";
+import ObjectManagement from "./pages/Gestion/ObjectManagement.jsx";
+import Objet from "./pages/Gestion/Objet.jsx";
+import AddObject from "./pages/Gestion/AddObject.jsx";
+import Signup from "./pages/Signup.jsx";
+import Login from "./pages/Login.jsx";
+import Profil from "./pages/Profil.jsx";
+import Sidebar from "./pages/Admin/sidebar.jsx";
+import User from "./pages/Admin/User.jsx";
+import Dashboard from "./pages/Admin/Dashboard.jsx";
+import AdminObjet from "./pages/Admin/AdminObjet.jsx";
+import ProtectedRoute from "./ProtectedRoute.jsx";
+
+function App() {
+ return (
+
+
+
+
+
+ {/* Routes publiques */}
+ } />
+ } />
+ } />
+ } />
+
+ {/* Routes protégées pour tous les utilisateurs connectés */}
+ } allowedRoles={['admin', 'complexe', 'user']} />} />
+ } allowedRoles={['admin', 'complexe', 'user']} />} />
+ } allowedRoles={['admin', 'complexe', 'user']} />} />
+
+ {/* Routes protégées pour les admins et complexes */}
+ } allowedRoles={['admin', 'complexe']} />} />
+ } allowedRoles={['admin', 'complexe','user']} />} />
+
+ {/* Routes protégées pour tous les utilisateurs connectés */}
+ } allowedRoles={['admin', 'complexe', 'user']} />} />
+ } allowedRoles={['admin', 'complexe', 'user']} />} />
+ {/* Routes protégées pour les admins uniquement */}
+ } allowedRoles={['admin']} />} />
+ } allowedRoles={['admin']} />} />
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/z1/Front-end/src/AuthContext.jsx b/z1/Front-end/src/AuthContext.jsx
new file mode 100644
index 0000000..545e406
--- /dev/null
+++ b/z1/Front-end/src/AuthContext.jsx
@@ -0,0 +1,47 @@
+import React, { createContext, useContext, useState, useEffect } from "react";
+import { jwtDecode } from "jwt-decode";
+
+
+// Créer le contexte
+const AuthContext = createContext();
+
+// Hook pour accéder facilement au contexte
+export const useAuth = () => useContext(AuthContext);
+
+// Fournisseur de contexte qui gère l'état du token et de l'utilisateur
+export const AuthProvider = ({ children }) => {
+ const [token, setToken] = useState(localStorage.getItem("token"));
+ const [user, setUser] = useState(null);
+
+ // Met à jour le token et décode l'utilisateur
+ useEffect(() => {
+ if (token) {
+ try {
+ const decoded = jwtDecode(token);
+ setUser(decoded);
+ } catch (error) {
+ console.error("Erreur lors du décodage du token:", error);
+ setUser(null);
+ }
+ } else {
+ setUser(null);
+ }
+ }, [token]);
+
+ const login = (newToken) => {
+ localStorage.setItem("token", newToken);
+ setToken(newToken);
+ };
+
+ const logout = () => {
+ localStorage.removeItem("token");
+ setToken(null);
+ setUser(null);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/z1/Front-end/src/ProtectedRoute.jsx b/z1/Front-end/src/ProtectedRoute.jsx
new file mode 100644
index 0000000..b329341
--- /dev/null
+++ b/z1/Front-end/src/ProtectedRoute.jsx
@@ -0,0 +1,20 @@
+import React from "react";
+import { Navigate } from "react-router-dom";
+import { useAuth } from "./AuthContext"; // Utilisation du contexte d'authentification
+
+function ProtectedRoute({ element, allowedRoles }) {
+ const { token, user } = useAuth(); // Vérifier si un token existe, donc si l'utilisateur est authentifié
+
+ // Si l'utilisateur n'est pas authentifié, redirigez-le vers la page de login
+ if (!token) {
+ return ;
+ }
+ if(user){
+ if (allowedRoles && !allowedRoles.includes(user?.role)) {
+ return ;
+ }
+ return element;
+ }
+}
+
+export default ProtectedRoute;
diff --git a/z1/Front-end/src/components/Alert.jsx b/z1/Front-end/src/components/Alert.jsx
new file mode 100644
index 0000000..615ca9b
--- /dev/null
+++ b/z1/Front-end/src/components/Alert.jsx
@@ -0,0 +1,33 @@
+import React, { useEffect } from "react";
+import { Check, X } from "lucide-react";
+import { useAuth } from "../AuthContext";
+
+function Alert({ affAlert, setAffAlert, message }) {
+ const { user } = useAuth();
+
+ useEffect(() => {
+ if (affAlert) {
+ const timer = setTimeout(() => {
+ setAffAlert(false);
+ }, 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [affAlert, setAffAlert]);
+
+ return (
+ affAlert && user?.role !== "user" && (
+
+
+
{message}
+
+
+ )
+ );
+}
+
+export default Alert;
diff --git a/z1/Front-end/src/components/AlertInactive.jsx b/z1/Front-end/src/components/AlertInactive.jsx
new file mode 100644
index 0000000..37e75bd
--- /dev/null
+++ b/z1/Front-end/src/components/AlertInactive.jsx
@@ -0,0 +1,23 @@
+import React, {useState} from "react";
+import { TriangleAlert,X } from "lucide-react";
+import { useAuth } from "../AuthContext";
+
+function AlertInactive({affAlert,setAffAlert, message}) {
+ const { user } = useAuth();
+ return (
+ (affAlert&&(user?.role!=="user")&&(
+
+
+
+
+
+
+ {message}
+
+
+ )));
+}
+
+export default AlertInactive;
\ No newline at end of file
diff --git a/z1/Front-end/src/components/BatterieInfo.jsx b/z1/Front-end/src/components/BatterieInfo.jsx
new file mode 100644
index 0000000..4a0b2b9
--- /dev/null
+++ b/z1/Front-end/src/components/BatterieInfo.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { Battery } from "lucide-react";
+import Progress from "react-circle-progress-bar";
+import { useTranslation } from "react-i18next";
+
+function BatterieInfo({ object }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+ {t('components.batterieInfo.title')}
+
+
+
+
+
+ {t('components.batterieInfo.batteryType')}{" "}
+ {object.type_batterie}
+
+
+
+ );
+}
+
+export default BatterieInfo;
diff --git a/z1/Front-end/src/components/BoutonGraphique.jsx b/z1/Front-end/src/components/BoutonGraphique.jsx
new file mode 100644
index 0000000..04c7599
--- /dev/null
+++ b/z1/Front-end/src/components/BoutonGraphique.jsx
@@ -0,0 +1,25 @@
+import React from "react";
+import { ChartLine } from "lucide-react";
+
+function BoutonGraphique({ type, setGraphStates, graphStates, graphCible }) {
+ const handleClick = () => {
+ setGraphStates((prev) => ({ ...prev, [type]: !prev[type] }));
+ };
+ return !graphStates[type] ? (
+
+ ) : (
+
+ );
+}
+
+export default BoutonGraphique;
diff --git a/z1/Front-end/src/components/FormNewObject.jsx b/z1/Front-end/src/components/FormNewObject.jsx
new file mode 100644
index 0000000..fa62272
--- /dev/null
+++ b/z1/Front-end/src/components/FormNewObject.jsx
@@ -0,0 +1,280 @@
+import React, { useEffect, useState } from "react";
+import { BadgePlus } from "lucide-react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import { useAuth } from "../AuthContext";
+import { useTranslation } from "react-i18next";
+
+function FormNewObject({ isAdmin }) {
+ const { t } = useTranslation();
+ const { user } = useAuth();
+ const [categorie, setCategorie] = useState();
+ const [description, setDescription] = useState("");
+ const [type, setType] = useState("");
+ const [location, setLocalisation] = useState("");
+ const [proprio_id, setProprio_id] = useState(user?.id);
+ const [batterieType, setBatterieType] = useState("");
+ const [status, setStatus] = useState("active");
+ const [nom, setNom] = useState("");
+ const [Response, setResponse] = useState(null);
+ const [isActive, setActive] = useState(true);
+ const [verif, setVerif] = useState(false);
+ const [enregistre, setEnregistre] = useState(false);
+ const [messRequete, setMessRequete] = useState("");
+ function handleSubmit(event) {
+ event.preventDefault();
+
+ if (verif) {
+ console.log("Envoi requete");
+ axios
+ .post(`${API_BASE_URL}/addObject`, {
+ nom,
+ description,
+ type,
+ location,
+ status,
+ batterieType,
+ proprio_id,
+ })
+ .then((response) => {
+ setMessRequete(t('components.formNewObject.successRecord'));
+ setEnregistre(true);
+ console.log("Ajout de l'objet réussit :", response.data);
+ })
+ .catch((error) => {
+ setMessRequete(t('components.formNewObject.errorRecord'));
+ console.error("Erreur lors de l'ajout de l'objet :", error);
+ });
+ setVerif(false);
+ resetForm();
+ } else {
+ setVerif(true);
+ }
+ }
+ useEffect(() => {
+ axios
+ .get(`${API_BASE_URL}/getCategories`)
+ .then((response) => {
+ if (response.data.length === 0) {
+ console.warn(t('components.formNewObject.noCategory'));
+ } else {
+ setCategorie(response.data);
+ }
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération des catégories :", error);
+ });
+ }, []);
+ function resetForm() {
+ setNom("");
+ setStatus("active");
+ setDescription("");
+ setType("");
+ setLocalisation("");
+ setBatterieType("");
+ if (isAdmin) set_id("");
+ setActive(true);
+ }
+ function handleCancel() {
+ if (verif) {
+ setVerif(false);
+ } else {
+ resetForm();
+ }
+ }
+ function handleStatusChange() {
+ setActive((prevIsActive) => {
+ const newIsActive = !prevIsActive;
+ setStatus(newIsActive ? "active" : "inactive");
+ return newIsActive;
+ });
+ }
+
+ return (
+
+ );
+}
+
+export default FormNewObject;
diff --git a/z1/Front-end/src/components/Header.jsx b/z1/Front-end/src/components/Header.jsx
new file mode 100644
index 0000000..57baa7f
--- /dev/null
+++ b/z1/Front-end/src/components/Header.jsx
@@ -0,0 +1,230 @@
+import React, { useState, useEffect } from "react";
+import { X, Menu, LogIn, UserPlus, LogOut, User } from "lucide-react";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { useAuth } from "../AuthContext";
+import LanguageSwitcher from "./LanguageSwitcher";
+
+function Header() {
+ const { t } = useTranslation();
+ const { token, user, logout } = useAuth();
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [showAdminDropdown, setShowAdminDropdown] = useState(false);
+
+ const toggleAdminDropdown = () => {
+ setShowAdminDropdown((prev) => !prev);
+ };
+
+ return (
+
+ );
+}
+
+export default Header;
diff --git a/z1/Front-end/src/components/InfoObject.jsx b/z1/Front-end/src/components/InfoObject.jsx
new file mode 100644
index 0000000..814e992
--- /dev/null
+++ b/z1/Front-end/src/components/InfoObject.jsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { Info } from "lucide-react";
+import { useAuth } from "../AuthContext";
+import { useTranslation } from "react-i18next";
+
+function InfoObject({ object,defafficherModif }) {
+ const { t } = useTranslation();
+ const {user} = useAuth();
+ return (
+
+
+
+
+
+
{t('components.infoObject.title')}
+
+
+
{t('components.infoObject.description')}
+
{object.description}
+
+
+
+
{t('components.infoObject.type')}
+
{object.type}
+
+
+
+
{t('components.infoObject.location')}
+
{object.location}
+
+
+
+
{t('components.infoObject.status')}
+
{object.status}
+
+
+
+
+ {t('components.infoObject.lastUpdate')}
+
+
{object.last_update}
+
+ {user?.role!=="user"&&(
+
+ )}
+
+ );
+}
+
+export default InfoObject;
diff --git a/z1/Front-end/src/components/LanguageSwitcher.jsx b/z1/Front-end/src/components/LanguageSwitcher.jsx
new file mode 100644
index 0000000..47adc33
--- /dev/null
+++ b/z1/Front-end/src/components/LanguageSwitcher.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+export default function LanguageSwitcher() {
+ const { i18n } = useTranslation();
+
+ const changeLanguage = (lng) => {
+ i18n.changeLanguage(lng);
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/z1/Front-end/src/components/MeteoGraph.jsx b/z1/Front-end/src/components/MeteoGraph.jsx
new file mode 100644
index 0000000..2bf6aa3
--- /dev/null
+++ b/z1/Front-end/src/components/MeteoGraph.jsx
@@ -0,0 +1,98 @@
+import React, { useEffect, useState } from "react";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ ReferenceLine,
+} from "recharts";
+
+import { Wind } from "lucide-react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import { useTranslation } from "react-i18next";
+
+function MeteoGraph({ object, categorie, Logo,reference}) {
+ const { t } = useTranslation();
+ const [rawData, setRawData] = useState([]);
+ const identifiant = object.id;
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/meteo?id=${identifiant}`).then((response) => {
+ setRawData(response.data);
+ });
+ }, [object]);
+ useEffect(() => {
+ if (reference?.current) {
+ reference.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [reference]);
+ function getAvg() {
+ let moyenne = 0;
+ rawData.forEach((element) => {
+ if(element){
+ moyenne += element[categorie];
+ }
+ });
+ return moyenne / rawData.length;
+ }
+ return (
+
+
+
+
+
+ {categorie === "temperature" ? (
+
+ {t('components.meteoGraph.historyTemp')}
+
+ ) : categorie === "humidity" ? (
+
+ {t('components.meteoGraph.historyHum')}
+
+ ) : (
+
+ {t('components.meteoGraph.historyPres')}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default MeteoGraph;
diff --git a/z1/Front-end/src/components/MeteoInfos.jsx b/z1/Front-end/src/components/MeteoInfos.jsx
new file mode 100644
index 0000000..1e77960
--- /dev/null
+++ b/z1/Front-end/src/components/MeteoInfos.jsx
@@ -0,0 +1,84 @@
+import { Thermometer, Sun, CircleGauge, Droplet } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import BoutonGraphique from "./BoutonGraphique";
+import AlertInactive from "./AlertInactive";
+import ParticularMeteo from "./ParticularMeteo";
+import { useTranslation } from "react-i18next";
+
+function MeteoInfos({ object, graphStates, setGraphStates, graphRefs }) {
+ const { t } = useTranslation();
+ const [rawData, setRawData] = useState([]);
+ const [AffAlert, setAffAlert] = useState(false);
+ const [AffRegles, setAffRegles] = useState(false);
+ const identifiant = object.id;
+
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/meteo?id=${identifiant}`).then((response) => {
+ setRawData(response.data);
+ if (rawData.length < 5) {
+ setAffAlert(true);
+ }
+ });
+ }, [object]);
+
+ const lastData = rawData.length > 0 ? rawData[rawData.length - 1] : null;
+ return (
+
+ {AffAlert && object.status === "active" && (
+
+ )}
+
+
+
+
+
{t('components.meteoInfos.currentWeather')}
+
+ {lastData ? (
+
+
+
+
+
+
+
+ {t('components.meteoInfos.lastRecord')} {lastData.timestamp}
+
+
+ ) : (
+
{t('components.meteoInfos.loading')}
+ )}
+
+ );
+}
+
+export default MeteoInfos;
diff --git a/z1/Front-end/src/components/ModifObject.jsx b/z1/Front-end/src/components/ModifObject.jsx
new file mode 100644
index 0000000..6cb1c40
--- /dev/null
+++ b/z1/Front-end/src/components/ModifObject.jsx
@@ -0,0 +1,164 @@
+import React, { useState } from "react";
+import { Info } from "lucide-react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import {useAuth} from "../AuthContext";
+import { useTranslation } from "react-i18next";
+
+function ModifObject({ object, defafficherModif }) {
+ const { t } = useTranslation();
+ const {user}=useAuth();
+ const [description, setDescription] = useState(object.description || "");
+ const [type, setType] = useState(object.type || "");
+ const [location, setLocalisation] = useState(object.location || "");
+ const [status, setStatus] = useState(object.status || "inactive");
+ const [isActive, setActive] = useState(object.status === "active");
+
+ function handleSubmit(event) {
+ event.preventDefault();
+ axios
+ .post(`${API_BASE_URL}/modifObjet`, {
+ id: object.id,
+ idUser:user.id,
+ description,
+ type,
+ location,
+ status,
+ shouldUpdatePoints:true
+ })
+ .then((response) => {
+ console.log("Modification réussie :", response.data);
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la modification :", error);
+ });
+ defafficherModif(false);
+ window.location.reload();
+ }
+
+ function handleCancel() {
+ defafficherModif(false);
+ }
+
+ function handleStatusChange() {
+ setActive((prevIsActive) => {
+ const newIsActive = !prevIsActive;
+ setStatus(newIsActive ? "active" : "inactive");
+ return newIsActive;
+ });
+ }
+
+ return (
+
+ );
+}
+
+export default ModifObject;
diff --git a/z1/Front-end/src/components/ParticularMeteo.jsx b/z1/Front-end/src/components/ParticularMeteo.jsx
new file mode 100644
index 0000000..7abb082
--- /dev/null
+++ b/z1/Front-end/src/components/ParticularMeteo.jsx
@@ -0,0 +1,170 @@
+import React, { useEffect, useState } from "react";
+import BoutonGraphique from "./BoutonGraphique";
+import { Bell } from "lucide-react";
+import Slider from "@mui/material/Slider";
+import { API_BASE_URL } from "../config";
+import axios from "axios";
+import { useAuth } from "../AuthContext";
+import { useTranslation } from "react-i18next";
+
+const identifiant = new URLSearchParams(window.location.search).get("id");
+function ParticularMeteo({
+ type,
+ data,
+ Icon,
+ texte1,
+ texte2,
+ graphStates,
+ setGraphStates,
+ graphRefs,
+}) {
+ const { t } = useTranslation();
+ const {user} = useAuth();
+ const [affRegles, setAffRegles] = useState(false);
+ const [rangeValue, setRangeValue] = useState([0, 0]);
+ const [alerteActive, setAlerteActive] = useState(false);
+ const minMaxValues = {
+ temperature: [-50, 60],
+ pressure: [940, 1060],
+ humidity: [10, 100],
+ };
+ const MIN = minMaxValues[type][0];
+ const MAX = minMaxValues[type][1];
+ const formatLabel = (value) => {
+ switch (type) {
+ case "temperature":
+ return `${value}°C`;
+ case "pressure":
+ return `${value} hPa`;
+ case "humidity":
+ return `${value}%`;
+ default:
+ return value;
+ }
+ };
+ const marks = [
+ {
+ value: MIN,
+ label: formatLabel(MIN),
+ },
+ {
+ value: MAX,
+ label: formatLabel(MAX),
+ },
+ ];
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/getRange?id=${identifiant}`).then((response) => {
+ setRangeValue([
+ response.data[0][type + "_min"],
+ response.data[0][type + "_max"],
+ ]);
+ });
+ }, [identifiant]);
+ const color =
+ rangeValue[0] > data[type] || rangeValue[1] < data[type]
+ ? "text-red-600"
+ : "text-indigo-600";
+
+ const defRangeData = () => {
+ console.log("Données envoyées :", {
+ id: identifiant,
+ min: rangeValue[0],
+ max: rangeValue[1],
+ type,
+ });
+ axios
+ .post(`${API_BASE_URL}/modifRangeData`, {
+ id: identifiant,
+ idUser:user.id,
+ min: parseFloat(rangeValue[0]),
+ max: parseFloat(rangeValue[1]),
+ type,
+ })
+ .then((response) => {
+ console.log("Modification réussie :", response.data);
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la modification :", error);
+ });
+ window.location.reload();
+ };
+
+ const handleChange = (event, newValue) => {
+ setRangeValue(newValue);
+ };
+ function valuetext(value) {
+ return `${value}°C`;
+ }
+ if (data[type]) {
+ return (
+
+
+
+
+
+
+
+
{texte1}
+
+ {Math.round(data[type])}{" "}
+ {texte2}
+
+
+
+
+ {user?.role!=="user" && (
+
+ )}
+
+
+
+ {affRegles && (
+
+
+ {t('components.particularMeteo.defineLimit')}
+
+
+ "Temperature range"}
+ value={rangeValue}
+ onChange={handleChange}
+ valueLabelDisplay="auto"
+ min={MIN}
+ max={MAX}
+ marks={marks}
+ getAriaValueText={valuetext}
+ disableSwap
+ />
+
+ {color=="text-red-600" &&(
+
{t('components.particularMeteo.outOfBounds')}
+ )}
+
+
+ )}
+
+ );
+ }
+}
+
+export default ParticularMeteo;
diff --git a/z1/Front-end/src/components/UserInfosObject.jsx b/z1/Front-end/src/components/UserInfosObject.jsx
new file mode 100644
index 0000000..25f4ede
--- /dev/null
+++ b/z1/Front-end/src/components/UserInfosObject.jsx
@@ -0,0 +1,51 @@
+import React,{useEffect, useState} from "react";
+import { User } from "lucide-react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import { useTranslation } from "react-i18next";
+
+function UserInfosObject({ user}) {
+ const { t } = useTranslation();
+ const [userInfo,setuserInfo]=useState({});
+ useEffect(()=>{
+ console.log(user);
+ axios
+ .post(`${API_BASE_URL}/publicUser`, {
+ id: user,
+ })
+ .then((response) => {
+ setuserInfo(response.data);
+ console.log("Modification réussie :", response.data);
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la modification :", error);
+ });
+ },[user]);
+
+ return (
+
+
+
+
+
+
{t('components.userInfosObject.title')}
+
+
+
{t('components.userInfosObject.pseudo')}
+
{userInfo.pseudo}
+
+
+
+
{t('components.userInfosObject.gender')}
+
{userInfo.gender}
+
+
+
+
{t('components.userInfosObject.points')}
+
{userInfo.points}
+
+
+ );
+}
+
+export default UserInfosObject;
diff --git a/z1/Front-end/src/components/WindGraph.jsx b/z1/Front-end/src/components/WindGraph.jsx
new file mode 100644
index 0000000..c0e86e5
--- /dev/null
+++ b/z1/Front-end/src/components/WindGraph.jsx
@@ -0,0 +1,69 @@
+import React, { useEffect, useState} from "react";
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
+
+import { Wind } from "lucide-react";
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import { useTranslation } from "react-i18next";
+
+function WindGraph({ object,reference }) {
+ const { t } = useTranslation();
+ const [rawData, setRawData] = useState([]);
+ const identifiant = object.id;
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/wind?id=${identifiant}`).then((response) => {
+ setRawData(response.data);
+ });
+ }, [object]);
+ const CustomTooltip = ({ payload, label, active }) => {
+ if (active && payload && payload.length) {
+ const { wind_speed, timestamp,wind_direction } = payload[0].payload;
+ return (
+
+
{t('components.windGraph.date')} {timestamp}
+
{t('components.windGraph.windSpeed')} {wind_speed} km/h
+
{t('components.windGraph.windDirection')} {wind_direction}
+
+ );
+ }
+
+ return null;
+ };
+ useEffect(() => {
+ if (reference?.current) {
+ reference.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [reference]);
+ return (
+
+
+
+
+
+
{t('components.windGraph.title')}
+
+
+
+
+
+
+ }/>
+
+
+
+
+
+ );
+}
+
+export default WindGraph;
diff --git a/z1/Front-end/src/components/WindInfo.jsx b/z1/Front-end/src/components/WindInfo.jsx
new file mode 100644
index 0000000..c4e0deb
--- /dev/null
+++ b/z1/Front-end/src/components/WindInfo.jsx
@@ -0,0 +1,76 @@
+import { Wind } from "lucide-react";
+import React, { useEffect, useState } from "react";
+
+import axios from "axios";
+import { API_BASE_URL } from "../config";
+import BoutonGraphique from "./BoutonGraphique";
+import { useTranslation } from "react-i18next";
+
+function WindInfo({ object, setGraphStates, graphStates, graphRefs, reference}) {
+ const { t } = useTranslation();
+ const [rawData, setRawData] = useState([]);
+ const identifiant = object.id;
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/wind?id=${identifiant}`).then((response) => {
+ setRawData(response.data);
+ });
+ }, [object]);
+ useEffect(() => {
+ if (reference?.current) {
+ reference.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [reference]);
+
+ const lastData = rawData.length > 0 ? rawData[rawData.length - 1] : null;
+
+ return (
+
+
+
+
+
+
{t('components.windInfo.currentWind')}
+
+ {lastData ? (
+
+

+
+ {lastData.wind_direction}
+
+
+
+
+
+
+
+
+
{t('components.windInfo.value')}
+
+ {lastData.wind_speed} Km/h
+
+
+
+
+
+
+
+ {t('components.windInfo.lastRecord')} {lastData.timestamp}
+
+
+ ) : (
+
{t('components.windInfo.loading')}
+ )}
+
+ );
+}
+
+export default WindInfo;
diff --git a/z1/Front-end/src/config.js b/z1/Front-end/src/config.js
new file mode 100644
index 0000000..5c2983f
--- /dev/null
+++ b/z1/Front-end/src/config.js
@@ -0,0 +1 @@
+export const API_BASE_URL = 'http://localhost:8888';
\ No newline at end of file
diff --git a/z1/Front-end/src/i18n.js b/z1/Front-end/src/i18n.js
new file mode 100644
index 0000000..06724a1
--- /dev/null
+++ b/z1/Front-end/src/i18n.js
@@ -0,0 +1,24 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+import frTranslation from './locales/fr.json';
+import enTranslation from './locales/en.json';
+
+const resources = {
+ fr: { translation: frTranslation },
+ en: { translation: enTranslation }
+};
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources,
+ fallbackLng: 'fr',
+ interpolation: {
+ escapeValue: false
+ }
+ });
+
+export default i18n;
diff --git a/z1/Front-end/src/index.css b/z1/Front-end/src/index.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/z1/Front-end/src/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/z1/Front-end/src/locales/en.json b/z1/Front-end/src/locales/en.json
new file mode 100644
index 0000000..dc21653
--- /dev/null
+++ b/z1/Front-end/src/locales/en.json
@@ -0,0 +1,363 @@
+{
+ "header": {
+ "home": "Home",
+ "about": "About",
+ "login": "Log in",
+ "signup": "Sign up",
+ "visualizer": "Visualizer",
+ "manage": "Manage",
+ "dashboard": "Dashboard",
+ "manageUsers": "Manage Users",
+ "manageObjects": "Manage Connected Objects",
+ "profile": "Profile",
+ "logout": "Log out",
+ "admin": "Admin"
+ },
+ "home": {
+ "title": "Real-time weather",
+ "subtitle": "Accurate and personalized forecasts to help you plan your day with peace of mind.",
+ "searchPlaceholder": "Search for a city ...",
+ "noCityFound": "No city found.",
+ "partlyCloudy": "Partly cloudy",
+ "feelsLike": "Feels like",
+ "humidity": "Humidity",
+ "wind": "Wind",
+ "rain": "Precipitation",
+ "sunrise": "Sunrise",
+ "sunset": "Sunset",
+ "hourlyForecast": "Hourly Forecast",
+ "dailyForecast": "5-day Forecast",
+ "weatherServices": "Weather Services",
+ "viewObjectsTitle": "Visualization of connected weather devices",
+ "viewObjectsDesc": "Browse all connected devices and their data throughout France",
+ "viewObjectsBtn": "View objects",
+ "signupTitle": "Sign up!",
+ "signupDesc": "Having an account on our site allows you to consult all connected weather objects throughout France.",
+ "signupBtn": "Sign up",
+ "loginTitle": "Log in!",
+ "loginDesc": "Happy to see you again! Log back into your account just as you left it!",
+ "loginBtn": "Log in",
+ "consultObjectsTitle": "Consult connected weather devices",
+ "consultObjectsDesc": "Access real-time data from connected weather devices, modify their parameters and check the history of measurements.",
+ "consultObjectsBtn": "Explore objects",
+ "addObjTitle": "Add a new connected device",
+ "addObjDesc": "Easily integrate a new connected device by entering its information and configuring its parameters for optimal management.",
+ "addObjBtn": "Add a device",
+ "adminDashTitle": "Access the Administration dashboard",
+ "adminDashDesc": "You will be able to manage the site's users as well as the connected devices",
+ "adminDashBtn": "Administration Dashboard",
+ "manageObjTitle": "Management of connected devices",
+ "manageObjDesc": "This module allows you to easily and effectively manage sensors and connected stations in France.",
+ "manageObjBtn": "Device Management",
+ "footerRights": "© 2025 VigiMétéo. All rights reserved."
+ },
+ "auth": {
+ "login": {
+ "title": "Log in",
+ "emailLabel": "Email:",
+ "passwordLabel": "Password:",
+ "submitButton": "Log in",
+ "noAccount": "Don't have an account?",
+ "signupLink": "Sign up here",
+ "loading": "Logging in...",
+ "success": "Login successful! Redirecting...",
+ "missingToken": "Authentication failed: missing token in response",
+ "incorrectAuth": "Incorrect email or password",
+ "invalidData": "Invalid form data",
+ "serverError": "Server error. Please try again later.",
+ "networkError": "Cannot reach the server. Check your internet connection.",
+ "genericError": "An error occurred. Please try again."
+ },
+ "signup": {
+ "title": "Sign up",
+ "firstNameLabel": "First Name:",
+ "lastNameLabel": "Last Name:",
+ "pseudoLabel": "Username:",
+ "genderLabel": "Gender:",
+ "genderMale": "Male",
+ "genderFemale": "Female",
+ "emailLabel": "Email:",
+ "passwordLabel": "Password:",
+ "confirmPasswordLabel": "Confirm Password:",
+ "submitButton": "Sign up",
+ "hasAccount": "Already have an account?",
+ "loginLink": "Log in here",
+ "passNoMatch": "Passwords do not match!",
+ "success": "Sign up successful!",
+ "error": "Error during sign up"
+ }
+ },
+ "profile": {
+ "title": "My Profile",
+ "personalInfo": "Personal Information",
+ "loyaltyPoints": "Loyalty points:",
+ "firstName": "First Name:",
+ "lastName": "Last Name:",
+ "pseudo": "Username:",
+ "email": "Email:",
+ "save": "Save",
+ "firstNameL": "First Name",
+ "lastNameL": "Last Name",
+ "pseudoL": "Username",
+ "emailL": "Email",
+ "changePasswordTitle": "Change Password",
+ "currentPassword": "Current password:",
+ "newPassword": "New password:",
+ "confirmNewPassword": "Confirm new password:",
+ "errorMismatch": "The new passwords do not match!",
+ "successPass": "Password successfully changed!",
+ "successUpdate": "Profile successfully updated!",
+ "errorGeneric": "An error occurred"
+ },
+ "about": {
+ "missionTitle": "Our Mission",
+ "missionDesc": "Our mission is to provide a complete and innovative solution for climate and environmental monitoring of the French territory. By combining high-quality precise weather forecasts with efficient IoT device management, we aim to offer a centralized platform to monitor local weather conditions in real time, while facilitating the analysis of data collected by IoT devices deployed across the country.",
+ "whoTitle": "Who are we?",
+ "whoDesc": "We are a team of tech, innovation, and environmental enthusiasts. We firmly believe that combining real-time weather data and the Internet of Things (IoT) can have a major impact on territorial management. Whether for local authorities, businesses, or public actors, our platform offers the tools needed for proactive and responsive environmental management.",
+ "visionTitle": "Our Vision",
+ "visionDesc": "In a world where climate conditions change rapidly, it is essential to anticipate and react effectively to meteorological phenomena. Thanks to our connected devices and intuitive interface, we allow users to track conditions in real time and act accordingly. From climate risk management to urban planning, our platform helps decision-makers make informed choices based on reliable, local data.",
+ "objectivesTitle": "The Objectives of Our Platform",
+ "obj1Desc": "Thanks to our connected devices, we collect local weather data, allowing continuous monitoring of climate conditions across the French territory.",
+ "obj1Title": "Real-time Monitoring",
+ "obj2Desc": "Using the best weather forecasting technologies, we provide you with accurate forecasts, whether it's temperature, wind speed, or air quality.",
+ "obj2Title": "Reliable Prediction",
+ "obj3Desc": "We allow users to easily manage their connected devices (weather stations, sensors, etc.) through a simple interface, while providing real-time tracking of their status and data.",
+ "obj3Title": "IoT Device Management",
+ "obj4Desc": "Our platform sends you instant alerts regarding extreme weather phenomena, allowing you to make quick and appropriate decisions.",
+ "obj4Title": "Rapid Response to Climate Alerts"
+ },
+ "admin": {
+ "sidebar": {
+ "panel": "Admin Panel",
+ "dashboard": "Dashboard",
+ "users": "Users",
+ "objects": "Device Management"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "manageWidgetsEnd": "Done Managing",
+ "manageWidgets": "Manage Widgets",
+ "summary": "Dashboard Summary",
+ "totalUsers": "Total Users",
+ "lastLog": "Last Log",
+ "noLog": "No log available",
+ "usersList": "User Management",
+ "username": "Username",
+ "email": "Email",
+ "access": "Access",
+ "noUser": "No users available",
+ "seeMore": "See more",
+ "objectsManagement": "Connected Device Management",
+ "consultObjects": "Consult connected devices",
+ "addObject": "Add a new device",
+ "objectsList": "List of Devices and Tools/Services",
+ "requestDelete": "Device delete request",
+ "generateReports": "Generate usage reports:",
+ "requestObjects": "Request devices",
+ "reportsStats": "Reports and Statistics",
+ "exportCsv": "Export to CSV",
+ "energyConsumption": "Total energy consumption",
+ "energyConsumptionDesc": "1372 kWh accumulated (estimated)",
+ "connectionRate": "User connection rate",
+ "connectionRateDesc": "87% of users active this month",
+ "mostUsedServices": "Most used services",
+ "addWidget": "Add a widget",
+ "chooseWidget": "Choose a widget type",
+ "widgetSummary": "Dashboard Summary",
+ "widgetUsers": "User Management",
+ "widgetObjects": "Connected Device Management",
+ "widgetObjectsList": "List of Devices & Tools",
+ "widgetReports": "Reports and Statistics",
+ "widgetDelete": "Device delete request",
+ "cancel": "Cancel"
+ },
+ "user": {
+ "title": "User Management",
+ "subtitle": "Add a user from this form",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "pseudo": "Username",
+ "email": "Email",
+ "password": "Password",
+ "genderMale": "Male",
+ "genderFemale": "Female",
+ "genderOther": "Other",
+ "addUserBtn": "Add User",
+ "manageTitle": "Manage users from this panel.",
+ "gender": "Gender",
+ "accessLevel": "Access Level",
+ "points": "Points",
+ "actions": "Actions",
+ "changeBtn": "Change",
+ "deleteBtn": "Delete",
+ "logsTitle": "Connection history and logs",
+ "action": "Action",
+ "timestamp": "Timestamp",
+ "downloadLogs": "Download Logs",
+ "successAdd": "User successfully added!",
+ "errorAdd": "Error adding user!",
+ "confirmDelete": "Are you sure you want to delete user {name}? This action may result in the deletion of associated objects.",
+ "successDelete": "User successfully deleted!",
+ "successLevel": "Access level successfully changed!",
+ "errorLevel": "There was an error changing the access level!",
+ "successPoints": "Points successfully saved!",
+ "errorPoints": "There was an error saving the points!"
+ },
+ "adminObjet": {
+ "title": "Administration of Devices and Tools/Services",
+ "catsTitle": "Category Management",
+ "newCatPlaceholder": "New category",
+ "addBtn": "Add",
+ "deleteBtn": "Delete",
+ "listTitle": "List of Devices and Tools/Services",
+ "sortBy": "-- Sort by --",
+ "sortOwner": "Owner",
+ "sortLocation": "Location",
+ "sortType": "Type",
+ "sortStatus": "Status",
+ "colName": "Name",
+ "colDesc": "Description",
+ "colType": "Type",
+ "colLocation": "Location",
+ "colOwner": "Owner",
+ "colStatus": "Status",
+ "noObjects": "No object or service available.",
+ "requestsTitle": "Device Deletion Requests",
+ "colObjId": "Device ID",
+ "colUserId": "User ID",
+ "colReqDate": "Request Date",
+ "acceptBtn": "Accept",
+ "rejectBtn": "Reject",
+ "noRequests": "No deletion requests found.",
+ "confirmCatDelete": "Are you sure you want to delete category {cat}? This action might impact devices.",
+ "successRecord": "Your device has been recorded successfully!",
+ "successReqDelete": "The request has been successfully deleted!",
+ "successObjDelete": "Your device has been successfully deleted!",
+ "errorReqDelete": "There was an error deleting the request!",
+ "errorObjDelete": "There was an error deleting your device!"
+ }
+ },
+ "gestion": {
+ "title": "Welcome to the module",
+ "moduleName": "Management",
+ "description": "This module allows you to easily and effectively manage connected sensors and stations in France.",
+ "consultTitle": "Consult connected weather devices",
+ "consultDesc": "Access real-time data from connected weather devices, modify their parameters and view the history of measurements.",
+ "exploreBtn": "Explore devices",
+ "addTitle": "Add a new connected device",
+ "addDesc": "Easily integrate a new connected device by providing its information and configuring its parameters for optimal management.",
+ "addBtn": "Add a device",
+ "objectManagement": {
+ "successDeleteReq": "Deletion request sent to administrator.",
+ "errorDeleteReq": "Error, request already sent to administrator.",
+ "titleAdmin": "Management",
+ "titleUser": "Visualization",
+ "titleSuffix": "of",
+ "titleObjects": "Connected Devices.",
+ "searchPlaceholder": "Search by name, category, or keywords...",
+ "filterAll": "All",
+ "filterStation": "Weather Station",
+ "filterSensor": "Sensor",
+ "filterActive": "Active",
+ "filterInactive": "Inactive",
+ "noObjects": "No device found",
+ "moreInfo": "More info",
+ "deleteItem": "Delete device",
+ "seeMore": "See more"
+ },
+ "objet": {
+ "dashboardTitle": "Dashboard - ",
+ "errorFetch": "Error fetching device"
+ },
+ "addObject": {
+ "title": "New device"
+ }
+ },
+ "components": {
+ "formNewObject": {
+ "successRecord": "Your device has been successfully registered!",
+ "errorRecord": "There was an error adding your device!",
+ "noCategory": "No category available.",
+ "addTitle": "Add a new device",
+ "enterData": "Enter the data for your new device",
+ "confirmData": "Are you sure about this data?",
+ "name": "Name:",
+ "description": "Description:",
+ "type": "Type:",
+ "selectType": "-- Select a type --",
+ "location": "Location:",
+ "batteryType": "Battery type:",
+ "owner": "Owner:",
+ "status": "Status:",
+ "inactive": "Inactive",
+ "active": "Active",
+ "confirmInfos": "Confirm information",
+ "sureBtn": "Yes, I am sure!",
+ "deleteInfos": "Delete information",
+ "changeBtn": "No, I want to change!"
+ },
+ "infoObject": {
+ "title": "Information",
+ "description": "Description:",
+ "type": "Type:",
+ "location": "Location:",
+ "status": "Status:",
+ "lastUpdate": "Last update:",
+ "modify": "Modify this information"
+ },
+ "modifObject": {
+ "title": "Modify information",
+ "description": "Description:",
+ "type": "Type:",
+ "location": "Location:",
+ "status": "Status:",
+ "inactive": "Inactive",
+ "active": "Active",
+ "confirmMods": "Confirm modifications",
+ "cancelMods": "Cancel modifications"
+ },
+ "particularMeteo": {
+ "defineLimit": "Set the threshold value for the alert:",
+ "outOfBounds": "Warning, the current value is out of the bounds you defined!",
+ "setAlert": "Set alert"
+ },
+ "meteoInfos": {
+ "alertInactive": "This device might be inactive due to lack of data. You can make it inactive by changing its status.",
+ "currentWeather": "Current Weather",
+ "temperature": "Temperature",
+ "pressure": "Pressure",
+ "humidity": "Humidity",
+ "lastRecord": "Last record:",
+ "loading": "Loading data..."
+ },
+ "batterieInfo": {
+ "title": "Battery Status",
+ "batteryType": "Battery type:"
+ },
+ "userInfosObject": {
+ "title": "Owner",
+ "pseudo": "Username:",
+ "gender": "Gender:",
+ "points": "Number of points:"
+ },
+ "windGraph": {
+ "title": "Wind History",
+ "date": "Date:",
+ "windSpeed": "Wind speed:",
+ "windDirection": "Wind direction:"
+ },
+ "meteoGraph": {
+ "historyTemp": "Temperature History",
+ "historyHum": "Humidity History",
+ "historyPres": "Pressure History",
+ "average": "Average"
+ },
+ "windInfo": {
+ "currentWind": "Current Wind",
+ "value": "Value",
+ "lastRecord": "Last record:",
+ "loading": "Loading data..."
+ }
+ }
+}
+
diff --git a/z1/Front-end/src/locales/fr.json b/z1/Front-end/src/locales/fr.json
new file mode 100644
index 0000000..5c2ac5a
--- /dev/null
+++ b/z1/Front-end/src/locales/fr.json
@@ -0,0 +1,363 @@
+{
+ "header": {
+ "home": "Accueil",
+ "about": "À propos",
+ "login": "Connexion",
+ "signup": "Inscription",
+ "visualizer": "Visualisation",
+ "manage": "Gestion",
+ "dashboard": "Dashboard",
+ "manageUsers": "Gestion des Utilisateurs",
+ "manageObjects": "Gestion des Objets Connectés",
+ "profile": "Profil",
+ "logout": "Déconnexion",
+ "admin": "Admin"
+ },
+ "home": {
+ "title": "La météo en temps réel",
+ "subtitle": "Prévisions précises et personnalisées pour vous aider à planifier votre journée en toute sérénité.",
+ "searchPlaceholder": "Rechercher une ville ...",
+ "noCityFound": "Aucune ville trouvée.",
+ "partlyCloudy": "Partiellement nuageux",
+ "feelsLike": "Ressenti",
+ "humidity": "Humidité",
+ "wind": "Vent",
+ "rain": "Précipitations",
+ "sunrise": "Lever du soleil",
+ "sunset": "Coucher du soleil",
+ "hourlyForecast": "Prévisions horaires",
+ "dailyForecast": "Prévisions 5 jours",
+ "weatherServices": "Services météo",
+ "viewObjectsTitle": "Visualisation des objets connectés météorologiques",
+ "viewObjectsDesc": "Consultez l'ensemble des objets connectés ainsi que leurs données dans l'ensemble de la France",
+ "viewObjectsBtn": "Voir les objets",
+ "signupTitle": "Inscrivez-vous !",
+ "signupDesc": "Avoir un compte sur notre site permet de consulter l'intégralité des objets connectés météorologiques dans l'ensemble de la France",
+ "signupBtn": "S'inscrire",
+ "loginTitle": "Connectez-vous !",
+ "loginDesc": "Heureux de vous retrouver ! Retrouvez votre compte tel que vous l'avez laissé !",
+ "loginBtn": "Se connecter",
+ "consultObjectsTitle": "Consulter les objets connectés météorologiques",
+ "consultObjectsDesc": "Accédez aux données en temps réel des objets connectés météorologiques, modifiez leurs paramètres et consultez l'historique des mesures.",
+ "consultObjectsBtn": "Explorer les objets",
+ "addObjTitle": "Ajouter un nouvel objet connecté",
+ "addObjDesc": "Intégrez facilement un nouvel objet connecté en renseignant ses informations et en configurant ses paramètres pour une gestion optimale.",
+ "addObjBtn": "Ajouter un objet",
+ "adminDashTitle": "Accéder au tableau d'Administration",
+ "adminDashDesc": "Vous pourrez gérer les utilisateurs du site mais aussi les objets connectés",
+ "adminDashBtn": "Tableau d'Administration",
+ "manageObjTitle": "Gestion des objets connectés",
+ "manageObjDesc": "Ce module vous permet de gérer les capteurs et stations connectés de France de manière simple et efficace.",
+ "manageObjBtn": "Gestion des objets",
+ "footerRights": "© 2025 VigiMétéo. Tous droits réservés."
+ },
+ "auth": {
+ "login": {
+ "title": "Connexion",
+ "emailLabel": "Email:",
+ "passwordLabel": "Mot de passe:",
+ "submitButton": "Se connecter",
+ "noAccount": "Vous n'avez pas de compte ?",
+ "signupLink": "Inscrivez-vous ici",
+ "loading": "Connexion en cours...",
+ "success": "Connexion réussie! Redirection...",
+ "missingToken": "Authentification échouée : token manquant dans la réponse",
+ "incorrectAuth": "Email ou mot de passe incorrect",
+ "invalidData": "Données de formulaire invalides",
+ "serverError": "Erreur serveur. Veuillez réessayer plus tard.",
+ "networkError": "Impossible de joindre le serveur. Vérifiez votre connexion internet.",
+ "genericError": "Une erreur s'est produite. Veuillez réessayer."
+ },
+ "signup": {
+ "title": "Inscription",
+ "firstNameLabel": "Prénom:",
+ "lastNameLabel": "Nom:",
+ "pseudoLabel": "Pseudo:",
+ "genderLabel": "Sexe:",
+ "genderMale": "Homme",
+ "genderFemale": "Femme",
+ "emailLabel": "Email:",
+ "passwordLabel": "Mot de passe:",
+ "confirmPasswordLabel": "Confirmer le mot de passe:",
+ "submitButton": "S'inscrire",
+ "hasAccount": "Vous avez déjà un compte ?",
+ "loginLink": "Connectez-vous ici",
+ "passNoMatch": "Les mots de passe ne correspondent pas !",
+ "success": "Inscription réussie !",
+ "error": "Erreur lors de l'inscription"
+ }
+ },
+ "profile": {
+ "title": "Mon Profil",
+ "personalInfo": "Informations Personnelles",
+ "loyaltyPoints": "Points de fidélité:",
+ "firstName": "Prénom:",
+ "lastName": "Nom:",
+ "pseudo": "Pseudo:",
+ "email": "Email:",
+ "save": "Sauvegarder",
+ "firstNameL": "Prénom",
+ "lastNameL": "Nom",
+ "pseudoL": "Pseudo",
+ "emailL": "Email",
+ "changePasswordTitle": "Modifier le mot de passe",
+ "currentPassword": "Mot de passe actuel:",
+ "newPassword": "Nouveau mot de passe:",
+ "confirmNewPassword": "Confirmer le nouveau mot de passe:",
+ "errorMismatch": "Les nouveaux mots de passe ne correspondent pas !",
+ "successPass": "Mot de passe modifié avec succès !",
+ "successUpdate": "Profil mis à jour avec succès !",
+ "errorGeneric": "Une erreur est survenue"
+ },
+ "about": {
+ "missionTitle": "Notre mission",
+ "missionDesc": "Notre mission est de fournir une solution complète et innovante pour la surveillance climatique et environnementale du territoire français. En combinant des prévisions météorologiques de haute qualité avec une gestion efficace des objets connectés, nous visons à offrir une plateforme centralisée permettant de surveiller en temps réel les conditions météorologiques locales, tout en facilitant l'analyse des données collectées par des objets connectés déployés à travers le pays.",
+ "whoTitle": "Qui sommes-nous ?",
+ "whoDesc": "Nous sommes une équipe de passionnés de technologie, d’innovation et d’environnement. Nous croyons fermement que la combinaison de la donnée météorologique en temps réel et de l’Internet des Objets (IoT) peut avoir un impact majeur sur la gestion des territoires. Que ce soit pour les collectivités locales, les entreprises ou les acteurs publics, notre plateforme offre les outils nécessaires pour une gestion proactive et réactive de l’environnement.",
+ "visionTitle": "Notre Vision",
+ "visionDesc": "Dans un monde où les conditions climatiques évoluent rapidement, il est essentiel de pouvoir anticiper et réagir efficacement face aux phénomènes météorologiques. Grâce à nos objets connectés et à notre interface intuitive, nous permettons aux utilisateurs de suivre les conditions en temps réel et d’agir en conséquence. De la gestion des risques climatiques à la planification urbaine, notre plateforme aide les décideurs à prendre des décisions éclairées basées sur des données fiables et locales.",
+ "objectivesTitle": "Les Objectifs de Notre Plateforme",
+ "obj1Desc": "Grâce à nos objets connectés, nous collectons des données météorologiques locales, permettant une surveillance continue des conditions climatiques sur tout le territoire français.",
+ "obj1Title": "Surveillance en temps réel",
+ "obj2Desc": "En utilisant les meilleures technologies de prévision météorologique, nous vous fournissons des prévisions précises, qu’il s’agisse de la température, de la vitesse du vent ou de la qualité de l’air.",
+ "obj2Title": "Prédiction fiable",
+ "obj3Desc": "Nous permettons aux utilisateurs de gérer facilement leurs objets connectés (stations météo, capteurs, etc.) à travers une interface simple, tout en offrant un suivi en temps réel de leur statut et de leurs données.",
+ "obj3Title": "Gestion des objets connectés",
+ "obj4Desc": "Notre plateforme vous envoie des alertes instantanées concernant les phénomènes météorologiques extrêmes, vous permettant de prendre des décisions rapides et adaptées.",
+ "obj4Title": "Réponse rapide aux alertes climatiques"
+ },
+ "admin": {
+ "sidebar": {
+ "panel": "Admin Panel",
+ "dashboard": "Tableau de bord",
+ "users": "Utilisateurs",
+ "objects": "Gestion des objets"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "manageWidgetsEnd": "Terminer la gestion",
+ "manageWidgets": "Gérer les widgets",
+ "summary": "Résumé du tableau de bord",
+ "totalUsers": "Total Utilisateur",
+ "lastLog": "Dernier Log",
+ "noLog": "Aucun log",
+ "usersList": "Gestion des Utilisateurs",
+ "username": "Username",
+ "email": "Email",
+ "access": "Access",
+ "noUser": "Aucun utilisateur disponible",
+ "seeMore": "Voir plus",
+ "objectsManagement": "Gestion des Objets Connectés",
+ "consultObjects": "Consulter les objets connectés",
+ "addObject": "Ajouter un nouvel objet",
+ "objectsList": "Liste des Objets et Outils/Services",
+ "requestDelete": "Requête suppression objets",
+ "generateReports": "Générer des rapports d'utilisation :",
+ "requestObjects": "Requête objets",
+ "reportsStats": "Rapports et Statistiques",
+ "exportCsv": "Exporter en CSV",
+ "energyConsumption": "Consommation énergétique totale",
+ "energyConsumptionDesc": "1372 kWh cumulés (estimation)",
+ "connectionRate": "Taux de connexion des utilisateurs",
+ "connectionRateDesc": "87% des utilisateurs actifs ce mois-ci",
+ "mostUsedServices": "Services les plus utilisés",
+ "addWidget": "Ajouter un widget",
+ "chooseWidget": "Choisir un type de widget",
+ "widgetSummary": "Dashboard Summary",
+ "widgetUsers": "Gestion des Utilisateurs",
+ "widgetObjects": "Gestion des Objets Connectés",
+ "widgetObjectsList": "Liste des Objets et Outils/Services",
+ "widgetReports": "Rapports et Statistiques",
+ "widgetDelete": "Demande de suppression d'objets",
+ "cancel": "Annuler"
+ },
+ "user": {
+ "title": "Gestion des utilisateurs",
+ "subtitle": "Ajoutez un utilisateur à partir de ce formulaire",
+ "firstName": "Prénom",
+ "lastName": "Nom",
+ "pseudo": "Pseudo",
+ "email": "Email",
+ "password": "Mot de passe",
+ "genderMale": "Homme",
+ "genderFemale": "Femme",
+ "genderOther": "Autre",
+ "addUserBtn": "Ajouter utilisateur",
+ "manageTitle": "Gérez les utilisateurs à partir de ce panneau.",
+ "gender": "Genre",
+ "accessLevel": "Niveau d'accès",
+ "points": "Points",
+ "actions": "Actions",
+ "changeBtn": "Changer",
+ "deleteBtn": "Supprimer",
+ "logsTitle": "Historique des connexions et journal des logs",
+ "action": "Action",
+ "timestamp": "Timestamp",
+ "downloadLogs": "Télécharger les logs",
+ "successAdd": "Ajout de l'utilisateur réussi !",
+ "errorAdd": "Erreur lors de l'ajout de l'utilisateur !",
+ "confirmDelete": "Êtes-vous sûr de vouloir supprimer l'utilisateur {name} ? Cette action pourrait entraîner la suppression des objets associés.",
+ "successDelete": "L'utilisateur a bien été supprimé !",
+ "successLevel": "Le changement de niveau a bien été enregistré !",
+ "errorLevel": "Il y a eu une erreur dans le changement de niveau !",
+ "successPoints": "Les points ont bien été enregistrés !",
+ "errorPoints": "Il y a eu une erreur dans l'ajout des points !"
+ },
+ "adminObjet": {
+ "title": "Administration des Objets et Outils/Services",
+ "catsTitle": "Gestion des Catégories",
+ "newCatPlaceholder": "Nouvelle catégorie",
+ "addBtn": "Ajouter",
+ "deleteBtn": "Supprimer",
+ "listTitle": "Liste des Objets et Outils/Services",
+ "sortBy": "-- Trier par --",
+ "sortOwner": "Propriétaire",
+ "sortLocation": "Lieux",
+ "sortType": "Type",
+ "sortStatus": "Status",
+ "colName": "Nom",
+ "colDesc": "Description",
+ "colType": "Type",
+ "colLocation": "Localisation",
+ "colOwner": "Propriétaire",
+ "colStatus": "Status",
+ "noObjects": "Aucun objet ou service disponible.",
+ "requestsTitle": "Demandes de Suppression d'Objets",
+ "colObjId": "Objet ID",
+ "colUserId": "Utilisateur ID",
+ "colReqDate": "Date de Requête",
+ "acceptBtn": "Accepter",
+ "rejectBtn": "Refuser",
+ "noRequests": "Aucune demande de suppression trouvée.",
+ "confirmCatDelete": "Êtes-vous sûr de vouloir supprimer la catégorie {cat} ? Cette action pourrait impacter des objets.",
+ "successRecord": "Votre objet a bien été enregistré !",
+ "successReqDelete": "La demande a bien été supprimée !",
+ "successObjDelete": "Votre objet a bien été supprimé !",
+ "errorReqDelete": "Il y a eu une erreur dans la suppression de la demande !",
+ "errorObjDelete": "Il y a eu une erreur dans la suppression de votre objet !"
+ }
+ },
+ "gestion": {
+ "title": "Bienvenue dans le module",
+ "moduleName": "Gestion",
+ "description": "Ce module vous permet de gérer les capteurs et stations connectés de France de manière simple et efficace.",
+ "consultTitle": "Consulter les objets connectés météorologiques",
+ "consultDesc": "Accédez aux données en temps réel des objets connectés météorologiques, modifiez leurs paramètres et consultez l'historique des mesures.",
+ "exploreBtn": "Explorer les objets",
+ "addTitle": "Ajouter un nouvel objet connecté",
+ "addDesc": "Intégrez facilement un nouvel objet connecté en renseignant ses informations et en configurant ses paramètres pour une gestion optimale.",
+ "addBtn": "Ajouter un objet",
+ "objectManagement": {
+ "successDeleteReq": "Demande de suppression envoyée à l'administrateur.",
+ "errorDeleteReq": "Erreur, demande déjà envoyée à l'administrateur.",
+ "titleAdmin": "Gestion",
+ "titleUser": "Visualisation",
+ "titleSuffix": "des",
+ "titleObjects": "Objets connectés.",
+ "searchPlaceholder": "Chercher par nom, catégorie ou mot clés...",
+ "filterAll": "Tout",
+ "filterStation": "Station météo",
+ "filterSensor": "Capteur",
+ "filterActive": "Actif",
+ "filterInactive": "Inactif",
+ "noObjects": "Aucun objet trouvé",
+ "moreInfo": "Plus d'infos",
+ "deleteItem": "Supprimer l'objet",
+ "seeMore": "Voir plus"
+ },
+ "objet": {
+ "dashboardTitle": "Tableau de bord - ",
+ "errorFetch": "Erreur de récupération de l'objet"
+ },
+ "addObject": {
+ "title": "Nouvel objet"
+ }
+ },
+ "components": {
+ "formNewObject": {
+ "successRecord": "Votre objet a bien été enregistré !",
+ "errorRecord": "Il y a eu une erreur dans l'ajout de votre objet !",
+ "noCategory": "Aucune catégorie disponible.",
+ "addTitle": "Ajouter un nouvel objet",
+ "enterData": "Entrez les données de votre nouvel objet",
+ "confirmData": "Êtes-vous sûr de ces données ?",
+ "name": "Nom :",
+ "description": "Description :",
+ "type": "Type :",
+ "selectType": "-- Sélectionner un type --",
+ "location": "Localisation :",
+ "batteryType": "Type de batterie :",
+ "owner": "Propriétaire :",
+ "status": "Status :",
+ "inactive": "Inactive",
+ "active": "Active",
+ "confirmInfos": "Confirmer les informations",
+ "sureBtn": "Oui je suis sûr !",
+ "deleteInfos": "Supprimer les informations",
+ "changeBtn": "Non je veux changer !"
+ },
+ "infoObject": {
+ "title": "Informations",
+ "description": "Description :",
+ "type": "Type :",
+ "location": "Localisation :",
+ "status": "Status :",
+ "lastUpdate": "Dernière mise à jour :",
+ "modify": "Modifier ces infos"
+ },
+ "modifObject": {
+ "title": "Modifier les infos",
+ "description": "Description :",
+ "type": "Type :",
+ "location": "Localisation :",
+ "status": "Status :",
+ "inactive": "Inactive",
+ "active": "Active",
+ "confirmMods": "Confirmer les modifications",
+ "cancelMods": "Annuler les modifications"
+ },
+ "particularMeteo": {
+ "defineLimit": "Définissez la valeur seuil pour l'alerte :",
+ "outOfBounds": "Attention, la valeur actuelle est hors des bornes que vous avez définies !",
+ "setAlert": "Définir alerte"
+ },
+ "meteoInfos": {
+ "alertInactive": "Cet objet peut être inactif dû à son manque de données. Vous pouvez le rendre inactif en changeant son statut.",
+ "currentWeather": "Météo actuelle",
+ "temperature": "Température",
+ "pressure": "Pression",
+ "humidity": "Humidité",
+ "lastRecord": "Dernier enregistrement :",
+ "loading": "Chargement des données..."
+ },
+ "batterieInfo": {
+ "title": "Etat de la batterie",
+ "batteryType": "Type de batterie :"
+ },
+ "userInfosObject": {
+ "title": "Propriétaire",
+ "pseudo": "Pseudo :",
+ "gender": "Genre :",
+ "points": "Nombre de points :"
+ },
+ "windGraph": {
+ "title": "Historique du vent",
+ "date": "Date :",
+ "windSpeed": "Vitesse du vent :",
+ "windDirection": "Direction du vent :"
+ },
+ "meteoGraph": {
+ "historyTemp": "Historique de la température",
+ "historyHum": "Historique de l'humidité",
+ "historyPres": "Historique de la pression",
+ "average": "Moyenne"
+ },
+ "windInfo": {
+ "currentWind": "Vent actuel",
+ "value": "Valeur",
+ "lastRecord": "Dernier enregistrement :",
+ "loading": "Chargement des données..."
+ }
+ }
+}
+
diff --git a/z1/Front-end/src/main.jsx b/z1/Front-end/src/main.jsx
new file mode 100644
index 0000000..26f752f
--- /dev/null
+++ b/z1/Front-end/src/main.jsx
@@ -0,0 +1,11 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App.jsx';
+import './index.css';
+import './i18n';
+
+createRoot(document.getElementById('root')).render(
+
+
+
+);
\ No newline at end of file
diff --git a/z1/Front-end/src/pages/About.jsx b/z1/Front-end/src/pages/About.jsx
new file mode 100644
index 0000000..5951ffc
--- /dev/null
+++ b/z1/Front-end/src/pages/About.jsx
@@ -0,0 +1,137 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+
+function About() {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {/* Grille principale */}
+
+ {/* Section Notre mission */}
+
+
+ {t('about.missionTitle')}
+
+
+ {t('about.missionDesc')}
+
+
+

+
+ {/* Section Qui sommes-nous */}
+

+
+
+ {t('about.whoTitle')}
+
+
+ {t('about.whoDesc')}
+
+
+
+ {/* Section Notre Vision */}
+
+
+ {t('about.visionTitle')}
+
+
+ {t('about.visionDesc')}
+
+
+

+
+
+ {/* Section Objectifs */}
+
+
+ {t('about.objectivesTitle')}
+
+
+ {/* Objectif 1 */}
+
+

+
+
+ {t('about.obj1Desc')}
+
+
+
+ {t('about.obj1Title')}
+
+
+
+ {/* Objectif 2 */}
+
+

+
+
+ {t('about.obj2Desc')}
+
+
+
{t('about.obj2Title')}
+
+
+ {/* Objectif 3 */}
+
+

+
+
+ {t('about.obj3Desc')}
+
+
+
+ {t('about.obj3Title')}
+
+
+
+ {/* Objectif 4 */}
+
+

+
+
+ {t('about.obj4Desc')}
+
+
+
+ {t('about.obj4Title')}
+
+
+
+
+
+
+
+ );
+}
+
+export default About;
diff --git a/z1/Front-end/src/pages/Admin/AdminObjet.jsx b/z1/Front-end/src/pages/Admin/AdminObjet.jsx
new file mode 100644
index 0000000..1bc5a8f
--- /dev/null
+++ b/z1/Front-end/src/pages/Admin/AdminObjet.jsx
@@ -0,0 +1,370 @@
+import React, { useState, useEffect } from "react";
+import Sidebar from "./sidebar.jsx";
+import axios from "axios";
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../../config";
+import AddObject from "../Gestion/AddObject.jsx";
+import FormNewObject from "../../components/FormNewObject.jsx";
+
+function AdminObjet() {
+ const { t } = useTranslation();
+ const [categories, setCategories] = useState();
+ const [newCategory, setNewCategory] = useState("");
+ const [objects, setObjects] = useState([]);
+ const [deleteRequests, setDeleteRequests] = useState([]);
+
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/objets`).then((response) => {
+ setObjects(response.data);
+ });
+ }, []);
+
+ useEffect(() => {
+ axios
+ .get(`${API_BASE_URL}/getDemandeSuppression`)
+ .then((response) => {
+ setDeleteRequests(response.data);
+ })
+ .catch((error) => {
+ console.error(
+ "Erreur lors de la récupération des requêtes de suppression :",
+ error
+ );
+ });
+ }, []);
+
+ const handleAddCategory = () => {
+ const trimmed = newCategory.trim();
+ if (trimmed !== "" && !categories.includes(trimmed)) {
+ axios
+ .post(`${API_BASE_URL}/addCategories`, { name: trimmed })
+ .then((response) => {
+ console.log("Catégorie ajoutée :", response.data);
+ setCategories([...categories, trimmed]);
+ setNewCategory("");
+ window.location.reload();
+ })
+ .catch((error) => {
+ console.error("Erreur lors de l'ajout de la catégorie :", error);
+ });
+ }
+ };
+ const handleDeleteCategory = (categoryToDelete) => {
+ const confirmation = window.confirm(
+ t('admin.adminObjet.confirmCatDelete').replace('{cat}', categoryToDelete)
+ );
+
+ if (confirmation) {
+ setCategories(categories.filter((cat) => cat !== categoryToDelete));
+ axios
+ .post(`${API_BASE_URL}/deleteCategories`, { name: categoryToDelete })
+ .then((response) => {
+ console.log("Catégorie supprimée :", response.data);
+ window.location.reload();
+ })
+ .catch((error) => {
+ console.error(
+ "Erreur lors de la suppression de la catégorie :",
+ error
+ );
+ });
+ } else {
+ console.log("Suppression annulée.");
+ }
+ };
+
+ useEffect(() => {
+ axios
+ .get(`${API_BASE_URL}/getCategories`)
+ .then((response) => {
+ if (response.data.length === 0) {
+ console.warn("Aucune catégorie disponible.");
+ } else {
+ setCategories(response.data);
+ }
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération des catégories :", error);
+ });
+ }, []);
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [type, setType] = useState("");
+ const [location, setLocation] = useState("");
+ const [proprietaire, setProprietaire] = useState("");
+ const [status, setStatus] = useState("active");
+ const [verif, setVerif] = useState(false);
+ const [enregistre, setEnregistre] = useState(false);
+ const [messRequete, setMessRequete] = useState("");
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ if (verif) {
+ const newObj = {
+ id: Date.now(),
+ name: name.trim(),
+ description: description.trim(),
+ type: type.trim(),
+ location: location.trim(),
+ proprietaire: proprietaire.trim(),
+ status: status,
+ };
+ setObjects([...objects, newObj]);
+ setMessRequete(t('admin.adminObjet.successRecord'));
+ setEnregistre(true);
+ setVerif(false);
+ resetForm();
+ window.location.reload();
+ } else {
+ setVerif(true);
+ }
+ };
+ const handleReject =(id) => {
+ axios
+ .post(`${API_BASE_URL}/reject`, {
+ id,
+ })
+ .then((response) => {
+ setMessRequete(t('admin.adminObjet.successReqDelete'));
+ console.log("La demande à été supprimée :", response.data);
+ window.location.reload();
+ })
+ .catch((error) => {
+ setMessRequete(
+ t('admin.adminObjet.errorReqDelete')
+ );
+ console.error("Erreur lors de la suppression de la demande :", error);
+ });
+ }
+
+ const handleDeleteObject = (id) => {
+ axios
+ .post(`${API_BASE_URL}/deleteObject`, {
+ id,
+ })
+ .then((response) => {
+ alert(t('admin.adminObjet.successObjDelete'));
+ console.log("Votre objet à été supprimé :", response.data);
+ window.location.reload();
+ })
+ .catch((error) => {
+ alert(
+ t('admin.adminObjet.errorObjDelete')
+ );
+ console.error("Erreur lors de la suppression de l'objet :", error);
+ });
+ };
+
+ const [sortCriteria, setSortCriteria] = useState("");
+
+ const sortedObjects = [...objects].sort((a, b) => {
+ if (!sortCriteria) return 0;
+ let fieldA = a[sortCriteria] || "";
+ let fieldB = b[sortCriteria] || "";
+ return fieldA.localeCompare(fieldB);
+ });
+
+ return (
+
+
+
+
+
+ {t('admin.adminObjet.title')}
+
+
+
+
+
+
+
+
+ {t('admin.adminObjet.listTitle')}
+
+
+
+
+
+
+
+ |
+ {t('admin.adminObjet.colName')}
+ |
+
+ {t('admin.adminObjet.colDesc')}
+ |
+
+ {t('admin.adminObjet.colType')}
+ |
+
+ {t('admin.adminObjet.colLocation')}
+ |
+
+ {t('admin.adminObjet.colOwner')}
+ |
+
+ {t('admin.adminObjet.colStatus')}
+ |
+ |
+
+
+
+ {sortedObjects.map((obj) => (
+
+ |
+
+ {obj.name}
+
+ |
+
+ {obj.description}
+ |
+
+ {obj.type}
+ |
+
+ {obj.location}
+ |
+
+ {obj.proprio_id}
+ |
+
+ {obj.status}
+ |
+
+
+ |
+
+ ))}
+ {objects.length === 0 && (
+
+ |
+ {t('admin.adminObjet.noObjects')}
+ |
+
+ )}
+
+
+
+
+
+
+
+
+
+ {t('admin.adminObjet.requestsTitle')}
+
+
+
+
+
+
+ | ID |
+ {t('admin.adminObjet.colObjId')} |
+ {t('admin.adminObjet.colUserId')} |
+ {t('admin.adminObjet.colReqDate')} |
+ {t('admin.user.actions')} |
+
+
+
+ {deleteRequests.map((request) => (
+
+ | {request.id} |
+ Objet n°{request.object_id} |
+ {request.requested_by} |
+ {request.request_date} |
+
+
+
+ |
+
+ ))}
+ {deleteRequests.length === 0 && (
+
+ |
+ {t('admin.adminObjet.noRequests')}
+ |
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default AdminObjet;
diff --git a/z1/Front-end/src/pages/Admin/Dashboard.jsx b/z1/Front-end/src/pages/Admin/Dashboard.jsx
new file mode 100644
index 0000000..c6a18ed
--- /dev/null
+++ b/z1/Front-end/src/pages/Admin/Dashboard.jsx
@@ -0,0 +1,408 @@
+import React, { useState, useEffect } from "react";
+import Sidebar from "./sidebar.jsx";
+import { RadioTower,Minus, ArrowRight, BadgePlus, Settings } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../../config.js";
+import axios from "axios";
+
+const exportCSV = () => {
+ const headers = ["Catégorie", "Valeur"];
+ const rows = [
+ ["Consommation énergétique", "1372 kWh"],
+ ["Taux de connexion", "87%"],
+ ["Service", "Consultation des données météo"],
+ ["Service", "Alertes et suivi de consommation"],
+ ["Service", "Ajout d'objets connectés"],
+ ];
+
+ const csvContent =
+ "\uFEFF" +
+ [headers, ...rows]
+ .map((row) => row.map((val) => `"${val.replace(/"/g, '""')}"`).join(","))
+ .join("\n");
+
+ const blob = new Blob([csvContent], {
+ type: "text/csv;charset=utf-8;",
+ });
+
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.setAttribute("download", "rapport_plateforme.csv");
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+};
+
+const initialWidgets = [
+ { id: 1, type: "summary" },
+ { id: 2, type: "users" },
+ { id: 3, type: "reporting" },
+ { id: 4, type: "adminobjet" },
+ { id: 5, type: "objects" },
+];
+
+function Dashboard() {
+ const { t } = useTranslation();
+ const [users, setUsers] = useState([]);
+
+ const [logs, setLogs] = useState([
+ {
+ id: 1,
+ username: "complexe",
+ action: "Accès attribué",
+ timestamp: new Date().toLocaleString(),
+ },
+ {
+ id: 2,
+ username: "admin",
+ action: "Accès attribué",
+ timestamp: new Date().toLocaleString(),
+ },
+ ]);
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/users`).then((response) => {
+ setUsers(response.data);
+ });
+
+ axios.get(`${API_BASE_URL}/objets`).then((response) => {
+ setAdminObjects(response.data);
+ });
+
+ }, []);
+
+ const [adminObjects, setAdminObjects] = useState([]);
+
+ const [manageMode, setManageMode] = useState(false);
+ const [widgets, setWidgets] = useState(initialWidgets);
+
+ const [showAddWidgetModal, setShowAddWidgetModal] = useState(false);
+
+ const handleDeleteWidget = (id) => {
+ setWidgets(widgets.filter((widget) => widget.id !== id));
+ };
+
+ const openAddWidgetModal = () => {
+ setShowAddWidgetModal(true);
+ };
+
+ const handleWidgetSelection = (widgetType) => {
+ const newWidget = { id: Date.now(), type: widgetType };
+ setWidgets([...widgets, newWidget]);
+ setShowAddWidgetModal(false);
+ };
+
+ return (
+
+
+
+
+
{t('admin.dashboard.title')}
+
+
+
+
+ {widgets.map((widget) => (
+
+ {manageMode && (
+
+ )}
+
+ {widget.type === "summary" && (
+
+
+ {t('admin.dashboard.summary')}
+
+
+
{t('admin.dashboard.totalUsers')}
+
{users.length}
+
+
+
{t('admin.dashboard.lastLog')}
+ {logs.length > 0 ? (
+
+ {logs[logs.length - 1].username} -{" "}
+ {logs[logs.length - 1].action}
+
+ ) : (
+
{t('admin.dashboard.noLog')}
+ )}
+
+
+ )}
+
+ {widget.type === "users" && (
+
+
+ {t('admin.dashboard.usersList')}
+
+
+
+
+
+ |
+ {t('admin.dashboard.username')}
+ |
+
+ {t('admin.dashboard.email')}
+ |
+
+ {t('admin.dashboard.access')}
+ |
+
+
+
+ {users.slice(0, 5).map((user) => (
+
+ |
+ {user.pseudo}
+ |
+
+ {user.email}
+ |
+
+ {user.role}
+ |
+
+ ))}
+ {users.length === 0 && (
+
+ |
+ {t('admin.dashboard.noUser')}
+ |
+
+ )}
+
+
+
+
+
+ )}
+
+ {widget.type === "objects" && (
+
+
+ {t('admin.dashboard.objectsManagement')}
+
+
+
+
+ )}
+
+ {widget.type === "adminobjet" && (
+
+
+ {t('admin.dashboard.objectsList')}
+
+
+ {adminObjects.slice(0, 2).map((obj) => (
+ -
+
{obj.name}
+ {obj.type}
+ {obj.status}
+
+ ))}
+
+
+
+ )}
+
+ {widget.type === "requestObject" && (
+
+
+ {t('admin.dashboard.requestDelete')}
+
+
+
+
+ {t('admin.dashboard.generateReports')}
+
+
+
+
+
+
+ )}
+ {widget.type === "reporting" && (
+
+
+ {t('admin.dashboard.reportsStats')}
+
+
+
+
+ {t('admin.dashboard.generateReports')}
+
+
+
+
+
+
+
+
+
+ {t('admin.dashboard.energyConsumption')}
+
+
+ {t('admin.dashboard.energyConsumptionDesc')}
+
+
+
+
+ {t('admin.dashboard.connectionRate')}
+
+
+ {t('admin.dashboard.connectionRateDesc')}
+
+
+
+
+ {t('admin.dashboard.mostUsedServices')}
+
+
+ - Consultation des données météo
+ - Alertes et suivi de consommation
+ - Ajout d'objets connectés
+
+
+
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ {showAddWidgetModal && (
+
+
+
+ {t('admin.dashboard.chooseWidget')}
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default Dashboard;
diff --git a/z1/Front-end/src/pages/Admin/User.jsx b/z1/Front-end/src/pages/Admin/User.jsx
new file mode 100644
index 0000000..db0631c
--- /dev/null
+++ b/z1/Front-end/src/pages/Admin/User.jsx
@@ -0,0 +1,358 @@
+import React, { useState, useEffect } from "react";
+import Sidebar from "./sidebar.jsx";
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../../config.js";
+import axios from "axios";
+
+const thTd = "p-2 border border-gray-300 text-left";
+const th = `${thTd} bg-gray-100`;
+
+function User() {
+ const { t } = useTranslation();
+ const [users, setUsers] = useState([]);
+ const [logs, setLogs] = useState([]);
+
+ const [name, setname] = useState("");
+ const [surname, setSurname] = useState("");
+ const [pseudo, setPseudo] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [gender, setGender] = useState("Homme");
+
+ const [pointsInput, setPointsInput] = useState({});
+
+ const handleAddUser = (e) => {
+ e.preventDefault();
+ axios
+ .post(`${API_BASE_URL}/signup`, {
+ name,
+ surname,
+ pseudo,
+ email,
+ password,
+ confirmPassword:password,
+ gender,
+ })
+ .then((response) => {
+ logAction(name, "Utilisateur ajouté");
+ alert(t('admin.user.successAdd'));
+ window.location.reload();
+ })
+ .catch((error) => {
+ alert(t('admin.user.errorAdd'));
+ });
+ };
+
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/users`).then((response) => {
+ setUsers(response.data);
+ });
+ }, []);
+
+ const handleDeleteUser = (userId) => {
+ const user = users.find((u) => u.id === userId);
+
+ if (user) {
+ const confirmation = window.confirm(
+ t('admin.user.confirmDelete').replace('{name}', user.name)
+ );
+ if (confirmation) {
+ axios
+ .post(`${API_BASE_URL}/deleteUser`, {
+ id: userId,
+ })
+ .then((response) => {
+ alert(t('admin.user.successDelete'));
+ console.log("L'utilisateur a été supprimé :", response.data);
+ window.location.reload();
+ })
+ .catch((error) => {
+ console.error(
+ "Erreur lors de la suppression de l'utilisateur :",
+ error
+ );
+ });
+
+ logAction(user.name, "Utilisateur supprimé");
+
+ setUsers(users.filter((u) => u.id !== userId));
+ } else {
+ console.log("Suppression annulée");
+ }
+ }
+ };
+
+ const handleChangeAccessLevel = (userId, newLevel) => {
+ setUsers(
+ users.map((user) => {
+ if (user.id === userId && newLevel !== user.role) {
+ const oldLevel = user.role;
+ user.role = newLevel;
+ if (user.role === "user") {
+ user.points = 0;
+ } else if (user.role === "complexe") {
+ user.points = 100;
+ } else if (user.role === "admin") {
+ user.points = 200;
+ }
+ axios
+ .post(`${API_BASE_URL}/setUserPoints`, {
+ id: user.id,
+ points: user.points,
+ })
+ .then((response) => {
+ alert(t('admin.user.successLevel'));
+ console.log("Changement de niveau réussit :", response.data);
+ })
+ .catch((error) => {
+ alert(t('admin.user.errorLevel'));
+ console.error("Erreur lors du changement de niveau :", error);
+ });
+ logAction(
+ user.name,
+ `Niveau d'accès changé de ${oldLevel} à ${newLevel}`
+ );
+ }
+ return user;
+ })
+ );
+ };
+
+ const handleAdjustPoints = (userId) => {
+ const pointsToAdd = parseInt(pointsInput[userId]) || 0;
+ setUsers(
+ users.map((user) => {
+ if (user.id === userId) {
+ user.points = pointsToAdd;
+ axios
+ .post(`${API_BASE_URL}/setUserPoints`, {
+ id: user.id,
+ points: user.points,
+ })
+ .then((response) => {
+ alert(t('admin.user.successPoints'));
+ console.log("Ajout des points réussi :", response.data);
+ })
+ .catch((error) => {
+ alert(t('admin.user.errorPoints'));
+ console.error("Erreur lors de l'ajout des points :", error);
+ });
+ logAction(user.name, `Points ajustés à ${pointsToAdd}`);
+ }
+ return user;
+ })
+ );
+ setPointsInput({ ...pointsInput, [userId]: "" });
+ };
+
+ const logAction = (name, action) => {
+ const timestamp = new Date().toLocaleString();
+ setLogs([...logs, { id: Date.now(), name, action, timestamp }]);
+ };
+
+ const downloadLogs = () => {
+ const logText = logs
+ .map((log) => `${log.timestamp} - ${log.name} - ${log.action}`)
+ .join("\n");
+ const blob = new Blob([logText], { type: "text/plain;charset=utf-8" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = "logs.txt";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ return (
+
+
+
+
+
+ {t('admin.user.title')}
+
+ {t('admin.user.subtitle')}
+
+
+
+
+ {/*Tableau utilisateur*/}
+
+
{t('admin.user.manageTitle')}
+
+
+
+
+
+ {t('admin.user.logsTitle')}
+
+
+
+
+
+ | {t('admin.user.lastName')} |
+ {t('admin.user.action')} |
+ {t('admin.user.timestamp')} |
+
+
+
+ {logs.map((log) => (
+
+ | {log.name} |
+ {log.action} |
+ {log.timestamp} |
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
+export default User;
diff --git a/z1/Front-end/src/pages/Admin/sidebar.jsx b/z1/Front-end/src/pages/Admin/sidebar.jsx
new file mode 100644
index 0000000..885d6e7
--- /dev/null
+++ b/z1/Front-end/src/pages/Admin/sidebar.jsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { Menu, X } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+function Sidebar({ isOpen, toggleSidebar }) {
+ const { t } = useTranslation();
+ return (
+
+ );
+}
+
+export default Sidebar;
diff --git a/z1/Front-end/src/pages/Gestion/AddObject.jsx b/z1/Front-end/src/pages/Gestion/AddObject.jsx
new file mode 100644
index 0000000..f93204d
--- /dev/null
+++ b/z1/Front-end/src/pages/Gestion/AddObject.jsx
@@ -0,0 +1,21 @@
+import React, { useState } from "react";
+import FormNewObject from "../../components/FormNewObject";
+import { useTranslation } from "react-i18next";
+
+function AddObject() {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+ {t('gestion.addObject.title')}
+
+
+
+
+
+ );
+}
+
+export default AddObject;
diff --git a/z1/Front-end/src/pages/Gestion/Gestion.jsx b/z1/Front-end/src/pages/Gestion/Gestion.jsx
new file mode 100644
index 0000000..a378de6
--- /dev/null
+++ b/z1/Front-end/src/pages/Gestion/Gestion.jsx
@@ -0,0 +1,75 @@
+import React from "react";
+import {
+ Search,
+ MapPin,
+ Calendar,
+ Bus,
+ ArrowRight,
+ LogIn,
+ UserPlus,
+ RadioTower,
+ Binoculars,
+ BadgePlus,
+} from "lucide-react";
+import { useAuth } from "../../AuthContext";
+import { useTranslation } from "react-i18next";
+
+function Gestion() {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+ {t('gestion.title')} {t('gestion.moduleName')}.
+
+
+
+ {t('gestion.description')}
+
+
+
+
+
+
+ );
+}
+
+export default Gestion;
diff --git a/z1/Front-end/src/pages/Gestion/ObjectManagement.jsx b/z1/Front-end/src/pages/Gestion/ObjectManagement.jsx
new file mode 100644
index 0000000..e6d4f50
--- /dev/null
+++ b/z1/Front-end/src/pages/Gestion/ObjectManagement.jsx
@@ -0,0 +1,240 @@
+import React from "react";
+import { Search, ArrowRight, RadioTower, Plus, Trash } from "lucide-react";
+import { useEffect, useState } from "react";
+import axios from "axios";
+import { API_BASE_URL } from "../../config";
+import { useAuth } from "../../AuthContext";
+import { useTranslation } from "react-i18next";
+import Alert from "../../components/Alert";
+import AlertInactive from "../../components/AlertInactive";
+
+function ObjectManagement() {
+ const { t } = useTranslation();
+ const { user } = useAuth();
+ const [searchQuery, setSearchQuery] = useState("");
+ const [activeFilter, setActiveFilter] = useState("");
+ const [objects, setObjects] = useState([]);
+ const [nbAffObject, setnbAffObject] = useState(6);
+ const [affAlert, setAffAlert] = useState(false);
+ const [messageAlert, setMessageAlert] = useState("");
+ const [success, setSuccess] = useState(false);
+
+ const filteredDATA = objects.filter((node) => {
+ const matchesSearchQuery =
+ searchQuery === "" ||
+ node.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ node.description.toLowerCase().includes(searchQuery.toLowerCase());
+
+ const matchesTag =
+ activeFilter === "" ||
+ node.name.toLowerCase().includes(activeFilter.toLowerCase()) ||
+ node.description.includes(activeFilter.toLowerCase()) ||
+ (activeFilter === "Active" && node.status.toLowerCase() === "active") ||
+ (activeFilter === "Inactive" && node.status.toLowerCase() === "inactive");
+ return matchesSearchQuery && matchesTag;
+ });
+
+ useEffect(() => {
+ axios.get(`${API_BASE_URL}/objets`).then((response) => {
+ setObjects(response.data);
+ });
+ }, []);
+
+ const handleRequestDeletion = async (objectId) => {
+ console.log("Demande de suppression pour l'objet", objectId);
+
+ try {
+ // Log des données envoyées
+ console.log("Envoi de la requête:", {
+ object_id: objectId,
+ requested_by: user.id,
+ });
+
+ const response = await axios.post(
+ `${API_BASE_URL}/requestDeleteObject`,
+ {
+ object_id: objectId,
+ requested_by: user.id,
+ },
+ {
+ headers: {
+ "Content-Type": "application/json", // Pas d'Authorization
+ },
+ }
+ );
+
+ console.log("Réponse du serveur:", response.data);
+ setMessageAlert(t('gestion.objectManagement.successDeleteReq'));
+ setAffAlert(true);
+ setSuccess(false);
+ } catch (error) {
+ console.error(
+ "Erreur lors de la requête :",
+ error.response?.data || error.message
+ );
+ setMessageAlert(t('gestion.objectManagement.errorDeleteReq'));
+ setAffAlert(true);
+ setSuccess(true);
+ }
+ };
+
+ return (
+
+
+ {success ? (
+
+ ):
+ }
+
+
+
+ {user?.role !== "user" ? t('gestion.objectManagement.titleAdmin') : t('gestion.objectManagement.titleUser')} {" "}
+ {t('gestion.objectManagement.titleSuffix')} {t('gestion.objectManagement.titleObjects')}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+ {/* Filtres responsifs - utilisation de flex-wrap et responsive spacing */}
+
+
+
+
+
+
+
+
+ {/* Grille responsive pour les objets */}
+
+ {filteredDATA.length === 0 ? (
+
{t('gestion.objectManagement.noObjects')}
+ ) : (
+ filteredDATA.slice(0, nbAffObject).map((object) => (
+
+ {object.status === "active" ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+
{object.name}
+
{object.description}
+
+
+ {t('gestion.objectManagement.moreInfo')}
+
+ {user?.role !== "user" && (
+
+ )}
+
+
+ ))
+ )}
+
+ {nbAffObject < filteredDATA.length && (
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default ObjectManagement;
diff --git a/z1/Front-end/src/pages/Gestion/Objet.jsx b/z1/Front-end/src/pages/Gestion/Objet.jsx
new file mode 100644
index 0000000..825853c
--- /dev/null
+++ b/z1/Front-end/src/pages/Gestion/Objet.jsx
@@ -0,0 +1,112 @@
+import React from "react";
+import { Thermometer, CircleGauge, Droplet } from "lucide-react";
+
+import { useEffect, useState, useRef } from "react";
+import axios from "axios";
+import { API_BASE_URL } from "../../config";
+
+import InfoObjet from "../../components/InfoObject";
+import ModifObject from "../../components/ModifObject";
+import WindGraph from "../../components/WindGraph";
+import WindInfo from "../../components/WindInfo";
+import MeteoInfos from "../../components/MeteoInfos";
+import MeteoGraph from "../../components/MeteoGraph";
+import BatterieInfo from "../../components/BatterieInfo";
+import { useAuth } from "../../AuthContext";
+import { useTranslation } from "react-i18next";
+import UserInfosObject from "../../components/UserInfosObject";
+function Objet() {
+ const { t } = useTranslation();
+ const {user} =useAuth();
+ const identifiant = new URLSearchParams(window.location.search).get("id");
+ const [object, setObject] = useState({});
+ const [graphStates, setGraphStates] = useState({
+ wind: false,
+ temperature: false,
+ pressure: false,
+ humidity: false,
+ });
+ const [afficherModif, defafficherModif] = useState(false);
+ const graphRefs = {
+ temperature: useRef(null),
+ pressure: useRef(null),
+ humidity: useRef(null),
+ wind: useRef(null),
+ };
+ useEffect(() => {
+ axios
+ .post(`${API_BASE_URL}/objet`, {
+ id: identifiant,
+ userId:user.id,
+ shouldUpdatePoints:true,
+ })
+ .then((response) => {
+ setObject(response.data[0]);
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération :", error);
+ });
+ }, [user]);
+ return object && object.id ? (
+
+
+
+
+ {t('gestion.objet.dashboardTitle')} {object.name}
+
+
+
+ {!afficherModif ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {graphStates.wind &&
}
+ {graphStates.temperature && (
+
+ )}
+ {graphStates.pressure && (
+
+ )}
+ {graphStates.humidity && (
+
+ )}
+
+
+ ) : (
+ {t('gestion.objet.errorFetch')}
+ );
+}
+export default Objet;
diff --git a/z1/Front-end/src/pages/Home.jsx b/z1/Front-end/src/pages/Home.jsx
new file mode 100644
index 0000000..0aad5a3
--- /dev/null
+++ b/z1/Front-end/src/pages/Home.jsx
@@ -0,0 +1,903 @@
+import React, { useState, useEffect } from "react";
+import {
+ UserPlus,
+ LogIn,
+ BadgePlus,
+ RadioTower,
+ Search,
+ Cloud,
+ CloudRain,
+ Droplets,
+ Wind,
+ Thermometer,
+ Sun,
+ Moon,
+ MapPin,
+ CalendarClock,
+ ArrowRight,
+ BellRing,
+ LayoutDashboard,
+ TowerControl,
+} from "lucide-react";
+import { useAuth } from "../AuthContext";
+import axios from "axios";
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../config";
+
+function EnhancedWeatherHome() {
+ const { t } = useTranslation();
+ const [searchQuery, setSearchQuery] = useState("");
+ const [locations, setLocations] = useState([]);
+ const [infoMeteo, setInfoMeteo] = useState([]);
+ const [activeFilter, setActiveFilter] = useState("all");
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const { user, token } = useAuth();
+ const [ville, setVille] = useState("Paris, France");
+ const heure = currentTime.getHours();
+ const isDayTime = heure > 6 && heure < 20;
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 60000);
+
+ return () => clearInterval(timer);
+ }, []);
+ useEffect(() => {
+ axios
+ .post(`${API_BASE_URL}/getMeteoHome`, {
+ location: ville,
+ })
+ .then((response) => {
+ if (response.data.length === 0) {
+ console.warn("Aucun infos disponible.");
+ } else {
+ console.log(response.data[0]);
+ setInfoMeteo(response.data[0]);
+ }
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération des infos :", error);
+ });
+ }, [ville]);
+ useEffect(() => {
+ axios
+ .get(`${API_BASE_URL}/getLocations`)
+ .then((response) => {
+ if (response.data.length === 0) {
+ console.warn("Aucun lieu disponible.");
+ } else {
+ console.log(response.data);
+ setLocations(response.data);
+ }
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération des catégories :", error);
+ });
+ }, []);
+ const filteredLocations = locations.filter((loc) =>
+ loc.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ const formatDate = (date) => {
+ const options = { weekday: "long", day: "numeric", month: "long" };
+ return date.toLocaleDateString("fr-FR", options);
+ };
+
+ const formatHeure = (h) => {
+ const heureFormatee = (h % 24).toString().padStart(2, "0");
+ return `${heureFormatee}:00`;
+ };
+ const hourlyForecast = [
+ {
+ time: "Maintenant",
+ temp: "22°",
+ icon: ,
+ },
+ {
+ time: formatHeure(heure + 1),
+ temp: "22°",
+ icon: ,
+ },
+ {
+ time: formatHeure(heure + 2),
+ temp: "22°",
+ icon: ,
+ },
+ {
+ time: formatHeure(heure + 3),
+ temp: "21°",
+ icon: ,
+ },
+ {
+ time: formatHeure(heure + 4),
+ temp: "20°",
+ icon: ,
+ },
+ {
+ time: formatHeure(heure + 5),
+ temp: "19°",
+ icon: ,
+ },
+ ];
+
+ const dailyForecast = [
+ {
+ day: "Lun",
+ temp: "21°/15°",
+ icon: ,
+ },
+ {
+ day: "Mar",
+ temp: "23°/16°",
+ icon: ,
+ },
+ {
+ day: "Mer",
+ temp: "24°/17°",
+ icon: ,
+ },
+ {
+ day: "Jeu",
+ temp: "22°/16°",
+ icon: ,
+ },
+ {
+ day: "Ven",
+ temp: "20°/14°",
+ icon: ,
+ },
+ ];
+
+ return (
+
+ {" "}
+
+
+
+ {isDayTime ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ {t('home.title')}
+
+
+ {t('home.subtitle')}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+ {searchQuery.length > 0 && (
+
+ {filteredLocations.length > 0 ? (
+ filteredLocations.map((loc, idx) => (
+ - {
+ setVille(loc);
+ setSearchQuery("");
+ }}
+ >
+ {loc}
+
+ ))
+ ) : (
+ -
+ {t('home.noCityFound')}
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {ville}
+
+
+
+ {formatDate(currentTime)}
+
+
+ {isDayTime ? (
+
+ ) : (
+
+ )}
+
+
+ {infoMeteo.temperature}°C
+
+
+ {t('home.partlyCloudy')}
+
+
+
+
+
+
+
+
+ {t('home.feelsLike')}
+
+
+
+
+ {infoMeteo.temperature - 2}°C
+
+
+
+
+
+ {t('home.humidity')}
+
+
+
+
+ {infoMeteo.humidity}%
+
+
+
+
+
+ {t('home.wind')}
+
+
+
+
+ {infoMeteo.wind_speed} km/h
+
+
+
+
+
+ {t('home.rain')}
+
+
+
+
+ 30%
+
+
+
+
+
+ {t('home.sunrise')}
+
+
+
+
+ 06:42
+
+
+
+
+
+ {t('home.sunset')}
+
+
+
+
+ 20:51
+
+
+
+
+
+
+
+
+
+ {t('home.hourlyForecast')}
+
+
+ {hourlyForecast.map((item, index) => (
+
+
+ {item.time}
+
+ {item.icon}
+
+ {item.temp}
+
+
+ ))}
+
+
+
+
+
+
+
+ {t('home.dailyForecast')}
+
+
+ {dailyForecast.map((item, index) => (
+
+
+
+ {item.day}
+
+ {item.icon}
+
+
+ {item.temp}
+
+
+ ))}
+
+
+
+
+
+ {t('home.weatherServices')}
+
+ {user?.role === "user" && (
+
+ )}
+
+ {!token && (
+
+ )}
+ {user?.role === "complexe" && (
+
+ )}
+ {user?.role === "admin" && (
+
+ )}
+
+
+
+ );
+}
+
+export default EnhancedWeatherHome;
diff --git a/z1/Front-end/src/pages/Login.jsx b/z1/Front-end/src/pages/Login.jsx
new file mode 100644
index 0000000..2f2459e
--- /dev/null
+++ b/z1/Front-end/src/pages/Login.jsx
@@ -0,0 +1,216 @@
+import React, { useState } from "react";
+import { Mail, Lock, AlertCircle, CheckCircle, Info, X } from "lucide-react";
+import { useNavigate, Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import axios from "axios";
+import { useAuth } from "../AuthContext";
+import { API_BASE_URL } from "../config";
+
+function Login() {
+ const { t } = useTranslation();
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+ const [alert, setAlert] = useState({
+ show: false,
+ type: "", // 'success', 'error', 'info', 'warning'
+ message: "",
+ });
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ if (alert.show) setAlert({ ...alert, show: false });
+ };
+
+ const showAlert = (type, message) => {
+ setAlert({
+ show: true,
+ type,
+ message,
+ });
+
+ // Auto-hide success and info alerts after 5 seconds
+ if (type === 'success' || type === 'info') {
+ setTimeout(() => {
+ setAlert(prev => ({ ...prev, show: false }));
+ }, 5000);
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setAlert({ show: false, type: "", message: "" });
+
+ try {
+ // Afficher un message de chargement
+ showAlert("info", t('auth.login.loading'));
+
+ const response = await axios.post(`${API_BASE_URL}/login`, formData, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const data = response.data;
+
+ if (data.token) {
+ showAlert("success", t('auth.login.success'));
+ login(data.token);
+
+ // Court délai pour montrer le message de succès avant la redirection
+ setTimeout(() => {
+ navigate("/");
+ }, 1000);
+ } else {
+ showAlert("error", t('auth.login.missingToken'));
+ }
+ } catch (error) {
+ console.error("Erreur lors de la connexion", error);
+
+ if (error.response) {
+ if (error.response.status === 401) {
+ showAlert("error", t('auth.login.incorrectAuth'));
+ } else if (error.response.status === 422) {
+ showAlert("error", t('auth.login.invalidData'));
+ } else if (error.response.status >= 500) {
+ showAlert("error", t('auth.login.serverError'));
+ } else {
+ showAlert("error", error.response.data.message || t('auth.login.genericError'));
+ }
+ } else if (error.request) {
+ showAlert("error", t('auth.login.networkError'));
+ } else {
+ showAlert("error", t('auth.login.genericError'));
+ }
+ }
+ };
+
+ // Configuration des alertes selon le type
+ const alertConfig = {
+ success: {
+ bgColor: "bg-green-50",
+ borderColor: "border-green-200",
+ textColor: "text-green-700",
+ icon:
+ },
+ error: {
+ bgColor: "bg-red-50",
+ borderColor: "border-red-200",
+ textColor: "text-red-700",
+ icon:
+ },
+ info: {
+ bgColor: "bg-blue-50",
+ borderColor: "border-blue-200",
+ textColor: "text-blue-700",
+ icon:
+ },
+ warning: {
+ bgColor: "bg-yellow-50",
+ borderColor: "border-yellow-200",
+ textColor: "text-yellow-700",
+ icon:
+ }
+ };
+
+ return (
+
+
+
+ {t('auth.login.title')}
+
+
+ {/* Système d'alertes */}
+ {alert.show && (
+
+
+ {alertConfig[alert.type].icon}
+ {alert.message}
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/z1/Front-end/src/pages/Profil.jsx b/z1/Front-end/src/pages/Profil.jsx
new file mode 100644
index 0000000..03e9b54
--- /dev/null
+++ b/z1/Front-end/src/pages/Profil.jsx
@@ -0,0 +1,330 @@
+import React, { useState, useEffect } from 'react';
+import { Mail, User, Lock, Edit, Save } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../config";
+import { useAuth } from "../AuthContext";
+import axios from "axios";
+
+function Profil() {
+ const { t } = useTranslation();
+ const [userData, setUserData] = useState({});
+
+ const { user } = useAuth();
+ useEffect(() => {
+ if (user) {
+ console.log("user.role:", user.id);
+ }
+ }, [user]);
+ const [formData, setFormData] = useState({
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ const [editMode, setEditMode] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [successMessage, setSuccessMessage] = useState('');
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ axios
+ .post(`${API_BASE_URL}/user`, {
+ id: user.id,
+ })
+ .then((response) => {
+ setUserData(response.data);
+ console.log("Infos récupérées :", response.data);
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la récupération :", error);
+ });
+ }, [user]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleProfileChange = (e) => {
+ const { name, value } = e.target;
+ setUserData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setErrorMessage('');
+ setSuccessMessage('');
+
+ if (formData.newPassword !== formData.confirmPassword) {
+ setErrorMessage(t('profile.errorMismatch'));
+ return;
+ }
+
+ try {
+ axios
+ .post(`${API_BASE_URL}/changePassword`, {
+ id: userData.id,
+ oldPassword: formData.oldPassword,
+ newPassword: formData.newPassword
+ })
+ .then((response) => {
+ console.log("Modification du mot de passe réussie :", response.data);
+ setSuccessMessage(t('profile.successPass'));
+ setFormData({
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+ })
+ .catch((error) => {
+ console.error("Erreur lors de la modification du mot de passe :", error);
+ setErrorMessage(error.response?.data?.error || t('profile.errorGeneric'));
+ });
+
+ setSuccessMessage(t('profile.successPass'));
+ setFormData({
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+ } catch (error) {
+ setErrorMessage(error.message || t('profile.errorGeneric'));
+ }
+ };
+
+ const handleProfileSubmit = async (e) => {
+ e.preventDefault();
+ setErrorMessage('');
+ setSuccessMessage('');
+
+ axios
+ .post(`${API_BASE_URL}/updateProfil`, {
+ id: userData.id,
+ name: userData.name,
+ surname: userData.surname,
+ pseudo:userData.pseudo,
+ email: userData.email
+ })
+
+ .catch((error) => {
+ console.error("Erreur lors de la mise à jour du profil :", error);
+ setErrorMessage(error.response?.data?.error || t('profile.errorGeneric'));
+ })
+ .then((response) => {
+ console.log("Mise à jour du profil réussie :", response.data);
+ setSuccessMessage(t('profile.successUpdate'));
+ setEditMode(false);
+ });
+ };
+
+ return (
+
+
+
{t('profile.title')}
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ {successMessage && (
+
+ {successMessage}
+
+ )}
+
+
+ {/* Informations du profil */}
+
+
+
{t('profile.personalInfo')}
+
+
+
+
+
+
+ {/* Changement de mot de passe */}
+
+
{t('profile.changePasswordTitle')}
+
+
+
+
+
+ );
+}
+
+export default Profil;
\ No newline at end of file
diff --git a/z1/Front-end/src/pages/Signup.jsx b/z1/Front-end/src/pages/Signup.jsx
new file mode 100644
index 0000000..7f3bb14
--- /dev/null
+++ b/z1/Front-end/src/pages/Signup.jsx
@@ -0,0 +1,239 @@
+import React, { useState } from 'react';
+import { Mail, User, Lock } from 'lucide-react';
+import { useNavigate, Link} from 'react-router-dom';
+import { useTranslation } from "react-i18next";
+import { API_BASE_URL } from "../config.js";
+
+function Signup() {
+ const { t } = useTranslation();
+ const [formData, setFormData] = useState({
+ name: '',
+ surname: '',
+ pseudo:'',
+ email: '',
+ gender: '',
+ password: '',
+ confirmPassword: ''
+ });
+ const navigate = useNavigate();
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (formData.password !== formData.confirmPassword) {
+ alert(t('auth.signup.passNoMatch'));
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/signup`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || t('auth.signup.error'));
+ }
+
+ alert(t('auth.signup.success'));
+
+
+ navigate("/");
+ } catch (error) {
+ alert(error.message);
+ }
+ };
+
+ return (
+
+
+
{t('auth.signup.title')}
+
+
+
+
+ );
+}
+
+export default Signup;
diff --git a/z1/Front-end/tailwind.config.js b/z1/Front-end/tailwind.config.js
new file mode 100644
index 0000000..d21f1cd
--- /dev/null
+++ b/z1/Front-end/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/z1/Front-end/vite.config.js b/z1/Front-end/vite.config.js
new file mode 100644
index 0000000..371f9a8
--- /dev/null
+++ b/z1/Front-end/vite.config.js
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ hmr: true, // Hot Module Replacement pour éviter les rechargements complets
+ },
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('node_modules')) {
+ return 'vendor'; // Sépare les gros modules pour un meilleur chargement
+ }
+ },
+ },
+ },
+ },
+ optimizeDeps: {
+ include: ['lucide-react'], // Réintègre Lucide pour éviter les blocages
+ },
+});
diff --git a/z1/docker-compose.yaml b/z1/docker-compose.yaml
new file mode 100644
index 0000000..18b5dae
--- /dev/null
+++ b/z1/docker-compose.yaml
@@ -0,0 +1,52 @@
+version: '3.8'
+
+services:
+ db:
+ image: postgres:17-alpine
+ container_name: vigimeteo_db
+ restart: always
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: admin
+ POSTGRES_DB: postgres
+ volumes:
+ - vigimeteo_data:/var/lib/postgresql/data
+ - ./sql/init_db.sql:/docker-entrypoint-initdb.d/init.sql #DB initializes automatically on first run
+ networks:
+ - vigimeteo_net
+
+ backend:
+ build:
+ context: ./Back-end
+ container_name: vigimeteo_backend
+ restart: always
+ environment:
+ DB_HOST: db
+ DB_PORT: 5432
+ DB_NAME: postgres
+ DB_USER: postgres
+ DB_PASSWORD: admin
+ ports:
+ - "8888:8888"
+ depends_on:
+ - db
+ networks:
+ - vigimeteo_net
+
+ frontend:
+ build:
+ context: ./Front-end
+ container_name: vigimeteo_frontend
+ restart: always
+ ports:
+ - "5000:80"
+ depends_on:
+ - backend
+ networks:
+ - vigimeteo_net
+
+volumes:
+ vigimeteo_data:
+
+networks:
+ vigimeteo_net:
diff --git a/z1/prepare-app.sh b/z1/prepare-app.sh
new file mode 100755
index 0000000..32de8d8
--- /dev/null
+++ b/z1/prepare-app.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+echo "Preparing app..."
+docker-compose build
+echo "App is prepared."
diff --git a/z1/remove-app.sh b/z1/remove-app.sh
new file mode 100755
index 0000000..4e4ead9
--- /dev/null
+++ b/z1/remove-app.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+echo "Removed app."
+docker-compose down -v --rmi local
diff --git a/z1/sql/init_db.sql b/z1/sql/init_db.sql
new file mode 100644
index 0000000..24f58e0
--- /dev/null
+++ b/z1/sql/init_db.sql
@@ -0,0 +1,602 @@
+--
+-- PostgreSQL database dump
+--
+
+-- Dumped from database version 17.4
+-- Dumped by pg_dump version 17.4
+
+-- Started on 2025-04-13 23:02:35
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET transaction_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+SET default_tablespace = '';
+
+SET default_table_access_method = heap;
+
+--
+-- TOC entry 226 (class 1259 OID 32826)
+-- Name: categories; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.categories (
+ id integer NOT NULL,
+ name text NOT NULL
+);
+
+
+ALTER TABLE public.categories OWNER TO postgres;
+
+--
+-- TOC entry 225 (class 1259 OID 32825)
+-- Name: categories_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.categories_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.categories_id_seq OWNER TO postgres;
+
+--
+-- TOC entry 4963 (class 0 OID 0)
+-- Dependencies: 225
+-- Name: categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
+--
+
+ALTER SEQUENCE public.categories_id_seq OWNED BY public.categories.id;
+
+
+--
+-- TOC entry 228 (class 1259 OID 32925)
+-- Name: deletion_requests; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.deletion_requests (
+ id integer NOT NULL,
+ object_id integer NOT NULL,
+ requested_by integer NOT NULL,
+ request_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+ status character varying(50) DEFAULT 'pending'::character varying
+);
+
+
+ALTER TABLE public.deletion_requests OWNER TO postgres;
+
+--
+-- TOC entry 227 (class 1259 OID 32924)
+-- Name: deletion_requests_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.deletion_requests_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.deletion_requests_id_seq OWNER TO postgres;
+
+--
+-- TOC entry 4964 (class 0 OID 0)
+-- Dependencies: 227
+-- Name: deletion_requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
+--
+
+ALTER SEQUENCE public.deletion_requests_id_seq OWNED BY public.deletion_requests.id;
+
+
+--
+-- TOC entry 222 (class 1259 OID 32768)
+-- Name: range_data; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.range_data (
+ station_id integer NOT NULL,
+ temperature_min numeric,
+ temperature_max numeric,
+ pressure_min numeric,
+ pressure_max numeric,
+ humidity_min numeric,
+ humidity_max numeric
+);
+
+
+ALTER TABLE public.range_data OWNER TO postgres;
+
+--
+-- TOC entry 224 (class 1259 OID 32789)
+-- Name: users; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.users (
+ id integer NOT NULL,
+ name character varying(100) NOT NULL,
+ surname character varying(100) NOT NULL,
+ email character varying(255) NOT NULL,
+ gender character varying(10) NOT NULL,
+ password character varying(255) NOT NULL,
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ points integer DEFAULT 0 NOT NULL,
+ pseudo character varying(100)
+);
+
+
+ALTER TABLE public.users OWNER TO postgres;
+
+--
+-- TOC entry 223 (class 1259 OID 32788)
+-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.users_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.users_id_seq OWNER TO postgres;
+
+--
+-- TOC entry 4965 (class 0 OID 0)
+-- Dependencies: 223
+-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
+--
+
+ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
+
+
+--
+-- TOC entry 221 (class 1259 OID 16479)
+-- Name: weather_data; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.weather_data (
+ id integer NOT NULL,
+ station_id integer NOT NULL,
+ temperature numeric(5,2),
+ humidity numeric(5,2),
+ pressure numeric(7,2),
+ wind_speed numeric(5,2),
+ wind_direction character varying(50),
+ "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP
+);
+
+
+ALTER TABLE public.weather_data OWNER TO postgres;
+
+--
+-- TOC entry 220 (class 1259 OID 16478)
+-- Name: weather_data_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.weather_data_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.weather_data_id_seq OWNER TO postgres;
+
+--
+-- TOC entry 4966 (class 0 OID 0)
+-- Dependencies: 220
+-- Name: weather_data_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
+--
+
+ALTER SEQUENCE public.weather_data_id_seq OWNED BY public.weather_data.id;
+
+
+--
+-- TOC entry 219 (class 1259 OID 16468)
+-- Name: weather_objects; Type: TABLE; Schema: public; Owner: postgres
+--
+
+CREATE TABLE public.weather_objects (
+ id integer NOT NULL,
+ name character varying(500) NOT NULL,
+ description text,
+ type character varying(100) NOT NULL,
+ location character varying(255),
+ last_update timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+ status character varying(50) DEFAULT 'active'::character varying,
+ batterie integer DEFAULT 100,
+ type_batterie character varying(50),
+ proprio_id integer,
+ CONSTRAINT weather_objects_batterie_check CHECK (((batterie >= 0) AND (batterie <= 100)))
+);
+
+
+ALTER TABLE public.weather_objects OWNER TO postgres;
+
+--
+-- TOC entry 217 (class 1259 OID 16466)
+-- Name: weather_objects_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.weather_objects_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.weather_objects_id_seq OWNER TO postgres;
+
+--
+-- TOC entry 218 (class 1259 OID 16467)
+-- Name: weather_objects_id_seq1; Type: SEQUENCE; Schema: public; Owner: postgres
+--
+
+CREATE SEQUENCE public.weather_objects_id_seq1
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.weather_objects_id_seq1 OWNER TO postgres;
+
+--
+-- TOC entry 4967 (class 0 OID 0)
+-- Dependencies: 218
+-- Name: weather_objects_id_seq1; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
+--
+
+ALTER SEQUENCE public.weather_objects_id_seq1 OWNED BY public.weather_objects.id;
+
+
+--
+-- TOC entry 4776 (class 2604 OID 32829)
+-- Name: categories id; Type: DEFAULT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.categories_id_seq'::regclass);
+
+
+--
+-- TOC entry 4777 (class 2604 OID 32928)
+-- Name: deletion_requests id; Type: DEFAULT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.deletion_requests ALTER COLUMN id SET DEFAULT nextval('public.deletion_requests_id_seq'::regclass);
+
+
+--
+-- TOC entry 4773 (class 2604 OID 32792)
+-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
+
+
+--
+-- TOC entry 4771 (class 2604 OID 16482)
+-- Name: weather_data id; Type: DEFAULT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_data ALTER COLUMN id SET DEFAULT nextval('public.weather_data_id_seq'::regclass);
+
+
+--
+-- TOC entry 4767 (class 2604 OID 16471)
+-- Name: weather_objects id; Type: DEFAULT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_objects ALTER COLUMN id SET DEFAULT nextval('public.weather_objects_id_seq1'::regclass);
+
+
+--
+-- TOC entry 4955 (class 0 OID 32826)
+-- Dependencies: 226
+-- Data for Name: categories; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.categories (id, name) FROM stdin;
+1 station
+2 capteur
+\.
+
+
+--
+-- TOC entry 4957 (class 0 OID 32925)
+-- Dependencies: 228
+-- Data for Name: deletion_requests; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.deletion_requests (id, object_id, requested_by, request_date, status) FROM stdin;
+\.
+
+
+--
+-- TOC entry 4951 (class 0 OID 32768)
+-- Dependencies: 222
+-- Data for Name: range_data; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.range_data (station_id, temperature_min, temperature_max, pressure_min, pressure_max, humidity_min, humidity_max) FROM stdin;
+1 -33 33 980 1040 30 84
+3 -15 39 980 1040 30 90
+9 -15 49 980 1040 30 90
+2 -15 50 980 1040 30 90
+4 -15 50 980 1040 30 90
+5 -15 50 980 1040 30 90
+6 -15 50 980 1040 30 90
+7 -15 50 980 1040 30 90
+8 -15 50 980 1040 30 90
+10 -15 50 980 1040 30 90
+\.
+
+
+--
+-- TOC entry 4953 (class 0 OID 32789)
+-- Dependencies: 224
+-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.users (id, name, surname, email, gender, password, created_at, points, pseudo) FROM stdin;
+9 complexe complexe complexe@gmail.com homme $2a$12$LC/9EhIC9z/5IF8y/SjFVuDWqeQbkkafhRtytNJ9VWIvx6lCgHDfq 2025-04-12 13:10:50.562087 100 complexe
+7 admin admin admin@a.com homme $2a$12$cugJ4JNxHjL.GE0ONZlkVerXRlKGc3jtVNlo9qQrck1Kahgnz6Fj2 2025-04-11 21:08:47.705738 247 admin
+10 user user user@gmail.com homme $2a$12$wja3M3Lc254Ooge7mE5hwuzHEP35YbVzMYYH6WXs5sKc2q4fvlBei 2025-04-12 14:18:22.728679 0 user
+11 admin super admin.a@gmail.com homme $2a$12$LC/9EhIC9z/5IF8y/SjFVuDWqeQbkkafhRtytNJ9VWIvx6lCgHDfq 2025-04-13 12:00:00.000000 999999 admin
+\.
+
+
+--
+-- TOC entry 4950 (class 0 OID 16479)
+-- Dependencies: 221
+-- Data for Name: weather_data; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.weather_data (id, station_id, temperature, humidity, pressure, wind_speed, wind_direction, "timestamp") FROM stdin;
+1 1 21.50 60.20 1013.10 5.40 Nord-Ouest 2025-03-29 18:47:46.685241
+2 2 22.30 55.00 1012.50 3.20 Sud-Ouest 2025-03-29 18:47:46.685241
+3 3 24.10 50.00 1010.80 6.00 Est 2025-03-29 18:47:46.685241
+4 1 19.80 65.40 1014.00 4.50 Ouest 2025-03-29 18:47:46.685241
+5 2 20.60 59.30 1013.50 2.80 Nord 2025-03-29 18:47:46.685241
+6 1 22.50 60.00 1012.50 12.30 Nord-Ouest 2025-03-29 08:00:00
+7 1 23.00 65.00 1013.25 14.00 Ouest 2025-03-29 09:00:00
+8 1 24.00 70.00 1014.75 15.20 Nord 2025-03-29 10:00:00
+9 2 21.50 55.00 1011.30 11.00 Sud 2025-03-29 08:30:00
+10 2 22.00 60.00 1012.80 13.00 Est 2025-03-29 09:30:00
+11 2 23.50 63.00 1013.50 14.50 Sud-Est 2025-03-29 10:30:00
+12 3 26.00 58.00 1012.90 17.00 Ouest 2025-03-29 11:00:00
+13 3 27.00 60.00 1014.00 18.50 Nord-Ouest 2025-03-29 12:00:00
+14 3 28.00 62.00 1015.10 16.00 Nord 2025-03-29 13:00:00
+15 4 19.50 75.00 1010.00 9.50 Sud-Ouest 2025-03-29 08:00:00
+16 4 20.00 80.00 1010.50 10.00 Sud 2025-03-29 09:00:00
+17 4 21.50 85.00 1011.00 11.50 Est 2025-03-29 10:00:00
+18 5 18.00 90.00 1010.70 8.00 Ouest 2025-03-29 08:30:00
+19 5 18.50 92.00 1011.20 7.00 Nord-Ouest 2025-03-29 09:30:00
+20 5 19.00 95.00 1011.80 6.50 Nord 2025-03-29 10:30:00
+21 6 24.50 65.00 1013.90 13.00 Sud 2025-03-29 11:00:00
+22 6 25.00 66.00 1014.20 14.50 Ouest 2025-03-29 12:00:00
+23 6 26.50 68.00 1015.50 16.00 Sud-Ouest 2025-03-29 13:00:00
+24 7 21.00 60.00 1012.50 11.50 Est 2025-03-29 08:00:00
+25 7 22.50 62.00 1013.00 12.00 Nord-Ouest 2025-03-29 09:00:00
+26 7 23.00 64.00 1013.75 13.50 Sud-Est 2025-03-29 10:00:00
+27 8 25.00 58.00 1012.10 16.50 Nord 2025-03-29 08:30:00
+28 8 26.00 60.00 1013.30 17.50 Ouest 2025-03-29 09:30:00
+29 8 27.00 62.00 1014.50 18.00 Sud-Ouest 2025-03-29 10:30:00
+30 9 22.00 67.00 1011.40 14.00 Est 2025-03-29 11:00:00
+31 9 23.00 69.00 1012.60 15.00 Nord-Ouest 2025-03-29 12:00:00
+32 9 24.00 72.00 1013.80 16.00 Nord 2025-03-29 13:00:00
+33 10 18.00 55.00 1010.20 10.00 Ouest 2025-03-29 08:00:00
+34 10 19.00 58.00 1011.00 11.50 Sud-Ouest 2025-03-29 09:00:00
+35 10 20.00 60.00 1011.70 12.50 Est 2025-03-29 10:00:00
+\.
+
+
+--
+-- TOC entry 4948 (class 0 OID 16468)
+-- Dependencies: 219
+-- Data for Name: weather_objects; Type: TABLE DATA; Schema: public; Owner: postgres
+--
+
+COPY public.weather_objects (id, name, description, type, location, last_update, status, batterie, type_batterie, proprio_id) FROM stdin;
+3 Station Marseille Station météo située à Marseille, France. Mesures de température, humidité, pression et vent. station Marseille, France 2025-03-30 17:01:10.631653 inactive 100 solaire 7
+4 Capteur Bordeaux Capteur de température et d'humidité à Bordeaux. capteur Bordeaux, France 2025-03-30 17:53:01.42853 active 100 solaire 7
+5 Capteur Lille Capteur de pression atmosphérique à Lille. capteur Lille, France 2025-03-31 21:32:04.955306 inactive 100 solaire 7
+6 Capteur Nantes Capteur de vent à Nantes. capteur Nantes, France 2025-03-30 20:10:18.547523 active 100 solaire 7
+7 Station Toulouse Station météo à Toulouse mesurant la température, l'humidité, la pression et la vitesse du vent. station Toulouse, France 2025-04-02 15:43:34.803703 active 100 solaire 7
+10 Capteur Paris Sud Capteur de température et humidité à Paris Sud. capteur Paris, France 2025-04-02 23:09:38.725522 inactive 100 solaire 7
+8 Capteur Grenoble Capteur de température à Grenoble. capteur Grenoble, France 2025-04-04 10:40:08.247433 active 100 solaire 7
+1 Station Paris Station météo située à Paris, France. Mesures de température, humidité, pression et vent. station Paris, France 2025-04-11 10:40:57.350173 active 100 solaire 7
+2 Station Lyon Station météo située à Lyon, France. Mesures de température, humidité, pression et vent. station Lyon, France 2025-04-11 23:08:56.344369 inactive 100 solaire 7
+9 Station Nice Station météo située à Nice, France. Elle mesure la température, l'humidité et la pression. station Nice, France 2025-04-13 19:26:43.601141 active 100 solaire 7
+\.
+
+
+--
+-- TOC entry 4968 (class 0 OID 0)
+-- Dependencies: 225
+-- Name: categories_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.categories_id_seq', 9, true);
+
+
+--
+-- TOC entry 4969 (class 0 OID 0)
+-- Dependencies: 227
+-- Name: deletion_requests_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.deletion_requests_id_seq', 31, true);
+
+
+--
+-- TOC entry 4970 (class 0 OID 0)
+-- Dependencies: 223
+-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.users_id_seq', 14, true);
+
+
+--
+-- TOC entry 4971 (class 0 OID 0)
+-- Dependencies: 220
+-- Name: weather_data_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.weather_data_id_seq', 35, true);
+
+
+--
+-- TOC entry 4972 (class 0 OID 0)
+-- Dependencies: 217
+-- Name: weather_objects_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.weather_objects_id_seq', 1, false);
+
+
+--
+-- TOC entry 4973 (class 0 OID 0)
+-- Dependencies: 218
+-- Name: weather_objects_id_seq1; Type: SEQUENCE SET; Schema: public; Owner: postgres
+--
+
+SELECT pg_catalog.setval('public.weather_objects_id_seq1', 44, true);
+
+
+--
+-- TOC entry 4792 (class 2606 OID 32835)
+-- Name: categories categories_name_key; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.categories
+ ADD CONSTRAINT categories_name_key UNIQUE (name);
+
+
+--
+-- TOC entry 4794 (class 2606 OID 32833)
+-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.categories
+ ADD CONSTRAINT categories_pkey PRIMARY KEY (id);
+
+
+--
+-- TOC entry 4796 (class 2606 OID 32932)
+-- Name: deletion_requests deletion_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.deletion_requests
+ ADD CONSTRAINT deletion_requests_pkey PRIMARY KEY (id);
+
+
+--
+-- TOC entry 4786 (class 2606 OID 32774)
+-- Name: range_data station_meteo_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.range_data
+ ADD CONSTRAINT station_meteo_pkey PRIMARY KEY (station_id);
+
+
+--
+-- TOC entry 4788 (class 2606 OID 32799)
+-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT users_email_key UNIQUE (email);
+
+
+--
+-- TOC entry 4790 (class 2606 OID 32797)
+-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+
+--
+-- TOC entry 4784 (class 2606 OID 16485)
+-- Name: weather_data weather_data_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_data
+ ADD CONSTRAINT weather_data_pkey PRIMARY KEY (id);
+
+
+--
+-- TOC entry 4782 (class 2606 OID 16477)
+-- Name: weather_objects weather_objects_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_objects
+ ADD CONSTRAINT weather_objects_pkey PRIMARY KEY (id);
+
+
+--
+-- TOC entry 4799 (class 2606 OID 32933)
+-- Name: deletion_requests deletion_requests_object_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.deletion_requests
+ ADD CONSTRAINT deletion_requests_object_id_fkey FOREIGN KEY (object_id) REFERENCES public.weather_objects(id) ON DELETE CASCADE;
+
+
+--
+-- TOC entry 4800 (class 2606 OID 32938)
+-- Name: deletion_requests deletion_requests_requested_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.deletion_requests
+ ADD CONSTRAINT deletion_requests_requested_by_fkey FOREIGN KEY (requested_by) REFERENCES public.users(id) ON DELETE CASCADE;
+
+
+--
+-- TOC entry 4797 (class 2606 OID 32820)
+-- Name: weather_objects fk_proprio; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_objects
+ ADD CONSTRAINT fk_proprio FOREIGN KEY (proprio_id) REFERENCES public.users(id) ON DELETE CASCADE;
+
+
+--
+-- TOC entry 4798 (class 2606 OID 32836)
+-- Name: weather_data weather_data_station_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
+--
+
+ALTER TABLE ONLY public.weather_data
+ ADD CONSTRAINT weather_data_station_id_fkey FOREIGN KEY (station_id) REFERENCES public.weather_objects(id) ON DELETE CASCADE;
+
+
+-- Completed on 2025-04-13 23:02:36
+
+--
+-- PostgreSQL database dump complete
+--
+
diff --git a/z1/start-app.sh b/z1/start-app.sh
new file mode 100755
index 0000000..9c12108
--- /dev/null
+++ b/z1/start-app.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+echo "Running app ..."
+docker-compose up -d
+echo "The app is available at http://localhost:5000"
diff --git a/z1/stop-app.sh b/z1/stop-app.sh
new file mode 100755
index 0000000..a92a7ee
--- /dev/null
+++ b/z1/stop-app.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+echo "Stopping app..."
+docker-compose stop