24 Commits

Author SHA1 Message Date
83963cbdc3 ? 2025-04-14 19:29:06 +02:00
66338ee020 agenda + parcourir 2025-04-09 03:39:27 +02:00
8d62ebf0af l'icone agenda dans le site 2025-04-02 15:42:11 +02:00
fe06e20c03 first agenda 2025-04-02 12:57:53 +02:00
8394bf02f2 test front 2025-03-19 03:09:43 +01:00
f48b570494 fix: to composition api 2025-02-26 03:39:59 +01:00
0733f8d5af Merge branch 'front_foundation' of https://gitea.piair.dev/piair/MyINPulse into front_foundation 2025-02-25 22:53:26 +01:00
8071c01c5d fix: fixing the issues regarding the use of href 2025-02-25 22:53:15 +01:00
4ee3d9bc44 fix: data from api is now correctly displayed 2025-02-19 12:15:39 +01:00
d75d45e204 fix + feat: api test + error fix 2025-02-19 11:02:15 +01:00
9f3754776f fix: fixed the code for AdminMain 2025-02-18 22:17:57 +01:00
651fb2b1a1 feat: adding first fake api (use this cmd in fake_data reportory: json-server --watch db.json --port 5000) + fetching witch axios for now (npm install axios) 2025-02-18 07:20:58 +01:00
aa5988ce75 fix: emptyed the app.vue and did some code reorganisation 2025-02-17 23:48:28 +01:00
9ae18e1e4b feat : adding an agenda, not included to the main page yet 2025-02-17 21:51:27 +01:00
22ebb0e1f4 feat: enhancing canvass layout 2025-02-11 20:47:37 +01:00
09e4b3262f feat: switching to composition API standard 2025-02-11 19:56:52 +01:00
6a3d4239ab feat: canvas are now generic 2025-02-10 22:46:59 +01:00
9d71c93b5b feat: layout changes 2025-02-10 15:53:10 +01:00
5145b833ae feat: rendez-vous agenda for admin and user 2025-02-10 15:15:58 +01:00
4080cee818 Meeeerge...
Merge branch 'main' of https://gitea.piair.dev/piair/MyINPulse
2025-02-08 22:40:42 +01:00
f4d73654d1 some minor changes on the main page 2025-02-08 22:40:35 +01:00
4fda5513a9 error corrected 2025-02-08 20:33:03 +01:00
32407b0e8f conflits... 2025-02-08 20:23:43 +01:00
b30e1196f4 canvas included in the main page, still shiting with vue 2025-02-08 20:18:44 +01:00
120 changed files with 3291 additions and 4144 deletions

View File

@ -1,18 +0,0 @@
name: Format
on: [ push, pull_request ]
jobs:
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v2 minimum required
- uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- uses: axel-op/googlejavaformat-action@v3
with:
args: "--set-exit-if-changed --skip-sorting-imports --aosp -n"

View File

@ -1,24 +0,0 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install everything
working-directory: ./front/MyINPulse-front
run: npm i
- name: Run ESLint
working-directory: ./front/MyINPulse-front
run: npx eslint
- name: Run prettier
working-directory: ./front/MyINPulse-front
run: npx prettier src --check
- name: Build frontend
working-directory: ./front/MyINPulse-front
run: npm run build

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
.idea
keycloak/CAS/target
docker-compose.yaml
postgres/data

View File

@ -1,16 +1,13 @@
help:
@echo "make [clean dev-front prod dev-back dev]"
@echo "make [clean dev-front prod dev-back]"
clean:
@cp config/frontdev.env front/MyINPulse-front/.env
@cp config/frontdev.env .env
@cp config/frontdev.env MyINPulse-back/.env
@cp config/prod.docker-compose.yaml docker-compose.yaml
@docker compose down
@rm -f docker-compose.yaml
@rm -f .env
@rm -f front/MyINPulse-front/.env
@rm -f MyINPulse-back/.env
# Install npm packages
front/MyINPulse-front/.installed:
@ -21,37 +18,24 @@ vite: ./front/MyINPulse-front/.installed
dev-front: clean vite
@cp config/frontdev.env front/MyINPulse-front/.env
@cp config/frontdev.env .env
@cp config/frontdev.env MyINPulse-back/.env
@cp config/frontdev.front.env front/MyINPulse-front/.env
@cp config/frontdev.main.env .env
@cp config/frontdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d --build
@cd ./front/MyINPulse-front/ && npm run dev
prod: clean
@cp config/prod.env front/MyINPulse-front/.env
@cp config/prod.env .env
@cp config/prod.env .env
@cp config/prod.docker-compose.yaml docker-compose.yaml
@cp config/prod.front.env front/MyINPulse-front/.env
@cp config/prod.main.env .env
@cp config/frontdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d --build
dev-back:
@cp config/backdev.env front/MyINPulse-front/.env
@cp config/backdev.env .env
@cp config/backdev.env MyINPulse-back/.env
@cp config/backdev.front.env front/MyINPulse-front/.env
@cp config/backdev.main.env .env
@cp config/backdev.docker-compose.yaml docker-compose.yaml
@docker compose up -d --build
@echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)'
@echo "./gradlew bootRun --args='--server.port=8081'"
dev: clean vite
@cp config/dev.env front/MyINPulse-front/.env
@cp config/dev.env .env
@cp config/dev.env MyINPulse-back/.env
@cp config/dev.docker-compose.yaml docker-compose.yaml
@docker compose up -d --build
@echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)'
@echo "./gradlew bootRun --args='--server.port=8081'"
@cd ./front/MyINPulse-front/ && npm run dev &
@echo "cd MyINPulse-back"
@echo "./gradlew bootRun --args='--server.port=8081'"

View File

@ -20,10 +20,6 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

View File

@ -1,15 +1,29 @@
package enseirb.myinpulse;
import enseirb.myinpulse.security.KeycloakJwtRolesConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@SpringBootApplication
public class MyinpulseApplication {
public static void main(String[] args) {
SpringApplication.run(MyinpulseApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(MyinpulseApplication.class, args);
}
}

View File

@ -0,0 +1,43 @@
package enseirb.myinpulse.api;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.CrossOrigin;
import java.security.Principal;
@SpringBootApplication
@RestController
public class GetUserInfo {
// TODO: understand how to get data
@GetMapping("/getUserInfo")
public Object user(Principal principal) {
System.out.println("GetUserInfo + " + principal);
System.out.println(SecurityContextHolder.getContext().getAuthentication());
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random")
public boolean rand(){
System.err.println("HELLO");
return Math.random() > 0.5;
}
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random2")
public boolean rand2(){
System.err.println("HELLO2");
return Math.random() > 0.5;
}
@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS})
@GetMapping("/random3")
public boolean rand3(){
System.err.println("HELLO");
return Math.random() > 0.5;
}
}

View File

@ -1,107 +0,0 @@
/*
* Source: https://github.com/ChristianHuff-DEV/secure-spring-rest-api-using-keycloak/blob/main/src/main/java/io/betweendata/RestApi/security/oauth2/KeycloakJwtRolesConverter.java
* edited by Pierre Tellier
*/
package enseirb.myinpulse.config;
import static java.util.stream.Collectors.toSet;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
/** Prefix used for realm level roles. */
public static final String PREFIX_REALM_ROLE = "ROLE_REALM_";
/** Prefix used in combination with the resource (client) name for resource level roles. */
public static final String PREFIX_RESOURCE_ROLE = "ROLE_";
/** Name of the claim containing the realm level roles */
private static final String CLAIM_REALM_ACCESS = "realm_access";
/** Name of the claim containing the resources (clients) the user has access to. */
private static final String CLAIM_RESOURCE_ACCESS = "resource_access";
/** Name of the claim containing roles. (Applicable to realm and resource level.) */
private static final String CLAIM_ROLES = "roles";
@Override
public AbstractAuthenticationToken convert(Jwt source) {
return new JwtAuthenticationToken(
source,
Stream.concat(
new JwtGrantedAuthoritiesConverter().convert(source).stream(),
tokenRolesExtractor(source).stream())
.collect(toSet()));
}
/**
* Extracts the realm and resource level roles from a JWT token distinguishing between them
* using prefixes.
*/
public Collection<GrantedAuthority> tokenRolesExtractor(Jwt jwt) {
// Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// Realm roles
// Get the part of the access token that holds the roles assigned on realm level
Map<String, Collection<String>> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);
// Verify that the claim exists and is not empty
if (realmAccess != null && !realmAccess.isEmpty()) {
// From the realm_access claim get the roles
Collection<String> roles = realmAccess.get(CLAIM_ROLES);
// Check if any roles are present
if (roles != null && !roles.isEmpty()) {
// Iterate of the roles and add them to the granted authorities
Collection<GrantedAuthority> realmRoles =
roles.stream()
// Prefix all realm roles with "ROLE_realm_"
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
.collect(Collectors.toList());
grantedAuthorities.addAll(realmRoles);
}
}
// Resource (client) roles
// A user might have access to multiple resources all containing their own roles. Therefore,
// it is a map of
// resource each possibly containing a "roles" property.
Map<String, Map<String, Collection<String>>> resourceAccess =
jwt.getClaim(CLAIM_RESOURCE_ACCESS);
// Check if resources are assigned
if (resourceAccess != null && !resourceAccess.isEmpty()) {
// Iterate of all the resources
resourceAccess.forEach(
(resource, resourceClaims) -> {
// Iterate of the "roles" claim inside the resource claims
resourceClaims
.get(CLAIM_ROLES)
.forEach(
// Add the role to the granted authority prefixed with ROLE_
// and the name of the resource
role ->
grantedAuthorities.add(
new SimpleGrantedAuthority(
PREFIX_RESOURCE_ROLE
+ resource
+ "_"
+ role)));
});
}
return grantedAuthorities;
}
}

View File

