diff --git a/Documentation/openapi/main.yaml b/Documentation/openapi/main.yaml new file mode 100644 index 0000000..263e9ab --- /dev/null +++ b/Documentation/openapi/main.yaml @@ -0,0 +1,741 @@ +openapi: 3.0.3 +info: + title: MyInpulse Backend Api + description: this document servers as a documentation for the backend api. + version: 0.0.0 + +tags: + - name: Entrepreneurs API + description: La partie de l'api dédiée aux entrepreneurs + - name: Admin API + description: La partie de l'api dédiée aux entrepreneurs + - name: Shared API + description: La partie de l'api dédiée aux entrepreneurs et admins + + +components: + schemas: + user: + type: object + properties: + nom: + type: string + prenom: + type: string + email: + type: string + example: "example@exmaple.com" + secondaryEmail: + type: string + example: "example@exmaple.com" + tel: + type: string + example: "0612345678" + user-entrepreneur: + type: object + properties: + user: + $ref: "#/components/schemas/user" + entrepreneur: + type: object + properties: + ecole: + type: string + example: "enseirb" + filiere: + type: string + example: "info" + status: + type: boolean + example: false + user-admin: + type: object + properties: + admin: + $ref: "#/components/schemas/user" + + securitySchemes: + MyINPulse: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + MyINPulse-admin: Administrateur + MyINPulse-entrepreneur: Utilisateur + +paths: + +# _ ____ __ __ ___ _ _ _ ____ ___ +# / \ | _ \| \/ |_ _| \ | | / \ | _ \_ _| +# / _ \ | | | | |\/| || || \| | / _ \ | |_) | | +# / ___ \| |_| | | | || || |\ | / ___ \| __/| | +# /_/ \_\____/|_| |_|___|_| \_| /_/ \_\_| |___| +# + + /admin/projects: + get: + summary: Retourne la liste of projets associés à l'admin + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + description: + JSON array of who's elements are objects containing necessary information for the view + (project name, entrepreneur names, etc..) + of the projects an admin is watching over. + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + E_names: + type: string + description: entrepreneur names + "400": + description: Bad request + "401": + description: Authorization information is missing or invalid + /admin/projects/pending/decision: + post: + summary: valider un projet en attente de validation + tags: + - Admin API + description: + if the request is accepted the status of the + project is changed to ongoing, entrepreneur + account is confirmed and the project is linked + to the admin accepting the request and the + entrepreneur requesting it. Else the pending + project and user info are deleted. + security: + - MyINPulse: + - MyINPulse-admin + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + pedingProjectId: + type: integer + decision: + type: boolean + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + /admin/projects/add: + post: + summary: Ajout manuel d'un projet + description: + Adds a project with the + inputed details + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + founder: + $ref: "#/components/schemas/user-entrepreneur" + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + /admin/appointments/report/{appointmentId}: + put: + summary: enregistrer un rapport du rendez-vous + description: + Generate a PDF file formatted + from input text and links it + to the appointement. + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + parameters: + - in: path + name: appointmentId + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + body: + type: string + conclusion: + type: string + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + post: + summary: modifier un rapport déja éxistant du rendez-vous + description: + Modifies the report file to input + text and links it to the appointement. + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + parameters: + - in: path + name: appointmentId + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + body: + type: string + conclusion: + type: string + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + + /admin/projects/remove/{projectId}: + delete: + summary: supression d'un project + description: + Removes the project + with the inputed id projectId + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + parameters: + - in: path + name: projectId + required: true + schema: + type: integer + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + + /admin/projects/pending: + get: + summary: Retourne la liste des projets en attente de validation + tags: + - Admin API + security: + - MyINPulse: + - MyINPulse-admin + description: + JSON array of who's elements are objects containing + necessary information for the view (project name, + entrepreneur names, etc..) of all pending projects. + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + founder: + $ref: "#/components/schemas/user-entrepreneur" + "400": + description: Bad request + "401": + description: Authorization information is missing or invalid + +# +# ____ _ _ _ ____ ___ +# / ___|| |__ __ _ _ __ ___ __| | / \ | _ \_ _| +# \___ \| '_ \ / _` | '__/ _ \/ _` | / _ \ | |_) | | +# ___) | | | | (_| | | | __/ (_| | / ___ \| __/| | +# |____/|_| |_|\__,_|_| \___|\__,_| /_/ \_\_| |___| +# + + /shared/appointments/upcoming: + get: + summary: Retourne la list des prochains rendez-vous de l'utilisateur + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + JSON array of upcoming appointment data (name, date, time etc..) for a user. + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + date: + type: string + time: + type: string + "400": + description: Bad request + "401": + description: Authorization information is missing or invalid + + /shared/projects/lcsection/{projectId}/{title}/{date}: + get: + summary: Retourne la liste de sections de LC avec un titre donné + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + JSON array containing Lean Canvas + section data with a title for the + current date (or given date if the + date parameter is passed) + parameters: + - in: path + required: true + name: projectId + schema: + type: integer + - in: path + required: true + description: this number can be 1, 2,...,8. It is associated with the title of the lcsection + name: title + schema: + type: integer + enum: [1, 2, 3, 4, 5, 6, 7, 8] + + - in: path + required: true + name: date + description: the date corresponding to the wanted version of lc section. "Nan" for the latest version + example: "NaN" + schema: + type: string + + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + section: + type: string + txt: + type: string + time: + type: string + "400": + description: Bad request + "401": + description: Authorization information is missing or invalid + + /shared/projects/entrepreneurs/{projectId}: + get: + summary: Retourne la liste d'entrepreneurs associée à un projet donné + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + JSON array of entrepreneur + names associated with a project + parameters: + - in: path + name: projectId + schema: + type: integer + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/user-entrepreneur" + "400": + description: Bad request + "401": + description: Authorization information is missing or invalid + + /shared/projects/admin/{projectId}: + get: + summary: Retourne les informations de l'admin qui accompagne le projet + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + JSON object containing information (name, gmail, tel, etc..) + the admin supervising the project with id projectID. + parameters: + - in: path + name: projectId + schema: + type: integer + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/user-admin" + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + /shared/projects/appointments/{projectId}: + get: + summary: Retourne les rendez-vous du projet + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + JSON array of upcoming and past appointment + data for the project with id projectID. + parameters: + - in: path + name: projectId + schema: + type: integer + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + appointementId: + type: integer + name: + type: string + date: + type: string + time: + type: string + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + /shared/projects/appointments/report/{apointementId}: + get: + summary: Retourne le rapport pdf du rendez-vous + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-admin + - MyINPulse-entrepreneur + description: + PDF file containing the ap- + pointment report + parameters: + - in: path + name: apointementId + schema: + type: integer + required: true + responses: + "200": + description: OK + content: + application/pdf: + schema: + type: string + format: binary + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + /shared/appointments/request: + post: + summary: demander un rendez-vous + description: + will add an appointement request request by the applicant + to have an appointment to be confirmed or denied by the + specified participants of the appointement. + tags: + - Shared API + security: + - MyINPulse: + - MyINPulse-entrepreneur + - MyINPulse-admin + requestBody: + description: \"participants\" property is an array containing userids of the participants in the appointement + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + start_time: + type: string + end_time: + type: string + place: + type: string + applicantId: + type: integer + participants: + #/* */ + type: array + items: + type: integer + + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + + +# _____ _ _ _____ ____ _____ ____ ____ _____ _ _ _____ _ _ ____ +# | ____| \ | |_ _| _ \| ____| _ \| _ \| ____| \ | | ____| | | | _ \ +# | _| | \| | | | | |_) | _| | |_) | |_) | _| | \| | _| | | | | |_) | +# | |___| |\ | | | | _ <| |___| __/| _ <| |___| |\ | |___| |_| | _ < +# |_____|_|_\_| |_| |_| \_\_____|_| |_| \_\_____|_| \_|_____|\___/|_| \_\ +# / \ | _ \_ _| +# / _ \ | |_) | | +# / ___ \| __/| | +# /_/ \_\_| |___| +# + + + /entrepreneur/projects/request: + post: + summary: demander la création et validation d'un projet + tags: + - Entrepreneurs API + description: + Adds project to pending projects + to then be accepted or rejected by + an admin + security: + - MyINPulse: + - MyINPulse-entrepreneur + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + founder: + $ref: "#/components/schemas/user-entrepreneur" + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + + /entrepreneur/lcsection/add/{projectId}: + post: + summary: ajouter une sections au LC + description: + Adds input data to the user's LC + with a specified title. + tags: + - Entrepreneurs API + security: + - MyINPulse: + - MyINPulse-entrepreneur + parameters: + - in: path + name: projectId + schema: + type: integer + required: true + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + txt: + type: string + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + /entrepreneur/lcsection/modify/{sectionId}: + put: + summary: modifier les données d'une section LC + description: + Modifies input Lean Canvas section by changing it to + the information in the request body and changes the + time stamp. + tags: + - Entrepreneurs API + security: + - MyINPulse: + - MyINPulse-entrepreneur + parameters: + - in: path + name: sectionId + schema: + type: integer + required: true + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + txt: + type: string + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + /entrepreneur/lcsection/remove/{sectionId}: + delete: + summary: supprimer une section LC. + description: + Deletes section from Lean Canvas + tags: + - Entrepreneurs API + security: + - MyINPulse: + - MyINPulse-entrepreneur + parameters: + - in: path + name: sectionId + schema: + type: integer + required: true + responses: + "200": + description: OK + "400": + description: Bad request + "401": + description: Authorization information is + missing or invalid + + + + \ No newline at end of file diff --git a/Makefile b/Makefile index 6f4e820..beb949c 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,8 @@ dev-front: clean vite keycloak @cp config/frontdev.docker-compose.yaml docker-compose.yaml @docker compose up -d --build @cd ./front/MyINPulse-front/ && npm run dev + @echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)' + @echo "./gradlew bootRun --args='--server.port=8081'" prod: clean keycloak @cp config/prod.env front/MyINPulse-front/.env @@ -40,6 +42,7 @@ prod: clean keycloak @cp config/prod.env .env @cp config/prod.docker-compose.yaml docker-compose.yaml @docker compose up -d --build + diff --git a/front/Dockerfile b/front/Dockerfile old mode 100644 new mode 100755 diff --git a/front/MyINPulse-front/fake_data/db.json b/front/MyINPulse-front/fake_data/db.json new file mode 100644 index 0000000..c04a261 --- /dev/null +++ b/front/MyINPulse-front/fake_data/db.json @@ -0,0 +1,63 @@ +{ + "entrepreneurs": [ + { "id": 1, "name": "Alice", "email": "alice@example.com" }, + { "id": 2, "name": "Bob", "email": "bob@example.com" }, + { "id": 3, "name": "Charlie", "email": "charlie@example.com" } + ], + "data": [ + { + "projectId": 1, + "title": 1, + "title_text": "1. Problème", + "description": "3 problèmes essentiels à résoudre pour le client" + }, + { + "projectId": 1, + "title": 2, + "title_text": "2. Segments", + "description": "Les segments de clientèle visés" + }, + { + "projectId": 1, + "title": 3, + "title_text": "3. Valeur", + "description": "La proposition de valeur" + }, + { + "projectId": 1, + "title": 4, + "title_text": "4. Solution", + "description": "Les solutions proposées" + }, + { + "projectId": 1, + "title": 5, + "title_text": "5. Avantage", + "description": "Les avantages concurrentiels" + }, + { + "projectId": 1, + "title": 6, + "title_text": "6. Canaux", + "description": "Les canaux de distribution" + }, + { + "projectId": 1, + "title": 7, + "title_text": "7. Indicateurs", + "description": "Les indicateurs clés de performance" + }, + { + "projectId": 1, + "title": 8, + "title_text": "8. Coûts", + "description": "Les coûts associés" + }, + { + "projectId": 1, + "title": 9, + "title_text": "9. Revenus", + "description": "Les sources de revenus" + } + ] +} diff --git a/front/MyINPulse-front/fake_data/open.sh b/front/MyINPulse-front/fake_data/open.sh new file mode 100755 index 0000000..cd3b9b6 --- /dev/null +++ b/front/MyINPulse-front/fake_data/open.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +json-server --watch db.json --port 5000 \ No newline at end of file diff --git a/front/MyINPulse-front/package-lock.json b/front/MyINPulse-front/package-lock.json index 8809acc..641b0b6 100644 --- a/front/MyINPulse-front/package-lock.json +++ b/front/MyINPulse-front/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.7.9", "cors": "^2.8.5", + "jwt-decode": "^4.0.0", "keycloak-js": "^26.1.0", "pinia": "^2.3.1", "pinia-plugin-persistedstate": "^4.2.0", @@ -3588,6 +3589,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keycloak-js": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.1.0.tgz", diff --git a/front/MyINPulse-front/package.json b/front/MyINPulse-front/package.json index ff46180..57ec628 100644 --- a/front/MyINPulse-front/package.json +++ b/front/MyINPulse-front/package.json @@ -18,7 +18,8 @@ "pinia": "^2.3.1", "pinia-plugin-persistedstate": "^4.2.0", "vue": "^3.5.13", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "jwt-decode": "^4.0.0" }, "devDependencies": { "@playwright/test": "^1.49.1", diff --git a/front/MyINPulse-front/src/App.vue b/front/MyINPulse-front/src/App.vue index feac4cf..795a6d2 100644 --- a/front/MyINPulse-front/src/App.vue +++ b/front/MyINPulse-front/src/App.vue @@ -1,47 +1,16 @@ <script setup lang="ts"> -import { RouterView } from "vue-router"; +import { /*RouterLink,*/ RouterView } from 'vue-router' import ErrorWrapper from "@/views/errorWrapper.vue"; -import ProjectComponent from "@/components/ProjectComponent.vue"; </script> <template> - <HeaderComponent /> - <error-wrapper></error-wrapper> - <div id="main"> - <ProjectComponent - v-for="(project, index) in projects" - :key="index" - :project-name="project.name" - /> - </div> + +<Header /> + <ErrorWrapper /> + <!--<RouterLink to="/">Home</RouterLink> | --> + <!--<RouterLink to="/canvas">Canvas</RouterLink> --> <RouterView /> </template> -<script lang="ts"> -import HeaderComponent from "@/components/HeaderComponent.vue"; -export default { - name: "App", - components: { - HeaderComponent, - }, - data() { - return { - projects: [ - { - name: "Projet Alpha", - //link: './project-alpha.html', - //members: ['Alice', 'Bob', 'Charlie'], - }, - { - name: "Projet Beta", - //link: './project-beta.html', - //members: ['David', 'Eve', 'Frank'], - }, - ], - }; - }, -}; -</script> -<style scoped></style> diff --git a/front/MyINPulse-front/src/components/AddProjectForm.vue b/front/MyINPulse-front/src/components/AddProjectForm.vue new file mode 100644 index 0000000..de3a21a --- /dev/null +++ b/front/MyINPulse-front/src/components/AddProjectForm.vue @@ -0,0 +1,110 @@ +<template> + <form class="add-project-form" @submit.prevent="submitProject"> + <h2>Ajouter un projet</h2> + + <div class="form-group"> + <label for="projectName">Nom du projet</label> + <input + id="projectName" + v-model="project.projectName" + type="text" + required + /> + </div> + + <div class="form-group"> + <label for="creationDate">Date de création</label> + <input + id="creationDate" + v-model="project.creationDate" + type="text" + placeholder="JJ-MM-AAAA" + required + /> + </div> + + <div class="form-group"> + <label for="logo">Logo</label> + <input + id="logo" + v-model="project.logo" + type="text" + placeholder="(à discuter)" + /> + </div> + + <button type="submit">Ajouter</button> + </form> +</template> + + <script setup lang="ts"> + import { ref } from "vue"; + import { postApi } from "@/services/api.ts"; + + const project = ref({ + projectName: "", + creationDate: "", + logo: "to be discussed not yet fixed", + }); + + function submitProject() { + postApi("/admin/projects/add", project.value); + } + </script> + + <style scoped> + + h2{ + font-size: 1.5rem; + color: #333; + margin-bottom: 1.2rem; + border-bottom: 2px solid #ddd; + padding-bottom: 0.5rem; + } + + .add-project-form { + max-width: 500px; + margin: 0 auto; + padding: 20px; + background: #fff; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + h2 { + margin-bottom: 20px; + font-size: 24px; + color: #333; + } + + .form-group { + margin-bottom: 15px; + display: flex; + flex-direction: column; + } + + label { + font-weight: bold; + margin-bottom: 5px; + } + + input { + padding: 8px; + border-radius: 5px; + border: 1px solid #ccc; + } + + button { + background-color: #4caf50; + color: white; + padding: 10px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + } + + button:hover { + background-color: #45a049; + } + </style> + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/Agenda.vue b/front/MyINPulse-front/src/components/Agenda.vue new file mode 100644 index 0000000..fabd137 --- /dev/null +++ b/front/MyINPulse-front/src/components/Agenda.vue @@ -0,0 +1,98 @@ +<template> + <div id="agenda"> + <h3>Rendez-vous</h3> + <table> + <thead> + <tr> + <th>Projet</th> + <th>Date</th> + <th>Lieu</th> + </tr> + </thead> + <tbody> + <tr v-for="(p, index) in projectRDV" :key="index"> + <td>{{ p.projectName }}</td> + <td>{{ p.date }}</td> + <td>{{ p.lieu }}</td> + </tr> + </tbody> + </table> + + </div> +</template> + +<script setup lang="ts"> + import { defineProps } from "vue"; + + interface rendezVous{ + projectName: string, + date: string, + lieu: string, + } + + const props = defineProps<{ + projectRDV: rendezVous[] + }>(); +</script> + +<style scoped> + + h3{ + font-size: 1.5rem; + color: #333; + margin-bottom: 1.2rem; + border-bottom: 2px solid #ddd; + padding-bottom: 0.5rem; + } + + #agenda { + padding: 20px; + background-color: white; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0,0,0,0.05); +} + + + /* Table Styling */ + table { + width: 100%; + border-collapse: collapse; + font-family: Arial, sans-serif; + text-align: left; + margin-top: 20px; + border: 1px solid #ccc; + } + + th { + background-color: #f0f2f5; + padding: 12px; + font-weight: 600; + color: #333; +} + + + /* Table Body Rows */ + tbody tr { + border-bottom: 1px solid #ddd; + transition: background-color 0.2s ease; /* Smooth hover effect */ + } + + tbody tr:hover { + background-color: #f9f9f9; /* Highlight row on hover */ + } + + /* Cells Styling */ + td { + padding: 10px; + border: 1px solid #eee; + font-size: 14px; + vertical-align: middle; /* Align text to middle */ + } + + /* First Column Styling */ + td:first-child { + text-align: center; + width: 50px; /* Adjust width as needed */ + } + +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/LoginComponent.vue b/front/MyINPulse-front/src/components/LoginComponent.vue new file mode 100644 index 0000000..60520c5 --- /dev/null +++ b/front/MyINPulse-front/src/components/LoginComponent.vue @@ -0,0 +1,198 @@ +<script lang="ts" setup> +import { onMounted, ref } from "vue"; +import { useRouter } from "vue-router"; +import {jwtDecode} from "jwt-decode"; // i hope this doesn't break the code later +import { store } from "../main.ts"; +import { callApi } from "@/services/api.ts"; + +const router = useRouter(); + +type TokenPayload = { + realm_access?: { + roles?: string[]; + }; +}; + +const customRequest = ref(''); + +onMounted(() => { + if (store.authenticated && store.user.token) { + try { + const decoded = jwtDecode<TokenPayload>(store.user.token); + const roles = decoded.realm_access?.roles || []; + + if (roles.includes("MyINPulse-admin")) { + router.push("/"); + } else if (roles.includes("MyINPulse-entrepreneur")) { + router.push("/leanCanva"); + } + } catch (err) { + console.error("Failed to decode token", err); + } + } +}); + +const loading = ref(false); + +const callApiWithLoading = async (path: string) => { + loading.value = true; + await callApi(path); + loading.value = false; +}; + + +</script> + + +<template> + <error-wrapper></error-wrapper> + <div class="auth-container"> + <div class="auth-card"> + <h1>Bienvenue</h1> + + <div class="status" :class="store.authenticated ? 'success' : 'error'"> + <p> + {{ store.authenticated ? '✅ Authenticated' : '❌ Not Authenticated' }} + </p> + </div> + + <div class="actions"> + <button @click="store.login">Login</button> + <button @click="store.logout">Logout</button> + <button @click="store.signup">Signup-admin</button> + <button @click="store.signup">Signup-Entrepreneur</button> + <button @click="store.refreshUserToken">Refresh Token</button> + </div> + + <div v-if="store.authenticated" class="token-section" > + <p><strong>Access Token:</strong></p> + <pre>{{ store.user.token }}</pre> + + <p><strong>Refresh Token:</strong></p> + <pre>{{ store.user.refreshToken }}</pre> + </div> + + <div class="api-calls"> + <h2>Test API Calls</h2> + <button @click="callApi('random')">Call Entrepreneur API</button> + <button @click="callApi('random2')">Call Admin API</button> + <button @click="callApi('unauth/dev')">Call Unauth API</button> + + <div class="custom-call"> + <input v-model="customRequest" placeholder="Custom endpoint" /> + <button @click="callApi(customRequest)">Call</button> + </div> + </div> + </div> + </div> +</template> + +<style scoped> +.auth-container { + display: flex; + justify-content: center; + align-items: center; + padding: 3rem 1rem; + min-height: 100vh; + background-color: #eef1f5; + font-family: Arial, sans-serif; +} + +.auth-card { + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 600px; +} + +h1 { + text-align: center; + margin-bottom: 1rem; + color: #333; +} + +.status { + text-align: center; + margin-bottom: 1.5rem; + font-weight: bold; +} +.success { + color: green; +} +.error { + color: red; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin-bottom: 1.5rem; +} + +.actions button { + padding: 0.6rem 1rem; + background-color: #4a90e2; + border: none; + color: white; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s; +} +.actions button:hover { + background-color: #357abd; +} + +.token-section pre { + background: #f6f8fa; + padding: 0.5rem; + overflow-x: auto; + border: 1px solid #ddd; + border-radius: 6px; + margin-bottom: 1rem; + font-size: 0.85rem; +} + +.api-calls { + margin-top: 2rem; +} +.api-calls h2 { + margin-bottom: 1rem; + color: #444; + font-size: 1.1rem; +} +.api-calls button { + margin-right: 0.5rem; + margin-bottom: 0.5rem; +} + +.custom-call { + margin-top: 1rem; + display: flex; + gap: 0.5rem; +} +.custom-call input { + flex: 1; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 6px; +} +/* +.status { + padding: 0.5rem 1rem; + border-radius: 8px; + display: inline-block; + background-color: #e0f7e9; + color: #2e7d32; +} +*/ +.status.error { + background-color: #ffe2e2; + color: #c62828; +} + + +</style> diff --git a/front/MyINPulse-front/src/components/PendingProjectComponent.vue b/front/MyINPulse-front/src/components/PendingProjectComponent.vue new file mode 100644 index 0000000..d93c94c --- /dev/null +++ b/front/MyINPulse-front/src/components/PendingProjectComponent.vue @@ -0,0 +1,133 @@ +<template> + <div class="project"> + <div class="project-header"> + <div class="project-title"> + <h2>{{ projectName }}</h2> + <p>Projet mis le: {{ creationDate }}</p> + </div> + <div class="project-buttons"> + <button id="accept" @click="acceptProject">Accepter</button> + <button id="refus" @click="refuseProject">Refuser</button> + </div> + </div> + </div> + +</template> + + + +<script setup lang="ts"> +import { defineProps } from "vue"; +import { postApi } from "@/services/api"; +import { addNewMessage, color } from "@/services/popupDisplayer"; + +const props = defineProps<{ + projectName: string; + creationDate: string; +}>(); + +const URI = "/admin/projects/pending/decision"; + +const sendDecision = (decision: "true" | "false") => { + postApi( + URI, + { + projectName: props.projectName, + decision, + }, + () => { + addNewMessage( + `Projet ${props.projectName} ${decision === "true" ? "accepté" : "refusé"}`, + color.Green + ); + }, + (err) => { + addNewMessage(`Erreur lors de la décision`, color.Red); + console.error(err); + } + ); +}; + +const acceptProject = () => sendDecision("true"); +const refuseProject = () => sendDecision("false"); +</script> + + + +<style scoped> +.project { + background: linear-gradient(to right, #f8f9fb, #ffffff); + border: 1px solid #e0e0e0; + border-radius: 16px; + padding: 1.5rem; + margin: 1.5rem 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + font-family: Arial, sans-serif; + transition: box-shadow 0.3s ease; +} + +.project:hover { + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.project-title { + display: flex; + flex-direction: column; +} + +.project-title h2 { + font-size: 1.25rem; + color: #222; + margin: 0; + font-weight: 600; +} + +.project-title p { + font-size: 0.9rem; + color: #666; + margin-top: 0.25rem; +} + +.project-buttons { + display: flex; + gap: 0.75rem; +} + +button { + padding: 0.5rem 1.1rem; + color: white; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +#accept { + background-color: #4CAF50; +} + +#accept:hover { + background-color: #3e8e41; + transform: translateY(-2px); +} + +#refus { + background-color: #e74c3c; +} + +#refus:hover { + background-color: #c0392b; + transform: translateY(-2px); +} + +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/ProjectComponent.vue b/front/MyINPulse-front/src/components/ProjectComponent.vue index 3802495..d4077f6 100644 --- a/front/MyINPulse-front/src/components/ProjectComponent.vue +++ b/front/MyINPulse-front/src/components/ProjectComponent.vue @@ -1,21 +1,109 @@ <template> - <div class="project"> + <div class="project" @click="goToLink" > <div class="project-header"> - <h2>{{ projectName }}</h2> + <h2 >{{ projectName }}</h2> + <div class="project-buttons"> + <button class="contact-btn">Contact</button> + </div> + </div> + <div class="project-body"> + <ul> + <li v-for="(name, index) in listName" :key="index">{{ name }}</li> + </ul> </div> </div> </template> -<script lang="ts"> -import type { PropType } from "vue"; -export default { - name: "ProjectComponent", - props: { - projectName: { - type: Object as PropType<string>, - required: true, - }, - }, + +<script setup lang="ts"> +import { defineProps } from "vue"; +import { useRouter } from 'vue-router' + + +const props = defineProps<{ + projectName: string; + listName: string[]; + projectLink: string; +}>(); + +const router = useRouter(); + +const goToLink = () => { + if (props.projectLink) { + router.push(props.projectLink); + } + }; </script> + + +<style scoped> +.project { + background: linear-gradient(to right, #f8f9fb, #ffffff); + border: 1px solid #e0e0e0; + border-radius: 16px; + padding: 1.5rem; + margin: 1.5rem 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + transition: box-shadow 0.3s ease; + cursor: pointer; +} + +.project:hover { + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.project-header h2 { + font-size: 1.25rem; + color: #222; + margin: 0; + font-weight: 600; +} + +.project-buttons { + display: flex; + gap: 0.5rem; +} + +.contact-btn { + background-color: #007bff; + color: #fff; + padding: 0.5rem 1rem; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.contact-btn:hover { + background-color: #0056b3; + transform: translateY(-2px); +} + +.project-body { + margin-top: 1rem; +} + +.project-body ul { + list-style-type: disc; + padding-left: 1.25rem; + margin: 0; +} + +.project-body ul li { + font-size: 0.95rem; + color: #555; + line-height: 1.6; +} + +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/CanvasItem.vue b/front/MyINPulse-front/src/components/canvas/CanvasItem.vue new file mode 100755 index 0000000..226c456 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/CanvasItem.vue @@ -0,0 +1,396 @@ +<template> + <div :class="['cell', { expanded }]" @click="handleClick"> + <h3 class="fs-5 fw-medium">{{ titleText }}</h3> + + <div v-for="(desc, index) in currentDescriptions" :key="index" class="section-bloc"> + +<!-- ADMIN --------------------------------------------------------------------------------------------> + + <template v-if="IS_ADMIN"> + <div class="description"> + <p class="m-0">{{ desc }}</p> + </div> + </template> + +<!-- ENTREP -------------------------------------------------------------------------------------------> + + <template v-if="!IS_ADMIN"> + <!-- Mode affichage --> + <template v-if="!isEditing[index]"> + <div class="description"> + <p class="m-0">{{ desc }}</p> + </div> + <div class="button-container"> + <button v-if="expanded" class="edit-button" @click.stop="startEditing(index)">Éditer</button> + </div> + </template> + + <!-- Mode édition --> + <template v-else> + <textarea v-model="editedDescriptions[index]" class="edit-input"></textarea> + <div class="button-container"> + <button class="save-button" @click.stop="saveEdit(index)">Enregistrer</button> + <button class="cancel-button" @click.stop="cancelEdit(index)">Annuler</button> + </div> + </template> + </template> + </div> +<!----------------------------------------------------------------------------------------------------> + <template v-if="expanded"> + <div class="canvas-exit-hint"> + Cliquez n'importe où pour quitter le canvas + </div> + </template> + </div> +</template> + +<script setup lang="ts"> +import { ref, defineProps, onMounted } from "vue"; +import axios from "axios"; +import { axiosInstance } from "@/services/api.ts"; + +const IS_MOCK_MODE = true; + +const props = defineProps<{ + projectId: number; + title: number; + titleText: string; + description: string; + is_admin: number; +}>(); + +const IS_ADMIN = props.is_admin; + +const expanded = ref(false); +const currentDescriptions = ref<string[]>([]); +currentDescriptions.value[0] = props.description; +const editedDescriptions = ref<string[]>([]); +const isEditing = ref<boolean[]>([]); + +onMounted(() => { + fetchData(props.projectId, props.title, "NaN", IS_MOCK_MODE); +}); + + +/* FOR LOCAL DATABASE +const fetchData = async () => { + try { + const response = await axios.get("http://localhost:5000/data"); // Met à jour l'URL + if (response.data.length > 0) { + currentDescription.value = response.data[0].canva_data; + editedDescription.value = response.data[0].canva_data; + } else { + console.warn("Aucune donnée reçue."); + } + } catch (error) { + console.error("Erreur lors de la récupération des données :", error); + } +}; +*/ + +// Fonction fetchData avec possibilité d'utiliser le mock +/* FOR FETCHING WITH AXIOS DIRECTLY +const fetchData = async (projectId: number, title: number, date: string, useMock = false) => { + try { + const responseData = useMock + ? await mockFetch(projectId, title, date) + : (await axios.get<{ txt: string }[]>( + `http://localhost:5000/shared/projects/lcsection/${projectId}/${title}/${date}` + )).data; + if (responseData.length > 0) { + currentDescriptions.value = responseData.map((item) => item.txt); + editedDescriptions.value = [...currentDescriptions.value]; + isEditing.value = Array(responseData.length).fill(false); + } else { + console.warn("Aucune donnée reçue."); + } + } catch (error) { + console.error("Erreur lors de la récupération des données :", error); + } +}; +*/ + +// Fonction fetchData avec possibilité d'utiliser le mock +const fetchData = async (projectId: number, title: number, date: string, useMock = false) => { + try { + const responseData = useMock + ? await mockFetch(projectId, title, date) + : (await axiosInstance.get<{ txt: string }[]>( + `/shared/projects/lcsection/${projectId}/${title}/${date}` + )).data; + + if (responseData.length > 0) { + currentDescriptions.value = responseData.map((item) => item.txt); + editedDescriptions.value = [...currentDescriptions.value]; + isEditing.value = Array(responseData.length).fill(false); + } else { + console.warn("Aucune donnée reçue."); + } + } catch (error) { + console.error("Erreur lors de la récupération des données :", error); + } +}; + +// Fonction de simulation de l'API +const mockFetch = async (projectId: number, title: number, date: string) => { + console.log(`Mock fetch pour projectId: ${projectId}, title: ${title}, date: ${date}`); + + return new Promise<{ txt: string }[]>((resolve) => { + setTimeout(() => { + resolve([ + {txt: "Ceci est une description 1 pour tester le front."}, + {txt: "Deuxième description."}, + {txt: "Troisième description."} + ]); + }, 500); // Simule un délai réseau de 500ms + }); +}; + +// Utilisation du mock dans handleClick pour tester sans serveur +const handleClick = async () => { + if (!expanded.value) { + await fetchData(props.projectId, props.title, "NaN", IS_MOCK_MODE); + } else if (!isEditing.value.includes(true)) { + // Réinitialiser les descriptions si aucune édition n'est en cours + currentDescriptions.value = [props.description]; + editedDescriptions.value = [props.description]; + } + + if (!isEditing.value.includes(true)) { + expanded.value = !expanded.value; + } +}; + + +const startEditing = (index: number) => { + isEditing.value[index] = true; +}; + +/* +const saveEdit = async (index: number) => { + try { + const id = index + 1; // À adapter selon l'ID réel des données + await axios.put(`http://localhost:5000/data/${id}`, { + canva_data: editedDescriptions.value[index] + }); + + // Mettre à jour l'affichage local après la mise à jour réussie + currentDescriptions.value[index] = editedDescriptions.value[index]; + isEditing.value[index] = false; + } catch (error) { + console.error("Erreur lors de la mise à jour des données :", error); + } +}; +*/ + +const saveEdit = async (index: number) => { + if (IS_MOCK_MODE) { + await mockSaveEdit(index); + } else { + try { + const id = index + 1; + await axios.put(`http://localhost:5000/data/${id}`, { + canva_data: editedDescriptions.value[index] + }); + + // Mettre à jour l'affichage local après la mise à jour réussie + currentDescriptions.value[index] = editedDescriptions.value[index]; + isEditing.value[index] = false; + } catch (error) { + console.error("Erreur lors de la mise à jour des données :", error); + } + } +}; + +// Fonction de mock pour l'enregistrement +const mockSaveEdit = async (index: number) => { + try { + const id = index + 1; + console.log(`Mock save pour l'ID ${id} avec la description : ${editedDescriptions.value[index]}`); + + await new Promise((resolve) => setTimeout(resolve, 500)); // Simulation de délai réseau + + // Mettre à jour l'affichage local après la mise à jour réussie + currentDescriptions.value[index] = editedDescriptions.value[index]; + isEditing.value[index] = false; + } catch (error) { + console.error("Erreur lors de la mise à jour des données mockées :", error); + } +}; + +const cancelEdit = (index: number) => { + editedDescriptions.value[index] = currentDescriptions.value[index]; + isEditing.value[index] = false; +}; +</script> + +<style scoped> +@import "@/components/canvas/style-project.css"; + + +.cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; + box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1); +} + + +.expanded-content { + justify-content: flex-start !important; +} + +.cell:not(.expanded):hover { + transform: scale(1.05); + box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2); +} + +.cell h3 { + font-size: 15px; + font-weight: 500; + font-family: 'Arial', sans-serif; +} + +.p { + font-size: 10px; + color: #666; + font-family: 'Arial', sans-serif; +} + +.expanded { + padding-top: 10%; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: white; + z-index: 10; + display: flex; + align-items: center; + justify-content: flex-start; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); +} + + +.description { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + height: 100%; + font-size: 16px; + margin-top: 10px; + margin-left: 2%; + margin-right: 4%; +} + +.description + .p { + align-items: center; + justify-content: center; + text-align: center; +} + + + +.edit-input { + width: 100%; + height: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + margin-top: 10px; + box-sizing: border-box; + margin-left: 2%; +} + + +.button-container { + display: block; + margin-top: 20px; + justify-content: center; + align-items: center; + gap: 10px; + padding-right: 1%; +} + + +.section-bloc ,.editing-section-bloc { + width: 100%; + justify-content: center; + align-items: center; + display: flex; + margin-right: 10%; + margin: 10px; +} + + + + + +.edit-button { + width: 100px; + height: 40px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s ease; + font-size: 12px; + margin-right: 20px; +} + +.save-button, .cancel-button { + width: 100px; + height: 40px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s ease; + font-size: 12px; + margin-bottom: 5px; +} + +.edit-button { + background-color: #007bff; + color: white; +} + +.save-button { + background-color: #28a745; + color: white; +} + +.cancel-button { + background-color: #dc3545; + color: white; +} + +.edit-button:hover { + background-color: #0056b3; +} + +.save-button:hover { + background-color: #218838; +} + +.cancel-button:hover { + background-color: #c82333; +} + +.canvas-exit-hint { + font-size: 0.75rem; + color: #666; + position: fixed; + bottom: 10px; + left: 0; + width: 100%; + text-align: center; + z-index: 1000; +} + +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/HeaderCanvas.vue b/front/MyINPulse-front/src/components/canvas/HeaderCanvas.vue new file mode 100644 index 0000000..3332465 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/HeaderCanvas.vue @@ -0,0 +1,218 @@ +<template> + <header class="header"> + <img src="../icons/logo inpulse.png" alt="INPulse Logo" class="logo" /> + + <div class="header-actions"> + <div class="dropdown-wrapper"> + <button class="contact-button" @click="toggleDropdown">Contact</button> + <div class="contact-dropdown" :class="{ 'dropdown-visible': isDropdownOpen }"> + <button @click="contactAll">Contacter tous</button> + <button + v-for="(email, index) in entrepreneurEmails" + :key="index" + @click="contactSingle(email)" + > + {{ email }} + </button> + </div> + </div> + <RouterLink to="/" class="return-button">Retour</RouterLink> + </div> + </header> +</template> + + +<script setup lang="ts"> +import { ref, onMounted } from "vue"; +import axios from "axios"; + +const IS_MOCK_MODE = true; + +const props = defineProps<{ + projectId: number; +}>(); + +type Entrepreneur = { + idUser: number; + userSurname: string; + userName: string; + primaryMail: string; + secondaryMail: string; + phoneNumber: string; + school: string; + course: string; + sneeStatus: boolean; +}; + +const isDropdownOpen = ref(false); +const entrepreneurEmails = ref<string[]>([]); + +const toggleDropdown = () => { + isDropdownOpen.value = !isDropdownOpen.value; + console.log("Dropdown toggled:", isDropdownOpen.value); +}; + +const fetchEntrepreneurs = async (projectId: number, useMock = IS_MOCK_MODE) => { + try { + const responseData: Entrepreneur[] = useMock + ? await mockFetchEntrepreneurs(projectId) + : (await axios.get(`http://localhost:5000/shared/projects/entrepreneurs/${projectId}`)).data; + + if (responseData.length > 0) { + entrepreneurEmails.value = responseData.map((item: Entrepreneur) => item.primaryMail); + } else { + console.warn("Aucun entrepreneur trouvé."); + } + } catch (error) { + console.error("Erreur lors de la récupération des entrepreneurs :", error); + } +}; + +// Fonction de simulation de l'API +const mockFetchEntrepreneurs = async (projectId :number) => { + console.log(`Mock fetch pour projectId: ${projectId}`); + + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + idUser: 1, + userSurname: "Doe", + userName: "John", + primaryMail: "john.doe@example.com", + secondaryMail: "johndoe@backup.com", + phoneNumber: "612345678", + school: "ENSEIRB", + course: "Info", + sneeStatus: false + }, + { + idUser: 2, + userSurname: "Smith", + userName: "Jane", + primaryMail: "jane.smith@example.com", + secondaryMail: "janesmith@backup.com", + phoneNumber: "698765432", + school: "ENSEIRB", + course: "Info", + sneeStatus: true + } + ]); + }, 500); + }); +}; + +const contactAll = () => { + const allEmails = entrepreneurEmails.value.join(", "); + navigator.clipboard.writeText(allEmails) + .then(() => { + alert("Tous les emails copiés dans le presse-papiers !"); + window.open("https://partage.bordeaux-inp.fr/", "_blank"); + }) + .catch(err => { + console.error("Erreur lors de la copie :", err); + }); +}; + +const contactSingle = (email: string) => { + navigator.clipboard.writeText(email) + .then(() => { + alert(`Adresse copiée : ${email}`); + window.open("https://partage.bordeaux-inp.fr/", "_blank"); + }) + .catch(err => { + console.error("Erreur lors de la copie :", err); + }); +}; + + +const copyToClipboard = (email: string) => { + navigator.clipboard.writeText(email).then(() => { + alert(`Adresse copiée : ${email}`); + }).catch(err => { + console.error("Erreur lors de la copie :", err); + }); +}; + +onMounted(() => fetchEntrepreneurs(props.projectId, IS_MOCK_MODE)); +</script> + +<style scoped> +@import "@/components/canvas/style-project.css"; + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 30px; + background-color: #f9f9f9; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.logo { + height: 50px; +} + +.header-actions { + display: flex; + align-items: center; + gap: 20px; + position: relative; +} + +.contact-button, +.return-button { + background-color: #009CDE; + color: white; + border: none; + padding: 10px 15px; + cursor: pointer; + font-size: 14px; + border-radius: 5px; + text-decoration: none; + transition: background-color 0.2s ease; + font-family: Arial, sans-serif; +} + +.return-button:hover, +.contact-button:hover { + background-color: #007bad; +} + + +.contact-dropdown { + position: absolute; + top: 100%; + left: 0; + background-color: #000; + color: white; + box-shadow: 0px 4px 8px rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 10px; + margin-top: 5px; + z-index: 1000; + min-width: 200px; + display: none; +} + +.contact-dropdown button { + display: block; + width: 100%; + padding: 5px; + text-align: left; + border: none; + background: none; + cursor: pointer; + color: white; +} + +.contact-dropdown button:hover { + background-color: #009CDE; +} + +.contact-dropdown.dropdown-visible { + display: block; +} + + +</style> diff --git a/front/MyINPulse-front/src/components/canvas/LeanCanvas.vue b/front/MyINPulse-front/src/components/canvas/LeanCanvas.vue new file mode 100644 index 0000000..69f332f --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/LeanCanvas.vue @@ -0,0 +1,82 @@ +<template> + <div class="canvas container-fluid"> + <CanvasItem + v-for="(item, index) in items" + :key="index" + :title="item.title" + :titleText="item.title_text" + :description="item.description" + :project-id="item.projectId" + :class="['canvas-item', item.class, 'card', 'shadow', 'p-3']" + :is_admin=is_admin + /> + </div> +</template> + +<script setup lang="ts"> +import { ref, onMounted } from "vue"; +import CanvasItem from "@/components/canvas/CanvasItem.vue"; + +const props = defineProps<{ + is_admin: number; +}>(); + +const items = ref([ + { projectId: 1, title: 1, title_text: "1. Problème", description: "3 problèmes essentiels à résoudre pour le client", class: "Probleme" }, + { projectId: 1, title: 2, title_text: "2. Segments", description: "Les segments de clientèle visés", class: "Segments" }, + { projectId: 1, title: 3, title_text: "3. Valeur", description: "La proposition de valeur", class: "Valeur" }, + { projectId: 1, title: 4, title_text: "4. Solution", description: "Les solutions proposées", class: "Solution" }, + { projectId: 1, title: 5, title_text: "5. Avantage", description: "Les avantages concurrentiels", class: "Avantage" }, + { projectId: 1, title: 6, title_text: "6. Canaux", description: "Les canaux de distribution", class: "Canaux" }, + { projectId: 1, title: 7, title_text: "7. Indicateurs", description: "Les indicateurs clés de performance", class: "Indicateurs" }, + { projectId: 1, title: 8, title_text: "8. Coûts", description: "Les coûts associés", class: "Couts" }, + { projectId: 1, title: 9, title_text: "9. Revenus", description: "Les sources de revenus", class: "Revenus" } +]); + +onMounted(() => { + const bootstrapCss = document.createElement('link') + bootstrapCss.rel = 'stylesheet' + bootstrapCss.href = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css' + bootstrapCss.integrity = 'sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+Fpc+NC' + bootstrapCss.crossOrigin = 'anonymous' + document.head.appendChild(bootstrapCss) + + const bootstrapJs = document.createElement('script') + bootstrapJs.src = 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js' + bootstrapJs.integrity = 'sha384-mQ93S0EhrF4Z1nM+fTflmYf0DyzsY5j7F5H3WlClDD6H3WUJh6kxBkF3GDW8n1j6' + bootstrapJs.crossOrigin = 'anonymous' + document.body.appendChild(bootstrapJs) +}) +</script> + +<style scoped> +@import "@/components/canvas/style-project.css"; + +.canvas { + display: grid; + grid-template-columns: repeat(10, 1fr); + grid-template-rows: repeat(6, 1fr); + gap: 12px; + padding: 30px; + /*background-color: #f8f9fa;*/ + position: relative; + height: 90vh; + overflow: auto; +} + +.Probleme { grid-column: 1 / 3; grid-row: 1 / 5; } +.Segments { grid-column: 9 / 11; grid-row: 1 / 5; } +.Valeur { grid-column: 5 / 7; grid-row: 1 / 5; } +.Solution { grid-column: 3 / 5; grid-row: 1 / 3; } +.Avantage { grid-column: 7 / 9; grid-row: 1 / 3; } +.Canaux { grid-column: 7 / 9; grid-row: 3 / 5; } +.Indicateurs { grid-column: 3 / 5; grid-row: 3 / 5; } +.Couts { grid-column: 1 / 6; grid-row: 5 / 7; } +.Revenus { grid-column: 6 / 11; grid-row: 5 / 7; } + +.canvas-item { + /*background-color: white;*/ + border: 1px solid #dee2e6; + border-radius: 0.5rem; +} +</style> diff --git a/front/MyINPulse-front/src/components/canvas/style-project.css b/front/MyINPulse-front/src/components/canvas/style-project.css new file mode 100644 index 0000000..e505a15 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/style-project.css @@ -0,0 +1,156 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 10px; + } + + .row { + display: flex; + } + + .cell { + flex: 1; + border: 1px solid #ddd; + padding: 10px; + text-align: center; + background-color: #f1f1f1; + } + + .produit { + background-color: #f9e4e4; + } + + .marche { + background-color: #e4f1f9; + } + + .valeur { + background-color: #f9f4e4; + } + + h3 { + margin: 0; + font-size: 18px; + color: #333; + } + + p { + margin: 5px 0 0; + font-size: 14px; + } + + + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f9f9f9; + } + + h1 img { + height: 80px; + margin: 40px; + text-align: center; + } + + .row { + display: flex; + margin-bottom: 10px; + } + + #ade { + max-width: 1200px; + margin: 20px auto; + padding: 20px; + text-align: center; + background-color: #e8f5e9; + border: 2px solid #4caf50; + border-radius: 10px; + } + + #ade h3 { + color: #2e7d32; + } + + #ade p { + margin: 10px 0; + font-size: 16px; + color: #333; + } + header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + background-color: #fff; + border-bottom: 2px solid #ddd; + } + + header img { + height: 60px; + } + + header .contact-menu { + position: relative; + } + + .contact-button, .return { + padding: 10px 15px; + border: none; + border-radius: 4px; + background-color: #2196f3; + color: #fff; + cursor: pointer; + } + + .contact-button:hover, .return:hover { + background-color: #1976d2; + } + + /* Dropdown styling */ + .contact-dropdown { + position: absolute; + right: 0; + top: 50px; + display: none; + flex-direction: column; + gap: 10px; + padding: 15px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 10; + } + + .contact-dropdown button { + padding: 8px 12px; + border: none; + border-radius: 4px; + background-color: #4caf50; + color: #fff; + cursor: pointer; + } + + .contact-dropdown button:hover { + background-color: #388e3c; + } + + .return { + background-color: #f44336; + } + + .return:hover { + background-color: #d32f2f; + } + + .header-buttons { + display: flex; + align-items: center; + gap: 15px; + } + + + a{ + color: white; + } \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/icons/logo inpulse.png b/front/MyINPulse-front/src/components/icons/logo inpulse.png index e5a30ee..059d30d 100644 Binary files a/front/MyINPulse-front/src/components/icons/logo inpulse.png and b/front/MyINPulse-front/src/components/icons/logo inpulse.png differ diff --git a/front/MyINPulse-front/src/main.ts b/front/MyINPulse-front/src/main.ts index 0eb983d..8c17236 100644 --- a/front/MyINPulse-front/src/main.ts +++ b/front/MyINPulse-front/src/main.ts @@ -26,6 +26,52 @@ keycloakService.CallInit(() => { console.error(e); createApp(App).mount("#app"); } -}); + +}) + +// this shit made by me so i can run the canva vue app +//createApp(App).use(router).mount('#app'); + +// TODO: fix the comment +/* +function tokenInterceptor () { + axios.interceptors.request.use(config => { + const keycloak = useKeycloak() + if (keycloak.authenticated) { + // Note that this is a simple example. + // you should be careful not to leak tokens to third parties. + // in this example the token is added to all usage of axios. + config.headers.Authorization = `Bearer ${keycloak.token}` + } + return config + }, error => { + console.error("tokenInterceptor: Rejected") + return Promise.reject(error) + }) +} +*/ + +/* +app.use(VueKeyCloak,{ + onReady: (keycloak) => { + console.log("Ready !") + tokenInterceptor() + }, + init: { + onLoad: 'login-required', + checkLoginIframe: false, + + }, + + config: { + realm: 'test', + url: 'http://localhost:7080', + clientId: 'myinpulse' + } +} ); +*/ + + + export { store }; diff --git a/front/MyINPulse-front/src/plugins/authStore.ts b/front/MyINPulse-front/src/plugins/authStore.ts new file mode 100644 index 0000000..1a5c06e --- /dev/null +++ b/front/MyINPulse-front/src/plugins/authStore.ts @@ -0,0 +1,14 @@ +// file: src/plugins/authStore.js + +import { useAuthStore } from "@/stores/authStore.ts"; +import keycloakService from '@/services/keycloak'; +// Setup auth store as a plugin so it can be accessed globally in our FE +const authStorePlugin = { + install(app: any, option: any) { + const store = useAuthStore(option.pinia); + app.config.globalProperties.$store = store; + keycloakService.CallInitStore(store); + } +} + +export default authStorePlugin; \ No newline at end of file diff --git a/front/MyINPulse-front/src/router/router.ts b/front/MyINPulse-front/src/router/router.ts index f2eb27d..c2a127a 100644 --- a/front/MyINPulse-front/src/router/router.ts +++ b/front/MyINPulse-front/src/router/router.ts @@ -1,17 +1,40 @@ import { createRouter, createWebHistory } from "vue-router"; const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: "/test", - name: "test", - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import("../views/testComponent.vue"), - }, - ], -}); + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/test', + name: 'test', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/testComponent.vue'), + }, + { + path: '/login', + name: 'login', + component: () => import('../components/LoginComponent.vue'), + }, + { + path: '/', + name: 'Admin-main', + component: () => import('../views/AdminMain.vue'), + }, + +// route pour les canvas (made by adnane), in fact the two vue apps are separated for now + { + path: '/canvas', + name: 'canvas', + component: () => import('../views/CanvasView.vue'), + }, + + { + path: '/signup', + name: 'signup', + component: () => import('../views/EntrepSignUp.vue'), + }, + ], +}) export default router; diff --git a/front/MyINPulse-front/src/services/api.ts b/front/MyINPulse-front/src/services/api.ts index 5c4fc7b..6b9513f 100644 --- a/front/MyINPulse-front/src/services/api.ts +++ b/front/MyINPulse-front/src/services/api.ts @@ -65,4 +65,28 @@ function callApi( ); } -export { callApi }; +function postApi( + endpoint: string, + data: any, + onSuccessHandler?: (response: AxiosResponse) => void, + onErrorHandler?: (error: AxiosError) => void +): void { + axiosInstance + .post(endpoint, data) + .then(onSuccessHandler ?? defaultApiSuccessHandler) + .catch(onErrorHandler ?? defaultApiErrorHandler); +} + +function deleteApi( + endpoint: string, + onSuccessHandler?: (response: AxiosResponse) => void, + onErrorHandler?: (error: AxiosError) => void +): void { + axiosInstance + .delete(endpoint) + .then(onSuccessHandler ?? defaultApiSuccessHandler) + .catch(onErrorHandler ?? defaultApiErrorHandler); +} + + +export { axiosInstance, callApi, postApi, deleteApi }; diff --git a/front/MyINPulse-front/src/stores/authStore.ts b/front/MyINPulse-front/src/stores/authStore.ts index 06187ca..a5874d4 100644 --- a/front/MyINPulse-front/src/stores/authStore.ts +++ b/front/MyINPulse-front/src/stores/authStore.ts @@ -54,7 +54,7 @@ const useAuthStore = defineStore("storeAuth", { async logout() { try { await keycloakService.CallLogout( - import.meta.env.VITE_APP_URL + "/test" + import.meta.env.VITE_APP_URL + "/login" //redirect to login page instead of test... ); await this.clearUserData(); } catch (error) { diff --git a/front/MyINPulse-front/src/views/AdminMain.vue b/front/MyINPulse-front/src/views/AdminMain.vue new file mode 100644 index 0000000..1bf8b5a --- /dev/null +++ b/front/MyINPulse-front/src/views/AdminMain.vue @@ -0,0 +1,166 @@ +<template> + <Header /> + <error-wrapper></error-wrapper> + <div id="container"> + <div id="main"> + <h3> Projet en cours </h3> + <ProjectComp + v-for="(project, index) in projects" + :key="index" + :project-name="project.name" + :list-name="project.members" + :project-link="project.link" + /> + + <div id ="main"> + <h3> Projet en attente </h3> + + <PendingProjectComponent + v-for="( project, index) in pendingProjects" + :key="index" + :project-name="project.name" + :creation-date="project.creationDate" + /> + </div> + + <AddProjectForm/> + </div> + + <Agenda :project-r-d-v="rendezVous" /> + </div> + +</template> + +<script setup lang="ts"> +import { ref, onMounted } from "vue"; +import { callApi } from "@/services/api"; + +import Header from "../components/HeaderComponent.vue"; +import Agenda from "../components/Agenda.vue"; +import ProjectComp from "../components/ProjectComponent.vue"; +import PendingProjectComponent from "@/components/PendingProjectComponent.vue"; +import AddProjectForm from "@/components/AddProjectForm.vue"; + +const PORT = "8081"; +const URI = `http://localhost:${PORT}`; + +//const projects = ref<{ name: string; link: string; members: string[] }[]>([]); + +/* const fetchProjects = () => { + callApi( + `${URI}/admin/projects`, + async (response) => { + console.log(response); + const projectList = response.data; + + const projectPromises = projectList.map((project: any) => { + return new Promise(async (resolve) => { + callApi( + `${URI}/shared/projects/entrepreneurs/${project.idProject}`, + (memberResponse) => { + const members = memberResponse.data.map((m: any) => m.userName); + resolve({ + name: project.projectName, + link: `/project/${project.idProject}`, + members, + }); + }, + () => { + // Error fetching members, still resolve with empty members + resolve({ + name: project.projectName, + link: `/project/${project.idProject}`, + members: [], + }); + } + ); + }); + }); + + projects.value = await Promise.all(projectPromises); + }, + (error) => { + console.error("Error fetching projects:", error); + } + ); +}; + +onMounted(fetchProjects); + */ + + +const projects = ref([ + { + name: "Projet Alpha", + link: "/canvas", // to test + members: ["Alice", "Bob", "Charlie"], + }, + { + name: "Projet Beta", + link: "./canvas", // to test + members: ["David", "Eve", "Frank"], + }, +]); + + +const pendingProjects = ref ([ + { name: "l'eau", creationDate: "26-02-2024" }, + { name: "l'air", creationDate: "09-03-2023" }, +]) + +const rendezVous = ref([ + { projectName: "Projet Alpha", date: "2025-03-10", lieu: "P106" }, + { projectName: "Projet Beta", date: "2025-04-15", lieu: "Td10" }, +]); + +</script> + +<style scoped> + +#container { + display: grid; + grid-template-columns: 3fr 1fr; + gap: 2rem; + padding: 2rem; + background-color: #f4f6f9; + min-height: 100vh; + box-sizing: border-box; +} + +#main { + background-color: #fff; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +h3 { + font-size: 1.5rem; + color: #333; + margin-bottom: 1.2rem; + border-bottom: 2px solid #ddd; + padding-bottom: 0.5rem; +} + +button { + padding: 10px 15px; + background-color: #007bff; + color: white; + border: none; + cursor: pointer; + border-radius: 6px; + font-weight: 500; + transition: background-color 0.2s ease; +} + +button:hover { + background-color: #0056b3; +} + +/* Add spacing between project sections */ +#main > * + * { + margin-top: 2rem; +} + + +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/CanvasView.vue b/front/MyINPulse-front/src/views/CanvasView.vue new file mode 100644 index 0000000..3c9c983 --- /dev/null +++ b/front/MyINPulse-front/src/views/CanvasView.vue @@ -0,0 +1,126 @@ +<template> + <div> + <header> + <HeaderCanvas :project-id="1" /> + </header> + </div> + <div> + <h1 class="page-title">PAGE CANVAS</h1> + + <p class="canvas-help-text"> + Cliquez sur un champ du tableau pour afficher son contenu en détail ci-dessous. + </p> + <LeanCanvas :is_admin=is_admin /> + + <div class="info-box"> + <p> + Responsable : <strong>{{ admin.userName }} {{ admin.userSurname }}</strong><br /> + Contact : <a href="mailto:{{ admin.primaryMail }}">{{ admin.primaryMail }}</a> | + <a href="tel:{{ admin.phoneNumber }}">{{ admin.phoneNumber }}</a> + </p> + </div> + </div> +</template> + +<script setup lang="ts"> + +import HeaderCanvas from "../components/canvas/HeaderCanvas.vue"; +import LeanCanvas from '../components/canvas/LeanCanvas.vue'; +import { ref, onMounted, defineProps} from "vue"; +import { axiosInstance } from "@/services/api.ts"; + +const IS_MOCK_MODE = true; + +/* +const props = defineProps<{ + projectId: number; + token: TokenPayload; +}>(); + + +is_admin = token.includes("MyINPulse-admin") + */ + +const is_admin = 0 + +// Variables pour les informations de l'administrateur +const admin = ref({ + idUser: 0, + userSurname: "", + userName: "", + primaryMail: "", + secondaryMail: "", + phoneNumber: "" +}); + +const mockAdminData = { + idUser: 1, + userSurname: "ALAMI", + userName: "Adnane", + primaryMail: "mock.admin@example.com", + secondaryMail: "admin.backup@example.com", + phoneNumber: "0600000000" +}; + +// Fonction pour récupérer les données de l'administrateur +const fetchAdminData = async (projectId: number, useMock = IS_MOCK_MODE) => { + try { + if (useMock) { + console.log("Utilisation des données mockées pour l'administrateur"); + admin.value = mockAdminData; + return; + } + + const response = await axiosInstance.get(`/shared/projects/admin/${projectId}`); + admin.value = response.data; + } catch (error) { + console.error("Erreur lors de la récupération des données de l'administrateur :", error); + } +}; + +// Appeler la fonction fetch au montage du composant +onMounted(() => { + const projectId = 1; + fetchAdminData(projectId); +}); +</script> + +<style scoped> +.page-title { + text-align: center; + font-size: 2.5rem; + margin-top: 20px; +} + +.canvas-help-text { + text-align: center; + font-size: 0.7rem; + color: #666; +} + +.info-box { + background-color: #f9f9f9; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + font-family: Arial, sans-serif; + width: 30%; + max-width: 600px; + margin: 20px auto; +} + +.info-box p { + font-size: 16px; + line-height: 1.5; + color: #333; +} + +.info-box a { + color: #007bff; + text-decoration: none; +} + +.info-box a:hover { + text-decoration: underline; +} +</style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/EntrepSignUp.vue b/front/MyINPulse-front/src/views/EntrepSignUp.vue new file mode 100644 index 0000000..681a2be --- /dev/null +++ b/front/MyINPulse-front/src/views/EntrepSignUp.vue @@ -0,0 +1,190 @@ +<template> + <form class="add-project-form" @submit.prevent="submitForm"> + <h2>Ajouter un projet</h2> + + <div class="form-group"> + <label for="name">Nom du projet</label> + <input + id="name" + v-model="form.name" + type="text" + required + /> + </div> + + <h3>Entrepreneur</h3> + + <div class="form-group"> + <label for="founderName">Nom</label> + <input + id="founderName" + v-model="form.founder.userName" + type="text" + required + /> + </div> + <div class="form-group"> + <label for="founderSurname">Prénom</label> + <input + id="founderSurname" + v-model="form.founder.userSurname" + type="text" + required + /> + </div> + <div class="form-group"> + <label for="founderPrimaryMail">Email Principal</label> + <input + id="founderPrimaryMail" + v-model="form.founder.primaryMail" + type="email" + required + /> + </div> + <div class="form-group"> + <label for="founderSecondaryMail">Email Secondaire</label> + <input + id="founderSecondaryMail" + v-model="form.founder.secondaryMail" + type="email" + /> + </div> + <div class="form-group"> + <label for="founderPhoneNumber">Numéro de téléphone</label> + <input + id="founderPhoneNumber" + v-model="form.founder.phoneNumber" + type="tel" + required + /> + </div> + <div class="form-group"> + <label for="founderSchool">École</label> + <input + id="founderSchool" + v-model="form.founder.school" + type="text" + required + /> + </div> + <div class="form-group"> + <label for="founderCourse">Département</label> + <input + id="founderCourse" + v-model="form.founder.course" + type="text" + required + /> + </div> + <div class="form-group"> + <label for="founderSneeStatus">Statut étudiant entrepreneur</label> + <input + id="founderSneeStatus" + v-model="form.founder.sneeStatus" + type="checkbox" + /> + </div> + + <button type="submit">Soumettre</button> + </form> + </template> + + <script setup lang="ts"> + import { ref } from "vue"; + import { postApi } from "@/services/api"; + + const form = ref({ + name: '', + founder: { + userSurname: '', + userName: '', + primaryMail: '', + secondaryMail: '', + phoneNumber: '', + school: '', + course: '', + sneeStatus: false + } + }); + + function submitForm() { + postApi("/entrepreneur/projects/request", form.value); + } + </script> + + <style scoped> + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); + + .add-project-form { + font-family: 'Inter', sans-serif; + max-width: 600px; + margin: 0 auto; + padding: 20px; + background: #fff; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + /* Le reste reste inchangé */ + h2 { + margin-bottom: 20px; + font-size: 24px; + color: #333; + } + + h3 { + margin-top: 20px; + font-size: 20px; + color: #555; + } + + .form-group { + margin-bottom: 15px; + display: flex; + flex-direction: column; + } + + label { + font-weight: 500; + margin-bottom: 5px; + } + + input { + padding: 8px; + border-radius: 5px; + border: 1px solid #ccc; + font-size: 1em; + } + + input[type="checkbox"] { + width: auto; + margin-right: 10px; + } + + button { + background-color: #4caf50; + color: white; + padding: 10px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + width: 100%; + font-weight: 500; + } + + button:hover { + background-color: #45a049; + } + + button:active { + background-color: #388e3c; + } + + input[type="text"]:focus, + input[type="email"]:focus, + input[type="tel"]:focus { + border-color: #4CAF50; + outline: none; + } + </style> \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/errorWrapper.vue b/front/MyINPulse-front/src/views/errorWrapper.vue index 807dffe..10afc81 100644 --- a/front/MyINPulse-front/src/views/errorWrapper.vue +++ b/front/MyINPulse-front/src/views/errorWrapper.vue @@ -15,11 +15,13 @@ import ErrorModal from "@/components/errorModal.vue"; </template> <style scoped> -.error-wrapper { - position: absolute; - left: 70%; - //background-color: blue; - height: 100%; - width: 30%; + +.error-wrapper{ + position: absolute; + left: 70%; + /*background-color: blue;*/ + height: 100%; + width: 30%; + } </style>