@ -1,8 +1,6 @@
package enseirb.myinpulse.config;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
import org.springframework.beans.factory.annotation.Value;
import enseirb.myinpulse.security.KeycloakJwtRolesConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -14,62 +12,39 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Configuration
public class WebSecurityCustomConfiguration {
// CORS configuration
@Value("${VITE_APP_URL}")
private String frontendUrl;
/**
* Configure the CORS (Cross Origin Ressource Sharing -- a security feature) configuration. The
* only allowed website is the frontend, defined in the .env file.
*
* @return the CORS configuration used by the backend
*/
// TODO: make sure to only accept our own domains
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(frontendUrl));
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS"));
configuration.setAllowedHeaders(
Arrays.asList("authorization", "content-type", "x-auth-token"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type",
"x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* Configure the authorisation required for each path.
*
* <p>admin endpoints are under /admin/* and entrepreneur are under /entrepreneur/*
*
* <p>If endpoints dont require authentication, they are under /unauth/
*
* @param http automatically filled in by spring.
* @return a securityfilterchain, automatically used by spring.
* @throws Exception TODO: figure out when the exception are raised
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers("/entrepreneur/**", "/shared/**")
.access(hasRole("REALM_MyINPulse-entrepreneur"))
.requestMatchers("/admin/**", "/shared/**")
.access(hasRole("REALM_MyINPulse-admin"))
.requestMatchers("/unauth/**")
.permitAll()
.anyRequest()
.authenticated())
.oauth2ResourceServer(
oauth2 ->
oauth2.jwt(
jwt ->
jwt.jwtAuthenticationConverter(
new KeycloakJwtRolesConverter())));
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/random2").access(hasRole("REALM_MyINPulse-entrepreneur"))
.requestMatchers("/random").access(hasRole("REALM_MyINPulse-admin"))
.requestMatchers("/random3").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.
jwtAuthenticationConverter(new KeycloakJwtRolesConverter())));
return http.build();
}
}
}

View File

@ -1,102 +0,0 @@
package enseirb.myinpulse.controller;
import enseirb.myinpulse.model.*;
import enseirb.myinpulse.service.AdminApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
@SpringBootApplication
@RestController
public class AdminApi {
private final AdminApiService adminApiService;
@Autowired
AdminApi(AdminApiService adminApiService) {
this.adminApiService = adminApiService;
}
/**
* TODO: description
*
* @return a list of all project managed by the current admin user
*/
@GetMapping("/admin/projects")
public Iterable<Administrator> getProjects() {
return adminApiService.getProjects();
}
/**
* TODO: Why in admin instead of shared ? + desc
*
* @return a list of upcoming appointments for the current user
*/
@GetMapping("/admin/appointments/upcoming")
public Iterable<Appointment> getUpcomingAppointments(@AuthenticationPrincipal Jwt principal) {
return adminApiService.getUpcomingAppointments(principal.getClaimAsString("email"));
}
/**
* TODO: description
*
* @return a list of current unvalidated projects, waiting to be accepted
*/
@GetMapping("/admin/projects/pending")
public Iterable<Project> getPendingProjects() {
return adminApiService.getPendingProjects();
}
/**
* Endpoint used to make a decision about a project.
*
* <p>The decision must contains the administrator
*
* @return the status code of the request
*/
@PostMapping("/admin/projects/decision")
public void validateProject(@RequestBody ProjectDecision decision) {
adminApiService.validateProject(decision);
}
/**
* Endpoint used to manually add a project by an admin
*
* @return the status code of the request
*/
@PostMapping("/admin/project/add")
public void addNewProject(@RequestBody Project project) {
adminApiService.addNewProject(project);
}
/**
* TODO: shouldn't it be an PUT request ? / What is the rerun type
*
* <p>Endpoint used to add a new report to an appointment
*
* @return the status code of the request
*/
@PostMapping("/admin/appoitements/report/{appointmentId}")
public void createAppointmentReport(
@PathVariable String appointmentId,
@RequestBody Report report,
@AuthenticationPrincipal Jwt principal) {
adminApiService.createAppointmentReport(
appointmentId, report, principal.getClaimAsString("email"));
}
/**
* TODO: Shouldn't a project be kept in history ? 2 different endpoints ?
*
* <p>Endpoint used to completely remove a project.
*
* @return the status code of the request
*/
@DeleteMapping("/admin/projects/remove/{projectId}")
public void deleteProject(@PathVariable String projectId) {
adminApiService.deleteProject(projectId);
}
}

View File

@ -1,81 +0,0 @@
package enseirb.myinpulse.controller;
import enseirb.myinpulse.model.LCSection;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.service.EntrepreneurApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
@SpringBootApplication
@RestController
public class EntrepreneurApi {
private final EntrepreneurApiService entrepreneurApiService;
@Autowired
EntrepreneurApi(EntrepreneurApiService entrepreneurApiService) {
this.entrepreneurApiService = entrepreneurApiService;
}
/**
* TODO: check return type
*
* <p>Endpoint used to update a LC section.
*
* @return status code
*/
@PutMapping("/entrepreneur/lcsection/modify/{sectionId}")
public void editLCSection(
@PathVariable String sectionId,
@RequestBody LCSection section,
@AuthenticationPrincipal Jwt principal) {
entrepreneurApiService.editLCSection(
sectionId, section, principal.getClaimAsString("email"));
}
/**
* TODO: checkReturn Type
*
* <p>Endpoint used to delete a LC section
*
* @return status code
*/
@DeleteMapping("/entrepreneur/lcsection/remove/{sectionId}")
public void removeLCSection(
@PathVariable String sectionId, @AuthenticationPrincipal Jwt principal) {
entrepreneurApiService.removeLCSection(sectionId, principal.getClaimAsString("email"));
}
/**
* TODO: check return type
*
* <p>Endpoint used to create a new LC section
*
* @return status code
*/
@PostMapping("/entrepreneur/lcsection/add/{sectionId}")
public void addLCSection(
@PathVariable String sectionId,
@RequestBody LCSection section,
@AuthenticationPrincipal Jwt principal) {
entrepreneurApiService.addLCSection(
sectionId, section, principal.getClaimAsString("email"));
}
/**
* TODO: check return type
*
* <p>Endpoint used to request the creation of a new project
*
* @return status code
*/
@PostMapping("/entrepreneur/project/request")
public void requestNewProject(
@RequestBody Project project, @AuthenticationPrincipal Jwt principal) {
entrepreneurApiService.requestNewProject(project, principal.getClaimAsString("email"));
}
}

View File

@ -1,96 +0,0 @@
package enseirb.myinpulse.controller;
import enseirb.myinpulse.model.*;
import enseirb.myinpulse.service.SharedApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
@SpringBootApplication
@RestController
public class SharedApi {
private final SharedApiService sharedApiService;
@Autowired
SharedApi(SharedApiService sharedApiService) {
this.sharedApiService = sharedApiService;
}
/**
* TODO: It does not looks like a good id to have the title and the date in the url. What even
* TODO: is the title btw ? if this is the LC section, wouldn't it be better to use an ID ?
* TODO: choose return type, cf comment in LCSection
*
* <p>Endpoint used to get the data inside the lean canvas
*
* @return a list of lean canvas sections
*/
@GetMapping("/shared/project/lcsection/{projectId}/{title}/{date}")
public Iterable<SectionCell> getLCSection(
@PathVariable("projectId") String projectId,
@PathVariable("title") String title,
@PathVariable("date") String date,
@AuthenticationPrincipal Jwt principal) {
return sharedApiService.getLCSection(
projectId, title, date, principal.getClaimAsString("email"));
}
/**
* Endpoint used to get entrepreneurs details
*
* @return a list of all entrepreneurs in a project
*/
@GetMapping("/shared/entrepreneurs/{projectId}")
public Iterable<Entrepreneur> getEntrepreneursByProjectId(
@PathVariable int projectId, @AuthenticationPrincipal Jwt principal) {
return sharedApiService.getEntrepreneursByProjectId(
projectId, principal.getClaimAsString("email"));
}
/**
* Endpoint used to get the administrator of a project.
*
* @return a list of all project managed by the current admin user
*/
@GetMapping("/shared/projects/admin/{projectId}")
public Iterable<Administrator> getAdminByProjectId(
@PathVariable int projectId, @AuthenticationPrincipal Jwt principal) {
return sharedApiService.getAdminByProjectId(projectId, principal.getClaimAsString("email"));
}
/**
* Endpoint used to get all appointments of a single project.
*
* @return a list of all appointments.
*/
@GetMapping("/shared/projects/appointments/{projectId}")
public Iterable<Appointment> getAppointmentsByProjectId(
@PathVariable int projectId, @AuthenticationPrincipal Jwt principal) {
return sharedApiService.getAppointmentsByProjectId(
projectId, principal.getClaimAsString("email"));
}
/**
* Endpoint used to generate a PDF report
*
* @return a PDF file? TODO: how does that works ?
*/
@GetMapping("/shared/projects/appointments/report/{appointmentId}")
public void getPDFReport(
@PathVariable int appointmentId, @AuthenticationPrincipal Jwt principal) {
sharedApiService.getPDFReport(appointmentId, principal.getClaimAsString("email"));
}
/**
* @return TODO
*/
@PostMapping("/shared/appointment/request")
public void createAppointmentRequest(
@RequestBody Appointment appointment, @AuthenticationPrincipal Jwt principal) {
sharedApiService.createAppointmentRequest(appointment, principal.getClaimAsString("email"));
}
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.exception;
public class RoleNotFoudException extends RuntimeException {
public RoleNotFoudException(String message) {
super(message);
}
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@ -1,43 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.persistence.PrimaryKeyJoinColumn;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "administrator")
@PrimaryKeyJoinColumn(name = "idAdministrator", referencedColumnName = "idUser")
public class Administrator extends User {
@OneToMany(mappedBy = "projectAdministrator", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Project> listProject = new ArrayList<>();
/*@OneToMany(mappedBy = "administratorSectionCell", fetch = FetchType.LAZY, orphanRemoval = true)
private List<SectionCell> listSectionCell = new ArrayList<>();*/
// should now be useless
@OneToMany(mappedBy = "administratorAnnotation", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Annotation> listAnnotation = new ArrayList<>();
/*@OneToMany(mappedBy = "administratorAppointment", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Appointment> listAppointment = new ArrayList<>();*/
// should now be useless
@OneToOne(mappedBy = "administratorAppointment", fetch = FetchType.LAZY, orphanRemoval = true)
private MakeAppointment makeAppointment;
public Administrator() {}
public Administrator(
Long idUser,
String userSurname,
String username,
String mainMail,
String secondaryMail,
String phoneNumber) {
super(idUser, userSurname, username, mainMail, secondaryMail, phoneNumber);
}
}

View File

@ -1,39 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
@Entity
@Table(name = "annotation")
public class Annotation {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idAnnotation;
private String comment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idSectionCell")
private SectionCell sectionCellAnnotation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAdministrator")
private Administrator administratorAnnotation;
public Annotation() {}
public Annotation(Long idAnnotation, String commentary) {
this.idAnnotation = idAnnotation;
this.comment = comment;
}
public String getComment() {
return comment;
}
public void setCommentary(String comment) {
this.comment = comment;
}
}

View File

@ -1,112 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "appointment")
public class Appointment {
/*@OneToMany(mappedBy = "appointmentEntrepreneurs", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Entrepreneur> listEntrepreneur =
new ArrayList<>(); */
// should now be useless
@OneToMany(mappedBy = "appointmentReport", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Report> listReport = new ArrayList<>();
@ManyToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.ALL})
@JoinTable(
name = "concern",
joinColumns = @JoinColumn(name = "idAppointment"),
inverseJoinColumns = @JoinColumn(name = "idSectionCell"))
List<SectionCell> listSectionCell = new ArrayList<>();
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idAppointment;
private LocalDate appointmentDate;
private LocalTime appointmentTime;
private LocalTime appointmentDuration;
@Column(length = 255)
private String appointmentPlace;
private String appointmentSubject;
public Appointment() {}
public Appointment(
Long idAppointment,
LocalDate appointmentDate,
LocalTime appointmentTime,
LocalTime appointmentDuration,
String appointmentPlace,
String appointmentSubject) {
this.idAppointment = idAppointment;
this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime;
this.appointmentDuration = appointmentDuration;
this.appointmentPlace = appointmentPlace;
this.appointmentSubject = appointmentSubject;
}
public Long getIdAppointment() {
return idAppointment;
}
public void setIdAppointment(Long idAppointment) {
this.idAppointment = idAppointment;
}
public LocalDate getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(LocalDate appointmentDate) {
this.appointmentDate = appointmentDate;
}
public LocalTime getAppointmentTime() {
return appointmentTime;
}
public void setAppointmentTime(LocalTime appointmentTime) {
this.appointmentTime = appointmentTime;
}
public LocalTime getAppointmentDuration() {
return appointmentDuration;
}
public void setAppointmentDuration(LocalTime appointmentDuration) {
this.appointmentDuration = appointmentDuration;
}
public String getAppointmentPlace() {
return appointmentPlace;
}
public void setAppointmentPlace(String appointmentPlace) {
this.appointmentPlace = appointmentPlace;
}
public String getAppointmentSubject() {
return appointmentSubject;
}
public void setAppointmentSubject(String appointmentSubject) {
this.appointmentSubject = appointmentSubject;
}
}

View File

@ -1,8 +0,0 @@
package enseirb.myinpulse.model;
public class DelAppointment {
int validated;
int[] akserId;
int[] destId;
String date; // TODO: date type ?
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.model;
public class DelProject {
int projectId;
String projectName;
String projectDescription;
}

View File

@ -1,6 +0,0 @@
package enseirb.myinpulse.model;
public class DelReport {
int projectId;
String reportContent;
}

View File

@ -1,78 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "entrepreneur")
@PrimaryKeyJoinColumn(name = "idEntrepreneur", referencedColumnName = "idUser")
public class Entrepreneur extends User {
@Column(length = 255)
private String school;
@Column(length = 255)
private String course;
private boolean sneeStatus;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idProjectParticipation", referencedColumnName = "idProject")
private Project projectParticipation;
// @Column(insertable=false, updatable=false)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idProjectProposed", referencedColumnName = "idProject")
private Project projectProposed;
/*@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAppointment")
private Appointment appointmentEntrepreneur;*/
// should now be useless
@OneToOne(mappedBy = "entrepreneurAppointment", fetch = FetchType.LAZY, orphanRemoval = true)
private MakeAppointment makeAppointment;
public Entrepreneur() {}
public Entrepreneur(
Long idUser,
String userSurname,
String username,
String mainMail,
String secondaryMail,
String phoneNumber,
String school,
String course,
boolean sneeStatus) {
super(idUser, userSurname, username, mainMail, secondaryMail, phoneNumber);
this.school = school;
this.course = course;
this.sneeStatus = sneeStatus;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public String getCourse() {
return course;
}
public void setCourse(String course) {
this.course = course;
}
public boolean isSneeStatus() {
return sneeStatus;
}
public void setSneeStatus(boolean statusSnee) {
this.sneeStatus = sneeStatus;
}
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.model;
// TODO: is this redundant with the Section class from the database ?
// TODO: In the one hand it represent the same data, and on the other it should be much lighter.
// TODO: btw why does a LC section have an administrator ?
public class LCSection {}

View File

@ -1,28 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
@Entity
@Table(name = "make_appointment")
public class MakeAppointment {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idMakeAppointment;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAdministrator")
private Administrator administratorAppointment;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idEntrepreneur")
private Entrepreneur entrepreneurAppointment;
public MakeAppointment() {}
public MakeAppointment(Long idMakeAppointment) {
this.idMakeAppointment = idMakeAppointment;
}
}

View File

@ -1,96 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "project")
public class Project {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idProject;
@Column(length = 255)
private String projectName;
private byte[] logo;
private LocalDate creationDate;
@Column(length = 255)
private String projectStatus;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAdministrator")
private Administrator projectAdministrator;
@OneToMany(mappedBy = "projectParticipation", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Entrepreneur> listEntrepreneurParticipation = new ArrayList<>();
@OneToOne(mappedBy = "projectProposed", fetch = FetchType.LAZY, orphanRemoval = true)
private Entrepreneur entrepreneurProposed;
@OneToMany(mappedBy = "projectSectionCell", fetch = FetchType.LAZY, orphanRemoval = true)
private List<SectionCell> listSectionCell = new ArrayList<>();
public Project() {}
public Project(
Long idProject,
String projectName,
byte[] logo,
LocalDate creationDate,
String projectStatus) {
this.idProject = idProject;
this.projectName = projectName;
this.logo = logo;
this.creationDate = creationDate;
this.projectStatus = projectStatus;
}
public Long getIdProject() {
return idProject;
}
public void setIdProject(Long idProject) {
this.idProject = idProject;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public byte[] getLogo() {
return logo;
}
public void setLogo(byte[] logo) {
this.logo = logo;
}
public LocalDate getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDate creationDate) {
this.creationDate = creationDate;
}
public String getProjectStatus() {
return projectStatus;
}
public void setProjectStatus(String projectStatus) {
this.projectStatus = projectStatus;
}
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.model;
public class ProjectDecision {
int projectId;
int adminId;
int isAccepted;
}

View File

@ -1,38 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
@Entity
@Table(name = "report")
public class Report {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idReport;
private String reportContent;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAppointment")
private Appointment appointmentReport;
public Report() {}
public Report(Long idReport, String reportContent) {
this.idReport = idReport;
this.reportContent = reportContent;
}
public String getReportContent() {
return reportContent;
}
public void setReportContent(String reportContent) {
this.reportContent = reportContent;
}
}

View File

@ -1,7 +0,0 @@
package enseirb.myinpulse.model;
public class RoleRepresentation {
public String id;
public String name;
public String description;
}

View File

@ -1,85 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "section_cell")
public class SectionCell {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idSectionCell;
@Column(length = 255)
private String title;
private String contentSectionCell;
private LocalDateTime modificationDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idProject")
private Project projectSectionCell;
/*@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idAdministrator")
private Administrator administratorSectionCell;*/
// should now be useless
@ManyToMany(mappedBy = "listSectionCell")
private List<Appointment> appointment = new ArrayList<>();
@OneToMany(mappedBy = "sectionCellAnnotation", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Annotation> listAnnotation = new ArrayList<>();
public SectionCell() {}
public SectionCell(
Long idSectionCell,
String title,
String contentSectionCell,
LocalDateTime modificationDate) {
this.idSectionCell = idSectionCell;
this.title = title;
this.contentSectionCell = contentSectionCell;
this.modificationDate = modificationDate;
}
public Long getIdSectionCell() {
return idSectionCell;
}
public void setIdSectionCell(Long idSectionCell) {
this.idSectionCell = idSectionCell;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContentSectionCell() {
return contentSectionCell;
}
public void setContentSectionCell(String contentSectionCell) {
this.contentSectionCell = contentSectionCell;
}
public LocalDateTime getModificationDate() {
return modificationDate;
}
public void setModificationDate(LocalDateTime modificationDate) {
this.modificationDate = modificationDate;
}
}

View File

@ -1,95 +0,0 @@
package enseirb.myinpulse.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
@Entity
@Table(name = "user_inpulse")
@Inheritance(strategy = InheritanceType.JOINED)
public class User {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idUser;
@Column(length = 255)
private String userSurname;
@Column(length = 255)
private String userName;
@Column(length = 255)
private String mainMail;
@Column(length = 255)
private String secondaryMail;
@Column(length = 20)
private String phoneNumber;
public User() {}
public User(
Long idUser,
String userSurname,
String userName,
String mainMail,
String secondaryMail,
String phoneNumber) {
this.idUser = idUser;
this.userSurname = userSurname;
this.userName = userName;
this.mainMail = mainMail;
this.secondaryMail = secondaryMail;
this.phoneNumber = phoneNumber;
}
public Long getIdUser() {
return idUser;
}
public void setIdUser(Long idUser) {
this.idUser = idUser;
}
public String getUserSurname() {
return userSurname;
}
public void setUserSurname(String userSurname) {
userSurname = userSurname;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
userName = userName;
}
public String getMainMail() {
return mainMail;
}
public void setMainMail(String mainMail) {
this.mainMail = mainMail;
}
public String getSecondaryMail() {
return secondaryMail;
}
public void setSecondaryMail(String secondaryMail) {
this.secondaryMail = secondaryMail;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
phoneNumber = phoneNumber;
}
}

View File

@ -1,6 +0,0 @@
package enseirb.myinpulse.model;
public class UserRepresentation {
public String id;
public String name;
}

View File

@ -1,14 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Administrator;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface AdministratorRepository extends JpaRepository<Administrator, Long> {
/* @Query("SELECT a from Administrators a")
Administrator findAllAdministrator(); */
}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Annotation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface AnnotationRepository extends JpaRepository<Annotation, Long> {}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Appointment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {}

View File

@ -1,14 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Entrepreneur;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface EntrepreneurRepository extends JpaRepository<Entrepreneur, Long> {
/* @Query("SELECT e from Entrepreneur e")
Entrepreneur findAllEntrepreneurl(); */
}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.MakeAppointment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface MakeAppointmentRepository extends JpaRepository<MakeAppointment, Long> {}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface ProjectRepository extends JpaRepository<Project, Long> {}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Report;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface ReportRepository extends JpaRepository<Report, Integer> {}

View File

@ -1,9 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.SectionCell;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface SectionCellRepository extends JpaRepository<SectionCell, Long> {}

View File

@ -1,14 +0,0 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface UserRepository extends JpaRepository<User, Long> {
/* @Query("SELECT u from User u")
User findAllUser(); */
}

View File

@ -0,0 +1,98 @@
package enseirb.myinpulse.security;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toSet;
public class KeycloakJwtRolesConverter implements Converter<Jwt, AbstractAuthenticationToken> {
/**
* Prefix used for realm level roles.
*/
public static final String PREFIX_REALM_ROLE = "ROLE_REALM_";
/**
* Prefix used in combination with the resource (client) name for resource level roles.
*/
public static final String PREFIX_RESOURCE_ROLE = "ROLE_";
/**
* Name of the claim containing the realm level roles
*/
private static final String CLAIM_REALM_ACCESS = "realm_access";
/**
* Name of the claim containing the resources (clients) the user has access to.
*/
private static final String CLAIM_RESOURCE_ACCESS = "resource_access";
/**
* Name of the claim containing roles. (Applicable to realm and resource level.)
*/
private static final String CLAIM_ROLES = "roles";
@Override
public AbstractAuthenticationToken convert(Jwt source)
{
return new JwtAuthenticationToken(source, Stream.concat(new JwtGrantedAuthoritiesConverter().convert(source)
.stream(), TEMPORARNAME(source).stream())
.collect(toSet()));
}
/**
* Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes.
*/
public Collection<GrantedAuthority> TEMPORARNAME(Jwt jwt) {
// Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// Realm roles
// Get the part of the access token that holds the roles assigned on realm level
Map<String, Collection<String>> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);
// Verify that the claim exists and is not empty
if (realmAccess != null && !realmAccess.isEmpty()) {
// From the realm_access claim get the roles
Collection<String> roles = realmAccess.get(CLAIM_ROLES);
// Check if any roles are present
if (roles != null && !roles.isEmpty()) {
// Iterate of the roles and add them to the granted authorities
Collection<GrantedAuthority> realmRoles = roles.stream()
// Prefix all realm roles with "ROLE_realm_"
.map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role))
.collect(Collectors.toList());
grantedAuthorities.addAll(realmRoles);
}
}
// Resource (client) roles
// A user might have access to multiple resources all containing their own roles. Therefore, it is a map of
// resource each possibly containing a "roles" property.
Map<String, Map<String, Collection<String>>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS);
// Check if resources are assigned
if (resourceAccess != null && !resourceAccess.isEmpty()) {
// Iterate of all the resources
resourceAccess.forEach((resource, resourceClaims) -> {
// Iterate of the "roles" claim inside the resource claims
resourceClaims.get(CLAIM_ROLES).forEach(
// Add the role to the granted authority prefixed with ROLE_ and the name of the resource
role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role))
);
});
}
return grantedAuthorities;
}
}

View File

@ -1,45 +0,0 @@
package enseirb.myinpulse.service;
import enseirb.myinpulse.model.*;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
public class AdminApiService {
// TODO
public Iterable<Administrator> getProjects() {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public Iterable<Appointment> getUpcomingAppointments(String email) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public Iterable<Project> getPendingProjects() {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public void validateProject(ProjectDecision decision) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public void addNewProject(Project project) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public void createAppointmentReport(String appointmentId, Report report, String email) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public void deleteProject(String projectId) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
}

View File

@ -1,29 +0,0 @@
package enseirb.myinpulse.service;
import enseirb.myinpulse.model.LCSection;
import enseirb.myinpulse.model.Project;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@Service
public class EntrepreneurApiService {
public void editLCSection(String sectionId, LCSection section, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public void removeLCSection(String sectionId, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public void addLCSection(String sectionId, LCSection section, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public void requestNewProject(Project project, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
}

View File

@ -1,135 +0,0 @@
package enseirb.myinpulse.service;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import enseirb.myinpulse.exception.UserNotFoundException;
import enseirb.myinpulse.model.RoleRepresentation;
import enseirb.myinpulse.model.UserRepresentation;
import org.springframework.web.client.RestClient;
import javax.management.relation.RoleNotFoundException;
public class KeycloakApi {
static final String keycloakUrl;
static final String realmName;
static {
if (System.getenv("VITE_KEYCLOAK_URL") == null) {
System.exit(-1);
}
keycloakUrl = System.getenv("VITE_KEYCLOAK_URL");
}
static {
if (System.getenv("VITE_KEYCLOAK_REALM") == null) {
System.exit(-1);
}
realmName = System.getenv("VITE_KEYCLOAK_REALM");
}
/**
* Uses Keycloak API to retrieve a role representation of a role by its name
*
* @param roleName name of the role
* @param bearer authorization header used by the client to authenticate to keycloak
*/
public static RoleRepresentation getRoleRepresentationByName(String roleName, String bearer)
throws RoleNotFoundException {
RoleRepresentation[] response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.get()
.uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName)
.retrieve()
.body(RoleRepresentation[].class);
if (response == null || response.length == 0) {
throw new RoleNotFoundException("Role not found");
}
return response[0];
}
/**
* Use keycloak API to to retreive a userID via his name or email.
*
* @param username username or mail of the user
* @param bearer bearer of the user, allowing access to database
* @return the userid, as a String
* @throws UserNotFoundException
*/
public static String getUserIdByName(String username, String bearer)
throws UserNotFoundException {
UserRepresentation[] response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.get()
.uri(
"/admin/realms/{realmName}/users?username={username}",
realmName,
username)
.retrieve()
.body(UserRepresentation[].class);
if (response == null || response.length == 0) {
throw new UserNotFoundException("User not found");
}
return response[0].id;
}
/**
* TODO: check for error
*
* <p>Set a keycloak role to a keycloak user.
*
* <p>Usual roles should be `MyINPulse-admin` and `MyINPulse-entrepreneur`
*
* @param username
* @param roleName
* @param bearer
* @throws RoleNotFoundException
* @throws UserNotFoundException
*/
public static void setRoleToUser(String username, String roleName, String bearer)
throws RoleNotFoundException, UserNotFoundException {
RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer);
String userId = getUserIdByName(username, bearer);
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.post()
.uri(
"/admin/realms/${realmName}/users/${userId}/role-mappings/realm",
realmName,
userId)
.body(roleRepresentation)
.contentType(APPLICATION_JSON)
.retrieve();
}
/**
* Delete a user from Keycloak database. TODO: check the bearer permission.
*
* @param username
* @param bearer
* @throws UserNotFoundException
*/
public static void deleteUser(String username, String bearer) throws UserNotFoundException {
String userId = getUserIdByName(username, bearer);
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.build()
.delete()
.uri("/admin/realms/${realmName}/users/${userId}", realmName, userId)
.retrieve();
}
}

View File

@ -1,36 +0,0 @@
package enseirb.myinpulse.service;
import enseirb.myinpulse.model.*;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@Service
public class SharedApiService {
public Iterable<SectionCell> getLCSection(
String projectId, String title, String date, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public Iterable<Entrepreneur> getEntrepreneursByProjectId(int projectId, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public Iterable<Administrator> getAdminByProjectId(int projectId, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public Iterable<Appointment> getAppointmentsByProjectId(int projectId, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public void getPDFReport(int appointmentId, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
public void createAppointmentRequest(Appointment appointment, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
}

View File

@ -1,66 +0,0 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.repository.ProjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.Optional;
@Service
public class ProjectService {
private final ProjectRepository projectRepository;
@Autowired
ProjectService(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
public Iterable<Project> getAllProjects() {
return this.projectRepository.findAll();
}
// TODO: change error
public Project getProjectById(Long id) {
Optional<Project> project = this.projectRepository.findById(id);
if (project.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce projet n'existe pas");
}
return project.get();
}
// TODO: validation
public Project addNewProject(Project project) {
return this.projectRepository.save(project);
}
public Project updateProject(
Long id,
String projectName,
byte[] logo,
LocalDate creationDate,
String projectStatus) {
Optional<Project> project = this.projectRepository.findById(id);
if (project.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce projet n'existe pas");
}
if (projectName != null) {
project.get().setProjectName(projectName);
}
if (logo != null) {
project.get().setLogo(logo);
}
if (creationDate != null) {
project.get().setCreationDate(creationDate);
}
if (projectStatus != null) {
project.get().setProjectStatus(projectStatus);
}
return this.projectRepository.save(project.get());
}
}

View File

@ -1,34 +0,0 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Report;
import enseirb.myinpulse.repository.ReportRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@Service
public class ReportService {
private final ReportRepository reportRepository;
@Autowired
ReportService(ReportRepository reportRepository) {
this.reportRepository = reportRepository;
}
Report getReportById(int id) {
Optional<Report> report = reportRepository.findById(id);
if (report.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce compte rendu n'existe pas");
}
return report.get();
}
// TODO: do some validation
void saveReport(Report report) {
reportRepository.save(report);
}
}

View File

@ -1,38 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.repository.AdministratorRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@RestController
public class AdministratorController {
@Autowired AdministratorRepository administratorRepository;
@GetMapping("/Administrator")
@ResponseBody
public Iterable<Administrator> allAdministrators() {
return this.administratorRepository.findAll();
}
@GetMapping("/Administrator/{id}")
public Administrator getAdministratorById(@PathVariable Long id) {
Optional<Administrator> administrator = this.administratorRepository.findById(id);
if (administrator.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cet administrateur n'existe pas");
}
return administrator.get();
}
@PostMapping("/Administrateurs")
public Administrator addAdministrator(@RequestBody Administrator administrator) {
return this.administratorRepository.save(administrator);
}
}

View File

@ -1,69 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import enseirb.myinpulse.model.Appointment;
import enseirb.myinpulse.repository.AppointmentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Optional;
@RestController
public class AppointmentController {
@Autowired AppointmentRepository appointmentRepository;
@GetMapping("/Appointment")
@ResponseBody
public Iterable<Appointment> allAppointments() {
return this.appointmentRepository.findAll();
}
@GetMapping("/Appointment/{id}")
public Appointment getAppointmentById(@PathVariable Long id) {
Optional<Appointment> appointment = this.appointmentRepository.findById(id);
if (appointment.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce rendez vous n'existe pas");
}
return appointment.get();
}
@PostMapping("/Appointment")
public Appointment addAppointment(@RequestBody Appointment appointment) {
return this.appointmentRepository.save(appointment);
}
@PostMapping("/Appointment/{id}")
public Appointment updateAppointment(
@PathVariable Long id,
LocalDate appointmentDate,
LocalTime appointmentTime,
LocalTime appointmentDuration,
String appointmentPlace,
String appointmentSubject) {
Optional<Appointment> appointment = this.appointmentRepository.findById(id);
if (appointment.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce rendez vous n'existe pas");
}
if (appointmentDate != null) {
appointment.get().setAppointmentDate(appointmentDate);
}
if (appointmentTime != null) {
appointment.get().setAppointmentTime(appointmentTime);
}
if (appointmentDuration != null) {
appointment.get().setAppointmentDuration(appointmentDuration);
}
if (appointmentPlace != null) {
appointment.get().setAppointmentPlace(appointmentPlace);
}
if (appointmentSubject != null) {
appointment.get().setAppointmentSubject(appointmentSubject);
}
return this.appointmentRepository.save(appointment.get());
}
}

View File

@ -1,58 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.repository.EntrepreneurRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@RestController
public class EntrepreneurController {
@Autowired EntrepreneurRepository entrepreneurRepository;
@GetMapping("/Entrepreneur")
@ResponseBody
public Iterable<Entrepreneur> allEntrepreneurs() {
return this.entrepreneurRepository.findAll();
}
@GetMapping("/Entrepreneur/{id}")
public Entrepreneur getEntrepreneurById(@PathVariable Long id) {
Optional<Entrepreneur> entrepreneur = entrepreneurRepository.findById(id);
if (entrepreneur.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cet entrepreneur n'existe pas");
}
return entrepreneur.get();
}
@PostMapping("/Entrepreneur")
public Entrepreneur addEntrepreneur(@RequestBody Entrepreneur entrepreneur) {
return this.entrepreneurRepository.save(entrepreneur);
}
@PostMapping("/Entrepreneur/{id}")
public Entrepreneur updateEntrepreneur(
@PathVariable Long id, String school, String course, Boolean sneeStatus) {
Optional<Entrepreneur> entrepreneur = entrepreneurRepository.findById(id);
if (entrepreneur.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cet entrepreneur n'existe pas");
}
if (school != null) {
entrepreneur.get().setSchool(school);
}
if (course != null) {
entrepreneur.get().setCourse(course);
}
if (sneeStatus != null) {
entrepreneur.get().setSneeStatus(sneeStatus);
}
return this.entrepreneurRepository.save(entrepreneur.get());
}
}

View File

@ -1,43 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import org.springframework.web.bind.annotation.*;
@RestController
public class ReportController {
/*
private final ReportRepository reportRepository;
@Autowired
public ReportController(ReportRepository reportRepository) {
this.reportRepository = reportRepository;
}
@GetMapping("/Report")
@ResponseBody
public Iterable<Report> allReports() {
System.out.println("\n\n");
System.out.println(ReportRepository);
System.out.println("\n\n");
return this.reportRepository.findAll();
}
@PostMapping("/Report")
public Report addReport(@RequestBody Report report) {
return this.reportRepository.save(report);
}
@PostMapping("/Report/{id}")
public Report updateProject(@PathVariable Long id, String reportContent) {
Optional<Report> report = this.reportRepository.findById(id);
if (report.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce compte rendu n'existe pas");
}
if (reportContent != null) {
report.get().setReportContent(reportContent);
}
return this.reportRepository.save(report.get());
}
*/
}

View File

@ -1,62 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.repository.SectionCellRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.Optional;
@RestController
public class SectionCellController {
@Autowired SectionCellRepository sectionCellRepository;
@GetMapping("/SectionCell")
@ResponseBody
public Iterable<SectionCell> allSectionCells() {
return this.sectionCellRepository.findAll();
}
@GetMapping("/SectionCell/{id}")
public SectionCell getSectionCellById(@PathVariable Long id) {
Optional<SectionCell> sectionCell = this.sectionCellRepository.findById(id);
if (sectionCell.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas");
}
return sectionCell.get();
}
@PostMapping("/SectionCell")
public SectionCell addSectionCell(@RequestBody SectionCell sectionCell) {
return this.sectionCellRepository.save(sectionCell);
}
@PostMapping("/SectionCell/{id}")
public SectionCell updateSectionCell(
@PathVariable Long id,
String title,
String contentSectionCell,
LocalDateTime modificationDate) {
Optional<SectionCell> sectionCell = this.sectionCellRepository.findById(id);
if (sectionCell.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas");
}
if (title != null) {
sectionCell.get().setTitle(title);
}
if (contentSectionCell != null) {
sectionCell.get().setContentSectionCell(contentSectionCell);
}
if (modificationDate != null) {
sectionCell.get().setModificationDate(modificationDate);
}
return this.sectionCellRepository.save(sectionCell.get());
}
}

View File

@ -1,67 +0,0 @@
package enseirb.myinpulse.service.database.old_controllers_to_convert_to_services;
import enseirb.myinpulse.model.User;
import enseirb.myinpulse.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@RestController
public class UserController {
@Autowired UserRepository userRepository;
@GetMapping("/User")
@ResponseBody
public Iterable<User> allUsers() {
return this.userRepository.findAll();
}
@GetMapping("/User/{id}")
public User getUserById(@PathVariable Long id) {
Optional<User> user = userRepository.findById(id);
if (user.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cet utilisateur n'existe pas");
}
return user.get();
}
@PostMapping("/User")
public User addUser(@RequestBody User user) {
return this.userRepository.save(user);
}
@PostMapping("/User/{id}")
public User updateUser(
@PathVariable Long id,
String userSurname,
String userName,
String mainMail,
String secondaryMail,
String phoneNumber) {
Optional<User> user = userRepository.findById(id);
if (user.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cet utilisateur n'existe pas");
}
if (userName != null) {
user.get().setUserName(userName);
}
if (userSurname != null) {
user.get().setUserSurname(userSurname);
}
if (mainMail != null) {
user.get().setMainMail(mainMail);
}
if (secondaryMail != null) {
user.get().setSecondaryMail(secondaryMail);
}
if (phoneNumber != null) {
user.get().setPhoneNumber(phoneNumber);
}
return this.userRepository.save(user.get());
}
}

View File

@ -2,7 +2,3 @@ spring.application.name=myinpulse
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:7080/realms/test/protocol/openid-connect/certs
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:7080/realms/test
logging.level.org.springframework.security=DEBUG
spring.datasource.url=jdbc:postgresql://${DATABASE_URL}/${BACKEND_DB}
spring.datasource.username=${BACKEND_USER}
spring.datasource.password=${BACKEND_PASSWORD}
spring.jpa.hibernate.ddl-auto=update

View File

@ -1,99 +0,0 @@
TRUNCATE project, user_inpulse, entrepreneur, administrator, section_cell, appointment, report, annotation CASCADE;
SELECT setval('annotation_id_annotation_seq', 1, false);
SELECT setval('appointment_id_appointment_seq', 1, false);
SELECT setval('make_appointment_id_make_appointment_seq', 1, false);
SELECT setval('project_id_project_seq', 1, false);
SELECT setval('report_id_report_seq', 1, false);
SELECT setval('section_cell_id_section_cell_seq', 1, false);
SELECT setval('user_inpulse_id_user_seq', 1, false);
INSERT INTO user_inpulse (user_surname, user_name, main_mail, secondary_mail, phone_number)
VALUES ('Dupont', 'Dupond', 'super@mail.fr', 'super2@mail.fr', '06 45 72 45 98'),
('Martin', 'Matin', 'genial@mail.fr', 'genial2@mail.fr', '06 52 14 58 73'),
('Charvet', 'Lautre', 'mieux@tmail.fr', 'mieux2@tmail.fr', '07 49 82 16 35'),
('Leguez', 'Theo', 'bof@mesmails.fr', 'bof2@mesmails.fr', '+33 6 78 14 25 29'),
('Kia', 'Bi', 'special@mail.fr', 'special2@mail.fr', '07 65 31 38 95'),
('Ducaillou', 'Pierre', 'maildefou@xyz.fr', 'maildefou2@xyz.fr', '06 54 78 12 62'),
('Janine', 'Dave', 'janine@labri.fr', 'janine2@labri.fr', '06 87 12 45 95');
INSERT INTO administrator (id_administrator)
VALUES (7);
INSERT INTO project (project_name, logo, creation_date, project_status, id_administrator)
VALUES ('Eau du robinet', decode('013d7d16d7ad4fefb61bd95b765c8ceb', 'hex'), TO_DATE('01-OCT-2023', 'DD-MON-YYYY'),
'En cours', 7),
('Air oxygéné', decode('150647a0984e8f228cd14b54', 'hex'), TO_DATE('04-APR-2024', 'DD-MON-YYYY'), 'En cours', 7),
('Débat concours', decode('022024abd5486e245c145dda65116f', 'hex'), TO_DATE('22-NOV-2023', 'DD-MON-YYYY'),
'Suspendu', 7),
('HDeirbMI', decode('ab548d6c1d595a2975e6476f544d14c55a', 'hex'), TO_DATE('07-DEC-2024', 'DD-MON-YYYY'),
'Lancement', 7);
INSERT INTO entrepreneur (school, course, snee_status, id_entrepreneur, id_project_participation, id_project_proposed)
VALUES ('ENSEIRB-MATMECA', 'INFO', TRUE, 1, 4, 4),
('ENSC', 'Cognitique', TRUE, 2, 2, null),
('ENSEIRB-MATMECA', 'MATMECA', FALSE, 3, 3, 3),
('SupOptique', 'Classique', TRUE, 4, 1, 1),
('ENSEGID', 'Géoscience', FALSE, 5, 1, null),
('ENSMAC', 'Matériaux composites - Mécanique', FALSE, 6, 2, 2);
INSERT INTO section_cell (title, content_section_cell, modification_date, id_project)
VALUES ('Problème', 'les problèmes...', TO_TIMESTAMP('15-JAN-2025 09:30:20', 'DD-MON-YYYY, HH24:MI:SS'), 2),
('Segment de client', 'Le segment AB passant le client n°8 est de longueur 32mm.
Le segment BC a quant à lui un longueur de 28mm. Quelle la longueur du segment AC ?',
TO_TIMESTAMP('12-OCT-2022 17:47:38', 'DD-MON-YYYY, HH24:MI:SS'), 3),
('Proposition de valeur unique', '''Son prix est de 2594€'' ''Ah oui c''est unique en effet',
TO_TIMESTAMP('25-MAY-2024 11:12:04', 'DD-MON-YYYY, HH24:MI:SS'), 2),
('Solution', 'Un problème ? Une solution', TO_TIMESTAMP('08-FEB-2024 10:17:53', 'DD-MON-YYYY, HH24:MI:SS'), 1),
('Canaux', 'Ici nous avons la Seine, là-bas le Rhin, oh et plus loin le canal de Suez',
TO_TIMESTAMP('19-JUL-2023 19:22:45', 'DD-MON-YYYY, HH24:MI:SS'), 4),
('Sources de revenus', 'Y''en n''a pas on est pas payé. Enfin y''a du café quoi',
TO_TIMESTAMP('12-JAN-2025 11:40:26', 'DD-MON-YYYY, HH24:MI:SS'), 1),
('Structure des coûts', '''Ah oui là ça va faire au moins 1000€ par mois'', Eirbware',
TO_TIMESTAMP('06-FEB-2025 13:04:06', 'DD-MON-YYYY, HH24:MI:SS'), 3),
('Indicateurs clés', 'On apprend les clés comme des badges, ça se fait',
TO_TIMESTAMP('05-FEB-2025 12:42:38', 'DD-MON-YYYY, HH24:MI:SS'), 4),
('Avantages concurrentiel', 'On est meilleur', TO_TIMESTAMP('23-APR-2024 16:24:02', 'DD-MON-YYYY, HH24:MI:SS'),
2);
INSERT INTO appointment (appointment_date, appointment_time, appointment_duration, appointment_place,
appointment_subject)
VALUES (TO_DATE('24-DEC-2023', 'DD-MON-YYYY'), '00:00:00', '00:37:53', 'À la maison', 'Ouvrir les cadeaux'),
(TO_DATE('15-AUG-2024', 'DD-MON-YYYY'), '22:35:00', '00:12:36', 'Sur les quais ou dans un champ probablement',
'BOUM BOUM les feux d''artifices (on fête quoi déjà ?)'),
(TO_DATE('28-FEB-2023', 'DD-MON-YYYY'), '14:20:00', '00:20:00', 'Salle TD 15',
'Ah mince c''est pas une année bissextile !'),
(TO_DATE('23-JAN-2024', 'DD-MON-YYYY'), '12:56:27', '11:03:33', 'Là où le vent nous porte',
'Journée la plus importante de l''année'),
(TO_DATE('25-AUG-2025', 'DD-MON-YYYY'), '00:09:00', '01:00:00', 'Euh c''est par où l''amphi 56 ?',
'Rentrée scolaire (il fait trop froid c''est quoi ça on est en août)');
INSERT INTO report (report_content, id_appointment)
VALUES ('Ah oui ça c''est super, ah ouais j''aime bien, bien vu de penser à ça', 1),
('Bonne réunion', 3),
('Ouais, j''ai rien compris mais niquel on fait comme vous avez dit', 3),
('Non non ça va pas du tout ce que tu me proposes, faut tout refaire', 4),
('Réponse de la DSI : non', 2),
('Trop dommage qu''Apple ait sorti leur logiciel avant nous, on avait la même idée et tout on aurait tellement pu leur faire de la concurrence',
5);
INSERT INTO annotation (comment, id_administrator, id_section_cell)
VALUES ('faut changer ça hein', 7, 5),
('??? sérieusement, vous pensez que c''est une bonne idée ?', 7, 7),
('ok donc ça c''est votre business plan, bah glhf la team', 7, 2);

View File

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS administrateurs, projets, utilisateurs, entrepreneurs, sections, rendez_vous, comptes_rendus, concerner CASCADE;
DROP TABLE IF EXISTS administrator, project, user_inpulse, entrepreneur, section_cell, appointment, make_appointment, report, annotation, concern CASCADE;

View File

@ -6,6 +6,8 @@ import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MyinpulseApplicationTests {
@Test
void contextLoads() {}
@Test
void contextLoads() {
}
}

24
agenda/.gitignore vendored Normal file
View File

@ -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?

3
agenda/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
agenda/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
agenda/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mon Agenda Vue</title>
<link rel="icon" type="image/svg+xml" href="public/agenda-icon.svg" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1106
agenda/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
agenda/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "agenda",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.2.0"
}
}

View File

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect x="4" y="10" width="56" height="50" rx="4" ry="4" fill="#f2f2f2" stroke="#333" stroke-width="2"/>
<rect x="4" y="10" width="56" height="12" fill="#1976d2"/>
<circle cx="14" cy="16" r="2" fill="#fff"/>
<circle cx="50" cy="16" r="2" fill="#fff"/>
<!-- Grille du calendrier -->
<g fill="#fff" stroke="#ccc">
<rect x="10" y="26" width="8" height="8"/>
<rect x="20" y="26" width="8" height="8"/>
<rect x="30" y="26" width="8" height="8"/>
<rect x="40" y="26" width="8" height="8"/>
<rect x="50" y="26" width="8" height="8"/>
<rect x="10" y="36" width="8" height="8"/>
<rect x="20" y="36" width="8" height="8"/>
<rect x="30" y="36" width="8" height="8"/>
<rect x="40" y="36" width="8" height="8"/>
<rect x="50" y="36" width="8" height="8"/>
<rect x="10" y="46" width="8" height="8"/>
<rect x="20" y="46" width="8" height="8"/>
<rect x="30" y="46" width="8" height="8"/>
<rect x="40" y="46" width="8" height="8"/>
<rect x="50" y="46" width="8" height="8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
agenda/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

17
agenda/src/App.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<div id="app">
<h1>Mon agenda</h1>
<Agenda />
</div>
</template>
<script>
import Agenda from './components/Agenda.vue'
export default {
name: 'App',
components: {
Agenda
}
}
</script>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,586 @@
<template>
<div class="ade-agenda-container">
<!-- Import ICS -->
<div style="margin-bottom: 15px; text-align: center;">
<input type="file" @change="handleFileUpload" accept=".ics" />
</div>
<!-- Affiche un message tant que skipCells n'est pas prêt -->
<div v-if="!skipCellsReady">
Chargement de la grille...
</div>
<!-- Tableau ADE, rendu UNIQUEMENT si skipCells est prêt -->
<table
class="ade-table"
v-if="skipCellsReady"
>
<thead>
<!-- Ligne du haut : Semaine + date de début -->
<tr class="top-row">
<th :colspan="daysOfWeek.length + 1" class="week-title">
S{{ isoWeekNumber }} {{ formatDDMM(currentMonday) }}
</th>
</tr>
<!-- Ligne des jours -->
<tr class="days-row">
<th class="time-col-header">Horaire</th>
<th
v-for="(day, index) in daysOfWeek"
:key="index"
class="day-header"
>
<div class="day-label">
{{ weekdayLabel(day) }} {{ formatDateShort(day) }}
</div>
<div class="resource-name">
{{ resourceName }}
</div>
</th>
</tr>
</thead>
<tbody>
<!-- Pour chaque créneau horaire -->
<tr v-for="(slot, slotIndex) in timeSlots" :key="slotIndex">
<!-- Colonne Horaire -->
<td class="time-col">
{{ slot.start }} - {{ slot.end }}
</td>
<!-- 7 jours -->
<template v-for="(day, dayIndex) in daysOfWeek" :key="dayIndex">
<!--
On n'affiche un <td> que si la case n'est pas "skip"
(déjà couverte par un rowspan au-dessus).
-->
<td
v-if="!skipCells[dayIndex][slotIndex].skip"
:rowspan="skipCells[dayIndex][slotIndex].rowSpan"
class="agenda-cell"
:style="eventStyleForCell(dayIndex, slotIndex)"
>
<!-- Si des événements commencent EXACTEMENT ici -->
<div
v-for="(evt, evtIndex) in skipCells[dayIndex][slotIndex].events"
:key="evtIndex"
class="event-card"
>
<div class="event-title">{{ evt.title }}</div>
<div class="event-info">
{{ evt.start }} - {{ evt.end }}
</div>
</div>
</td>
</template>
</tr>
</tbody>
</table>
<!-- Barre d'onglets de navigation -->
<div class="week-tabs">
<button class="arrow-btn" @click="prevPage" :disabled="startIndex === 0">
</button>
<button
v-for="(week, idx) in displayedWeeks"
:key="idx"
class="week-tab-button"
@click="goToWeek(week.monday)"
>
S{{ week.isoWeek }} {{ formatDDMM(week.monday) }}
</button>
<button
class="arrow-btn"
@click="nextPage"
:disabled="startIndex + weeksToShow >= allWeeks.length"
>
</button>
</div>
</div>
</template>
<script>
/** Calcule la semaine ISO d'une date donnée. */
function getIsoWeekNumber(date) {
const tempDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = tempDate.getUTCDay() || 7;
tempDate.setUTCDate(tempDate.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(tempDate.getUTCFullYear(), 0, 1));
return Math.ceil(((tempDate - yearStart) / 86400000 + 1) / 7);
}
/** Retourne 'YYYY-MM-DD' */
function formatDate(dateObj) {
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, "0");
const dd = String(dateObj.getDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}
export default {
name: "AdeLikeAgenda",
data() {
const currentMonday = new Date(2025, 2, 31); // Par exemple
return {
currentMonday,
resourceName: "El Alaoui El Ismaili Omar",
// Créneaux de 30 min
timeSlots: [
{ start: "08:00", end: "08:30" },
{ start: "08:30", end: "09:00" },
{ start: "09:00", end: "09:30" },
{ start: "09:30", end: "10:00" },
{ start: "10:00", end: "10:30" },
{ start: "10:30", end: "11:00" },
{ start: "11:00", end: "11:30" },
{ start: "11:30", end: "12:00" },
{ start: "12:00", end: "12:30" },
{ start: "12:30", end: "13:00" },
{ start: "13:00", end: "13:30" },
{ start: "13:30", end: "14:00" },
{ start: "14:00", end: "14:30" },
{ start: "14:30", end: "15:00" },
{ start: "15:00", end: "15:30" },
{ start: "15:30", end: "16:00" },
{ start: "16:00", end: "16:30" },
{ start: "16:30", end: "17:00" },
{ start: "17:00", end: "17:30" },
{ start: "17:30", end: "18:00" },
{ start: "18:00", end: "18:30" },
{ start: "18:30", end: "19:00" },
{ start: "19:00", end: "19:30" },
{ start: "19:30", end: "19:55" },
],
events: [], // Contiendra la liste des événements importés
allWeeks: [], // Liste de semaines pour la barre d'onglets
weeksToShow: 7, // 7 semaines affichées dans la barre
startIndex: 0, // Index de la 1ère semaine dans la barre
/**
* Tableau 2D (dayIndex, slotIndex) pour savoir si on "saute" (skip) cette case,
* combien de rowSpan, et quels événements démarrent ici.
*/
skipCells: [],
};
},
computed: {
/** Les 7 jours de currentMonday */
daysOfWeek() {
const result = [];
for (let i = 0; i < 7; i++) {
const d = new Date(this.currentMonday);
d.setDate(d.getDate() + i);
result.push(d);
}
return result;
},
/** Numéro de semaine (ISO) */
isoWeekNumber() {
return getIsoWeekNumber(this.currentMonday);
},
/** Liste des semaines à afficher (avec pagination) */
displayedWeeks() {
return this.allWeeks.slice(this.startIndex, this.startIndex + this.weeksToShow);
},
/**
* Renvoie true si skipCells est "prêt" :
* - 7 jours
* - Au moins 1 timeSlot
* - skipCells a la même taille que daysOfWeek x timeSlots
*/
skipCellsReady() {
if (!this.skipCells || !this.skipCells.length) return false;
if (this.skipCells.length !== this.daysOfWeek.length) return false;
if (!this.skipCells[0].length) return false;
if (this.skipCells[0].length !== this.timeSlots.length) return false;
return true;
},
},
watch: {
// À chaque changement de semaine ou de liste d'événements,
// on recalcule la matrice skipCells
currentMonday() {
this.buildSkipCells();
},
events: {
handler() {
this.buildSkipCells();
},
deep: true,
},
},
methods: {
/** Initialise la liste de toutes les semaines */
initWeeks() {
const totalCount = 15; // ex: 15 semaines
const baseDate = new Date(this.currentMonday);
for (let i = 0; i < totalCount; i++) {
const temp = new Date(baseDate);
temp.setDate(baseDate.getDate() + i * 7);
this.allWeeks.push({
monday: temp,
isoWeek: getIsoWeekNumber(temp),
});
}
},
/** Construit skipCells = la matrice skip/rowSpan/events pour chaque jour/slot */
buildSkipCells() {
// Si daysOfWeek ou timeSlots sont vides, on annule
if (!this.daysOfWeek.length || !this.timeSlots.length) {
this.skipCells = [];
return;
}
// Réinitialise skipCells
this.skipCells = Array(this.daysOfWeek.length) // nb de jours
.fill(null)
.map(() =>
Array(this.timeSlots.length)
.fill(null)
.map(() => ({ skip: false, rowSpan: 1, events: [] }))
);
// Parcours de chaque jour
this.daysOfWeek.forEach((day, dayIndex) => {
const dayStr = formatDate(day);
// Parcours des créneaux
for (let slotIndex = 0; slotIndex < this.timeSlots.length; slotIndex++) {
const cell = this.skipCells[dayIndex][slotIndex];
if (cell.skip) continue; // déjà couvert par un rowspan
const slotStart = this.timeSlots[slotIndex].start;
const slotEnd = this.timeSlots[slotIndex].end;
// Cherche événements qui chevauchent ce créneau
const eventsInSlot = this.events.filter((evt) => {
const evtStart = evt.start;
const evtEnd = evt.end;
// Vérifie si l'événement commence ou chevauche ce créneau
return (
evt.day === dayStr &&
((evtStart >= slotStart && evtStart < slotEnd) || // Commence dans ce créneau
(evtEnd > slotStart && evtEnd <= slotEnd) || // Se termine dans ce créneau
(evtStart < slotStart && evtEnd > slotEnd)) // Couvre entièrement ce créneau
);
});
if (eventsInSlot.length > 0) {
// rowSpan = max de tous les événements qui chevauchent ce créneau
let maxSpan = 1;
eventsInSlot.forEach((evt) => {
const r = this.getEventRowSpan(evt, slotIndex);
if (r > maxSpan) maxSpan = r;
});
// Associe la liste des événements et le rowSpan
cell.events = eventsInSlot;
cell.rowSpan = maxSpan;
// "Skip" les créneaux suivants couverts par ce rowSpan
for (let i = 1; i < maxSpan; i++) {
if (slotIndex + i < this.timeSlots.length) {
this.skipCells[dayIndex][slotIndex + i].skip = true;
}
}
}
}
});
},
/** Style d'arrière-plan si un événement est présent. */
eventStyleForCell(dayIndex, slotIndex) {
const cellInfo = this.skipCells[dayIndex][slotIndex];
if (!cellInfo.events.length) {
// Pas d'événement => style neutre
return {};
}
// On peut par ex. prendre la couleur du 1er événement
const firstEvt = cellInfo.events[0];
return {
backgroundColor: firstEvt.color || "#cef",
};
},
/** Calcule le rowSpan d'un événement (nombre de créneaux couverts) */
getEventRowSpan(evt, slotIndex) {
const toMinutes = (time) => {
const [h, m] = time.split(":").map(Number);
return h * 60 + m;
};
const evtStart = toMinutes(evt.start);
const evtEnd = toMinutes(evt.end);
const slotStart = toMinutes(this.timeSlots[slotIndex].start);
const duration = evtEnd - Math.max(evtStart, slotStart); // Durée restante à partir de ce créneau
const slotDuration = 30; // 30 min par créneau
return Math.ceil(duration / slotDuration);
},
/** Formatage de dates */
formatDateShort(dateObj) {
const dd = String(dateObj.getDate()).padStart(2, "0");
const mm = String(dateObj.getMonth() + 1).padStart(2, "0");
const yyyy = dateObj.getFullYear();
return `${dd}/${mm}/${yyyy}`;
},
formatDDMM(dateObj) {
const dd = String(dateObj.getDate()).padStart(2, "0");
const mm = String(dateObj.getMonth() + 1).padStart(2, "0");
return `${dd}/${mm}`;
},
weekdayLabel(dateObj) {
const dayIndex = dateObj.getDay();
const labels = ["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"];
return labels[dayIndex];
},
/** Navigation dans la barre de semaines */
goToWeek(mondayDate) {
this.currentMonday = new Date(
mondayDate.getFullYear(),
mondayDate.getMonth(),
mondayDate.getDate()
);
},
prevPage() {
if (this.startIndex > 0) this.startIndex--;
},
nextPage() {
if (this.startIndex + this.weeksToShow < this.allWeeks.length) {
this.startIndex++;
}
},
/** Import de fichier ICS */
handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
this.parseICS(content);
};
reader.readAsText(file);
},
/** Parse ICS simple */
parseICS(text) {
const lines = text.split(/\r?\n/);
let currentEvent = null;
const events = [];
for (const line of lines) {
if (line === "BEGIN:VEVENT") {
currentEvent = {};
} else if (line === "END:VEVENT") {
if (currentEvent) events.push(currentEvent);
currentEvent = null;
} else if (currentEvent) {
if (line.startsWith("DTSTART")) {
currentEvent.start = this.parseICSTime(line);
} else if (line.startsWith("DTEND")) {
currentEvent.end = this.parseICSTime(line);
} else if (line.startsWith("SUMMARY")) {
currentEvent.title = line.split(":")[1] || "Sans titre";
}
}
}
// Transforme {start: {date, time}, end: {date, time}} en {day, start, end}
this.events = events.map((e) => ({
title: e.title,
day: e.start.date,
start: e.start.time,
end: e.end.time,
color: "#acf",
}));
},
/** Parse la ligne ICS pour extraire la date + heure (fuseau Europe/Paris) */
parseICSTime(line) {
const match = line.match(/:(\d{8})T(\d{6})Z?/);
if (!match) return { date: "", time: "" };
const dateStr = match[1];
const timeStr = match[2];
// Date en UTC
const utcDate = new Date(
Date.UTC(
parseInt(dateStr.slice(0, 4), 10),
parseInt(dateStr.slice(4, 6), 10) - 1,
parseInt(dateStr.slice(6, 8), 10),
parseInt(timeStr.slice(0, 2), 10),
parseInt(timeStr.slice(2, 4), 10),
parseInt(timeStr.slice(4, 6), 10)
)
);
// Convertit en local (Europe/Paris)
const localDate = new Date(
utcDate.toLocaleString("en-US", { timeZone: "Europe/Paris" })
);
return {
date: `${localDate.getFullYear()}-${String(localDate.getMonth() + 1).padStart(2, "0")}-${String(localDate.getDate()).padStart(2, "0")}`,
time: `${String(localDate.getHours()).padStart(2, "0")}:${String(localDate.getMinutes()).padStart(2, "0")}`,
};
},
},
/** Au montage, on initialise la liste des semaines et on construit skipCells */
mounted() {
this.initWeeks();
// Force un build initial
this.buildSkipCells();
// Optionnel: Debug en console
console.log("skipCells initial:", this.skipCells);
},
};
</script>
<style scoped>
.ade-agenda-container {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
}
/* Tableau principal */
.ade-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-bottom: 10px;
background-color: #fff;
border: 1px solid #ccc;
}
/* Titre de la semaine */
.top-row .week-title {
background-color: #cde;
font-size: 1.1rem;
padding: 8px;
text-align: center;
color: #333;
border-bottom: 2px solid #bbb;
}
/* En-têtes des jours */
.days-row th {
background-color: #eee;
color: #333;
border: 1px solid #ccc;
padding: 4px;
text-align: center;
font-weight: normal;
}
.time-col-header {
background-color: #ececec;
font-weight: bold;
width: 80px;
}
.day-header {
width: calc((100% - 80px) / 7);
}
.day-label {
font-weight: bold;
}
.resource-name {
font-size: 0.85rem;
color: #555;
margin-top: 2px;
}
/* Corps du tableau */
td {
border: 1px solid #ccc;
padding: 3px;
vertical-align: top;
}
.time-col {
background-color: #f8f8f8;
text-align: center;
font-size: 0.85rem;
font-weight: bold;
color: #333;
}
.agenda-cell {
min-height: 40px;
background-color: transparent;
border: 1px solid #ccc;
}
/* Cartes d'événement */
.event-card {
margin: 2px 0;
padding: 4px;
border: 1px solid #999;
border-radius: 4px;
font-size: 0.8rem;
background-color: #acf;
text-align: center;
vertical-align: top;
}
.event-title {
font-weight: bold;
margin-bottom: 2px;
}
.event-info {
font-size: 0.75rem;
color: #555;
}
/* Barre d'onglets en bas */
.week-tabs {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
margin-bottom: 20px;
}
.week-tab-button {
padding: 4px 8px;
border: 1px solid #666;
background-color: #eee;
cursor: pointer;
border-radius: 4px;
font-size: 0.8rem;
min-width: 60px;
}
.week-tab-button:hover {
background-color: #ccc;
}
.arrow-btn {
padding: 4px 8px;
border: 1px solid #666;
background-color: #ddd;
cursor: pointer;
border-radius: 4px;
font-size: 0.8rem;
}
.arrow-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

5
agenda/src/main.js Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

79
agenda/src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

7
agenda/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})

View File

@ -1,14 +1,15 @@
services:
postgres:
env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
image: postgres:latest
container_name: MyINPulse-DB
ports:
- 5433:5432
#ports:
# - 5432:5432
volumes:
- ./postgres/data:/var/lib/postgresql/data
- ./postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
keycloak:
container_name: MyINPulse-keycloak

View File

@ -1,22 +0,0 @@
POSTGRES_DB=postgres_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password
DATABASE_URL=localhost:5433
VITE_KEYCLOAK_URL=http://localhost:7080
VITE_KEYCLOAK_CLIENT_ID=myinpulse
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=http://localhost:8080
VITE_BACKEND_URL=http://localhost:8081/

5
config/backdev.front.env Normal file
View File

@ -0,0 +1,5 @@
VITE_KEYCLOAK_URL=http://localhost:7080
VITE_KEYCLOAK_CLIENT_ID=myinpulse
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=http://localhost:8080
VITE_BACKEND_URL=http://localhost:8081/

6
config/backdev.main.env Normal file
View File

@ -0,0 +1,6 @@
POSTGRES_DB=keycloak_db
POSTGRES_USER=keycloak_db_user
POSTGRES_PASSWORD=keycloak_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost

View File

@ -1,52 +0,0 @@
services:
postgres:
env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
container_name: MyINPulse-DB
ports:
- 5433:5432
volumes:
- ./postgres/data:/var/lib/postgresql/data
keycloak:
container_name: MyINPulse-keycloak
build:
context: ./keycloak
dockerfile: Dockerfile
args:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
environment:
KC_HOSTNAME_PORT: 7080
KC_HOSTNAME_STRICT_BACKCHANNEL: "true"
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_LOG_LEVEL: info
command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"]
ports:
- "7080:7080"
- "7443:7443"
depends_on:
- postgres
#front:
# build:
# context: ./front/
# dockerfile: Dockerfile
# container_name: MyINPulse-front
# ports:
# - "8080:80"
#back:
# build:
# context: ./MyINPulse-back/
# dockerfile: Dockerfile
# container_name: MyINPulse-back
# ports:
# - "8081:8080"

View File

@ -1,22 +0,0 @@
POSTGRES_DB=postgres_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password
DATABASE_URL=localhost:5433
VITE_KEYCLOAK_URL=http://localhost:7080
VITE_KEYCLOAK_CLIENT_ID=myinpulse-dev
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=http://localhost:5173
VITE_BACKEND_URL=http://localhost:8081/

View File

@ -1,15 +1,15 @@
services:
postgres:
env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
image: postgres:latest
container_name: MyINPulse-DB
#ports:
# - 5432:5432
volumes:
- ./postgres/data:/var/lib/postgresql/data
- ./postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
keycloak:
container_name: MyINPulse-keycloak

View File

@ -1,22 +0,0 @@
POSTGRES_DB=postgres_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password
DATABASE_URL=MyINPulse-DB
VITE_KEYCLOAK_URL=http://localhost:7080
VITE_KEYCLOAK_CLIENT_ID=myinpulse-dev
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=http://localhost:5173
VITE_BACKEND_URL=http://localhost:8081/

View File

@ -0,0 +1,5 @@
VITE_KEYCLOAK_URL=http://localhost:7080
VITE_KEYCLOAK_CLIENT_ID=myinpulse-dev
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=http://localhost:5173
VITE_BACKEND_URL=http://localhost:8081/

6
config/frontdev.main.env Normal file
View File

@ -0,0 +1,6 @@
POSTGRES_DB=keycloak_db
POSTGRES_USER=keycloak_db_user
POSTGRES_PASSWORD=keycloak_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=localhost

View File

@ -1,14 +1,11 @@
services:
postgres:
env_file: .env
build:
context: postgres/
dockerfile: Dockerfile
image: postgres:latest
container_name: MyINPulse-DB
#ports:
# - 5432:5432
volumes:
- ./postgres/data:/var/lib/postgresql/data
- ./postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
@ -30,10 +27,10 @@ services:
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_LOG_LEVEL: info
command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"] # TODO: remove start-dev
#ports:
# - "7080:7080"
# - "7443:7443"
command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"]
ports:
- "7080:7080"
- "7443:7443"
depends_on:
- postgres
@ -50,6 +47,6 @@ services:
context: ./MyINPulse-back/
dockerfile: Dockerfile
container_name: MyINPulse-back
#ports:
# - "8081:8080"
ports:
- "8081:8080"

View File

@ -1,22 +0,0 @@
POSTGRES_DB=postgres_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr
KEYCLOAK_DB=keycloak_db
KEYCLOAK_USER=keycloak_db_user
KEYCLOAK_PASSWORD=keycloak_db_user_password
BACKEND_DB=backend_db
BACKEND_USER=backend_db_user
BACKEND_PASSWORD=backend_db_user_password
DATABASE_URL=MyINPulse-DB
VITE_KEYCLOAK_URL=https://0549cd63f912d5dc9b31278d6f.eirb.fr
VITE_KEYCLOAK_CLIENT_ID=myinpulse-eirb
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=https://0549cd63f912d5dc9b31278d6f.piair.dev
VITE_BACKEND_URL=http://TODO/

5
config/prod.front.env Normal file
View File

@ -0,0 +1,5 @@
VITE_KEYCLOAK_URL=https://0549cd63f912d5dc9b31278d6f.eirb.fr
VITE_KEYCLOAK_CLIENT_ID=myinpulse-eirb
VITE_KEYCLOAK_REALM=test
VITE_APP_URL=https://0549cd63f912d5dc9b31278d6f.piair.dev
VITE_BACKEND_URL=http://TODO/

6
config/prod.main.env Normal file
View File

@ -0,0 +1,6 @@
POSTGRES_DB=keycloak_db
POSTGRES_USER=keycloak_db_user
POSTGRES_PASSWORD=keycloak_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr

View File

@ -36,4 +36,4 @@ playwright-report/
# Custom
.installed
./package-lock.json
package-lock.json

View File

@ -1,7 +0,0 @@
{
"useTabs":false,
"semi":true,
"trailingComma":"es5",
"arrowParens":"always",
"tabWidth":4
}

View File

@ -1,29 +0,0 @@
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginVue from "eslint-plugin-vue";
import globals from "globals";
import typescriptEslint from "typescript-eslint";
export default typescriptEslint.config(
{ ignores: ["*.d.ts", "**/coverage", "**/dist"] },
{
extends: [
eslint.configs.recommended,
...typescriptEslint.configs.recommended,
...eslintPluginVue.configs["flat/recommended"],
],
files: ["**/*.{ts,vue}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parserOptions: {
parser: typescriptEslint.parser,
},
},
rules: {
// your rules
},
},
eslintConfigPrettier
);

View File

@ -0,0 +1,10 @@
{
"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": [
{ "canva_data": "this is a fake data to test api" }
]
}

View File

@ -0,0 +1,2 @@
#!/usr/bin/bash
json-server --watch db.json --port 5000

File diff suppressed because it is too large Load Diff

View File

@ -26,15 +26,8 @@
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.0",
"typescript": "~5.7.3",
"typescript-eslint": "^8.23.0",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"

View File

@ -1,47 +1,15 @@
<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 />
<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>

View File

@ -0,0 +1,75 @@
<template>
<div id="agenda">
<h3>Rendez-vous</h3>
<table>
<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>
#agenda {
padding: 20px;
}
/* Table Styling */
table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
text-align: left;
margin-top: 20px;
border: 1px solid #ccc;
}
/* Header Row (if exists) */
th {
background-color: #f4f4f4;
padding: 12px;
font-weight: bold;
border: 1px solid #ccc;
}
/* 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>

View File

@ -1,26 +1,26 @@
<template>
<header>
<img src="./icons/logo inpulse.png" alt="INPulse" />
<img src="./icons/logo inpulse.png" alt="INPulse" />
</header>
</template>
<script lang="ts">
export default {
name: "HeaderComponent",
};
</script>
<style scoped>
header img {
</template>
<script lang="ts">
export default {
name: 'Header',
};
</script>
<style scoped>
header img {
width: 100px;
}
}
header {
header{
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-bottom: 2px solid #ddd;
}
</style>
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div @click="goToLink" class="project">
<div class="project-header">
<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 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, #f4f4f4, #ffffff);
border: 1px solid #ddd;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
}
/* Header Styling */
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.project-header h2 {
font-size: 20px;
color: #333;
margin: 0;
}
/* Button Container */
.project-buttons {
display: flex;
gap: 10px;
}
.info-btn {
background-color: #4CAF50;
color: #fff;
}
.info-btn:hover {
background-color: #45a049;
transform: scale(1.05);
}
.contact-btn {
background-color: #007BFF;
color: #fff;
}
.contact-btn:hover {
background-color: #0056b3;
transform: scale(1.05);
}
.project-body {
margin-top: 15px;
}
.project-body p {
font-size: 16px;
color: #555;
margin-bottom: 10px;
}
.project-body ul {
list-style-type: disc;
margin: 0;
padding-left: 20px;
}
.project-body ul li {
font-size: 14px;
color: #666;
line-height: 1.6;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background-color: #0056b3;
}
</style>

View File

@ -1,21 +0,0 @@
<template>
<div class="project">
<div class="project-header">
<h2>{{ projectName }}</h2>
</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>

View File

@ -0,0 +1,88 @@
<template>
<div :class="['cell', { expanded }]"
@click="toggleExpand"
:style="{ justifyContent: expanded ? 'flex-start' : 'center' }"> <!-- Looking for finding a way
to make this style in the toggleExpand event -->
<h3>{{ title }}</h3>
<p>{{ currentDescription }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, onMounted } from "vue";
import axios from "axios";
const props = defineProps<{
title: string;
description: string;
}>();
const expanded = ref(false);
const currentDescription = ref(props.description);
const fetchData = async () => {
try {
const response = await axios.get("http://localhost:5000/data"); // Update the URL if needed
currentDescription.value = response.data[0].canva_data;
} catch (error) {
console.error("Erreur lors de la récupération des données :", error);
}
};
const toggleExpand = async () => {
if (!expanded.value) {
await fetchData();
} else {
currentDescription.value = props.description;
}
expanded.value = !expanded.value;
};
</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);
}
.cell:not(.expanded):hover {
transform: scale(1.05);
box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2);
}
.cell h3 {
font-size: 20px;
font-weight: 500;
/*margin-bottom: 10px;*/
}
.cell p {
font-size: 14px;
color: #666;
}
.expanded {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<header>
<img src="../icons/logo inpulse.png" alt="INPulse Logo">
<div class="header-buttons">
<div class="menu">
<button class="contact-button" @click="toggleDropdown">Contact</button>
<div class="contact-dropdown" v-bind:class="{ 'dropdown-visible': isDropdownOpen }">
<button @click="contactAll">Contact All</button>
<button v-for="(email, index) in entrepreneurEmails" :key="index">
{{ email }}
</button>
</div>
<button class="return-button">
<RouterLink to="/">Return to list project</RouterLink>
</button>
</div>
</div>
</header>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
const isDropdownOpen = ref(false);
const entrepreneurEmails = ref([]);
const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value;
console.log("Dropdown toggled:", isDropdownOpen.value); // for debug purposes
};
const fetchEntrepreneurs = async () => {
try {
const response = await axios.get("http://localhost:5000/entrepreneurs");
entrepreneurEmails.value = response.data.map(e => e.email);
} catch (error) {
console.error("Erreur lors de la récupération des entrepreneurs:", error);
}
};
const contactAll = () => {
alert("Contacter tous les entrepreneurs : " + entrepreneurEmails.value.join(", "));
};
onMounted(fetchEntrepreneurs);
</script>
<style scoped>
@import "@/components/canvas/style-project.css";
header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10px;
}
.header-buttons {
display: flex;
align-items: flex-start;
}
.menu {
display: flex;
flex-direction: column;
gap: 10px;
}
.contact-button, .return-button {
background-color: #000;
color: white;
border: none;
padding: 10px;
cursor: pointer;
font-size: 14px;
text-align: center;
}
.return-button a {
color: white;
text-decoration: none;
}
.contact-dropdown {
display: none;
position: absolute;
background-color: white;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 10px;
margin-top: 5px;
z-index: 1000;
}
.contact-dropdown button {
display: block;
width: 100%;
padding: 5px;
text-align: left;
border: none;
background: none;
cursor: pointer;
}
.contact-dropdown button:hover {
background-color: #f0f0f0;
}
.dropdown-visible {
display: block;
}
header img {
width: 100px;
height: auto;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="canvas">
<CanvasItem
v-for="(item, index) in items"
:key="index"
:title="item.title"
:description="item.description"
:class="item.class"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import CanvasItem from "@/components/canvas/CanvasItem.vue";
const items = ref([
{ title: "1. Problème", description: "3 problèmes essentiels à résoudre pour le client", class: "Probleme" },
{ title: "2. Segments", description: "Les segments de clientèle visés", class: "Segments" },
{ title: "3. Valeur", description: "La proposition de valeur", class: "Valeur" },
{ title: "4. Solution", description: "Les solutions proposées", class: "Solution" },
{ title: "5. Avantage", description: "Les avantages concurrentiels", class: "Avantage" },
{ title: "6. Canaux", description: "Les canaux de distribution", class: "Canaux" },
{ title: "7. Indicateurs", description: "Les indicateurs clés de performance", class: "Indicateurs" },
{ title: "8. Coûts", description: "Les coûts associés", class: "Couts" },
{ title: "9. Revenus", description: "Les sources de revenus", class: "Revenus" }
]);
</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: 10px;
padding: 10px;
max-width: 1200px;
margin: 20px auto;
background-color: #fff;
position: relative;
height: 80vh;
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; }
</style>

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More