diff --git a/.gitignore b/.gitignore
index a7e4258..b09a99b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,45 +1,41 @@
-# ---> Java
-# Compiled class file
+# Java
*.class
-
-# Log file
*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
*.jar
*.war
*.nar
*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
-# ---> Maven
+# Maven
+backend/target/
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
-# Eclipse m2e generated files
-# Eclipse Core
+# IDE
+.idea/
+*.iml
+.vscode/
.project
-# JDT-specific (Eclipse Java Development Tools)
.classpath
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+*.env.local
+
+# Flutter
+frontend/.dart_tool/
+frontend/.flutter-plugins
+frontend/.flutter-plugins-dependencies
+frontend/build/
+
+# Docs sensibles
+memoire de fin d'etude.docx
diff --git a/Livrables/SUIVI-PROJET.md b/Livrables/SUIVI-PROJET.md
new file mode 100644
index 0000000..b5b780a
--- /dev/null
+++ b/Livrables/SUIVI-PROJET.md
@@ -0,0 +1,104 @@
+# Suivi du Projet ERP SUARL Rayhan
+**PFE — Ali Guennari**
+**Coach & Développeur : Claude (Nabil Derouiche)**
+**Dernière mise à jour : 19 Avril 2026**
+
+---
+
+## État Global du Projet
+
+| Phase | Description | Statut |
+|-------|-------------|--------|
+| 1 | Analyse & Stratégie | ✅ Terminé |
+| 2 | Modélisation UML | ✅ Terminé (par Ali) |
+| 3 | Backend Spring Boot API | 🔄 En cours |
+| 4 | Frontend Flutter | ⏳ À faire |
+| 5 | Tests & Validation | ⏳ À faire |
+| 6 | Déploiement Production | ⏳ À faire |
+| 7 | Rapport de PFE | 🔄 En cours |
+
+---
+
+## Dépôt Git
+
+- **URL** : https://gitea.bolbol.tn/bolbol/rayhan-erp
+- **Branche principale** : `main`
+- **Organisation** : Spring Boot (backend/) + Flutter (frontend/)
+
+---
+
+## Phase 3 — Backend Spring Boot API
+
+### Architecture Technique
+- **Framework** : Spring Boot 3.x (Java 17)
+- **Sécurité** : Spring Security + JWT (JJWT 0.12.x)
+- **Base de données** : MySQL 8 (JPA/Hibernate)
+- **Build** : Maven
+
+### Modules API — Détail
+
+| Module | Endpoints | Statut |
+|--------|-----------|--------|
+| Authentification & Sécurité | `POST /api/auth/signin`, `POST /api/auth/signup` | ✅ Codé |
+| Gestion des Utilisateurs | `GET/PUT /api/users` | ✅ Codé |
+| Articles (Référentiel) | `CRUD /api/articles` | ✅ Codé |
+| Clients & Fournisseurs | `CRUD /api/clients`, `/api/fournisseurs` | ✅ Codé |
+| Gestion des Stocks | `GET /api/stock`, `POST /api/inventory/adjust` | ✅ Codé |
+| Cycle d'Achat | `POST /api/purchase-orders`, `/api/goods-receipts` | ✅ Codé |
+| Cycle de Vente | `POST /api/sales-orders`, `/api/delivery-notes` | ✅ Codé |
+| Cycle de Production (BOM + OF) | `GET/POST /api/bom`, `/api/production-orders` | ✅ Codé |
+| Tableau de Bord (KPIs) | `GET /api/dashboard` | ✅ Codé |
+| Facturation | `POST /api/invoices` | ⏳ À faire |
+| Paie & RH | `POST /api/payroll` | ⏳ À faire |
+
+### Rôles Utilisateurs
+
+| Rôle | Accès |
+|------|-------|
+| `ROLE_PDG` | Accès complet + tableau de bord |
+| `ROLE_RESPONSABLE_VENTE` | Ventes, clients, facturation |
+| `ROLE_RESPONSABLE_ACHAT` | Achats, fournisseurs |
+| `ROLE_RESPONSABLE_PRODUCTION` | Production, BOM, ordres de fabrication |
+| `ROLE_MAGASINIER` | Stock, mouvements |
+| `ROLE_RH` | Paie, congés, employés |
+
+---
+
+## Phase 4 — Frontend Flutter (À venir)
+
+- Architecture : Provider / BLoC
+- Écrans prioritaires : Login → Dashboard → Articles → Ventes → Achats → Production
+
+---
+
+## Livrables Produits
+
+| Fichier | Description | Statut |
+|---------|-------------|--------|
+| `SUIVI-PROJET.md` (ce fichier) | Suivi global du projet | ✅ |
+| `backend/` | Code source API Spring Boot | 🔄 |
+| `docs/UML-DiagrammeClasses.md` | Explication du diagramme de classes | ⏳ |
+| `docs/UML-CasUtilisation.md` | Explication des cas d'utilisation | ⏳ |
+| `docs/Architecture-API.md` | Documentation complète des endpoints | ⏳ |
+| `docs/Guide-Deploiement.md` | Guide Docker + déploiement local | ⏳ |
+| `docs/Guide-Tests-Postman.md` | Collection Postman + scénarios de test | ⏳ |
+
+---
+
+## Infrastructure Serveur
+
+- **Serveur local** : 192.168.100.33
+- **SSH** : port 22222, user Best0f
+- **Portainer** : http://192.168.100.33:9000/
+- **Gitea** : https://gitea.bolbol.tn
+
+---
+
+## Notes Importantes
+
+- LDPE (pas BDPE) = Polyéthylène Basse Densité
+- TVA Tunisie : 19% standard (vérifier avec Rayhan)
+- CNSS Tunisie : patronal ~16.57%, salarial ~9.18%
+- Timbre fiscal sur factures : 0.600 DT (à confirmer)
+- 4 produits finis : Sac Bertel, Sac Poubelle, Sac Alimentaire, Film Rétractable
+- 3 machines : Extrudeuse, Découpe/Soudure, Densificateur (recyclage chutes)
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..7e22375
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,21 @@
+# ==========================================
+# Dockerfile — Rayhan ERP Backend
+# Spring Boot 3 / Java 17
+# ==========================================
+
+# Étape 1 : Build avec Maven
+FROM maven:3.9.6-eclipse-temurin-17 AS build
+WORKDIR /app
+COPY pom.xml .
+RUN mvn dependency:go-offline -B
+COPY src ./src
+RUN mvn clean package -DskipTests -B
+
+# Étape 2 : Image finale légère
+FROM eclipse-temurin:17-jre-alpine
+WORKDIR /app
+COPY --from=build /app/target/erp-1.0.0-SNAPSHOT.jar app.jar
+
+EXPOSE 8080
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 0000000..c1e4859
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,114 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+
+ com.rayhan
+ erp
+ 1.0.0-SNAPSHOT
+ Rayhan ERP
+ ERP sur mesure pour SUARL Rayhan — PFE Ali Guennari
+
+
+ 17
+ 0.12.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java b/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java
new file mode 100644
index 0000000..e08bc26
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/RayhanErpApplication.java
@@ -0,0 +1,11 @@
+package com.rayhan.erp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class RayhanErpApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(RayhanErpApplication.class, args);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java b/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java
new file mode 100644
index 0000000..b21e533
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/config/DataInitializer.java
@@ -0,0 +1,55 @@
+package com.rayhan.erp.config;
+
+import com.rayhan.erp.model.ERole;
+import com.rayhan.erp.model.Role;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.RoleRepository;
+import com.rayhan.erp.repository.UserRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * Initialise la base de données avec les rôles et un utilisateur PDG par défaut.
+ * S'exécute au démarrage de l'application si la base est vide.
+ */
+@Component
+public class DataInitializer implements CommandLineRunner {
+
+ @Autowired private RoleRepository roleRepository;
+ @Autowired private UserRepository userRepository;
+ @Autowired private PasswordEncoder passwordEncoder;
+
+ @Override
+ public void run(String... args) {
+ initRoles();
+ initDefaultAdmin();
+ }
+
+ private void initRoles() {
+ for (ERole eRole : ERole.values()) {
+ if (roleRepository.findByName(eRole).isEmpty()) {
+ roleRepository.save(new Role(eRole));
+ }
+ }
+ }
+
+ private void initDefaultAdmin() {
+ if (!userRepository.existsByUsername("admin")) {
+ User admin = new User(
+ "admin",
+ "admin@rayhan.tn",
+ passwordEncoder.encode("Rayhan2024!"),
+ "Ahmed",
+ "Fekih");
+ Role pdgRole = roleRepository.findByName(ERole.ROLE_PDG)
+ .orElseThrow(() -> new RuntimeException("Rôle PDG non trouvé"));
+ admin.setRoles(Set.of(pdgRole));
+ userRepository.save(admin);
+ System.out.println("✅ Utilisateur admin créé (username: admin, password: Rayhan2024!)");
+ }
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java b/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java
new file mode 100644
index 0000000..729b124
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/config/WebSecurityConfig.java
@@ -0,0 +1,71 @@
+package com.rayhan.erp.config;
+
+import com.rayhan.erp.security.jwt.AuthEntryPointJwt;
+import com.rayhan.erp.security.jwt.AuthTokenFilter;
+import com.rayhan.erp.security.services.UserDetailsServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableMethodSecurity
+public class WebSecurityConfig {
+
+ @Autowired
+ UserDetailsServiceImpl userDetailsService;
+
+ @Autowired
+ private AuthEntryPointJwt unauthorizedHandler;
+
+ @Bean
+ public AuthTokenFilter authenticationJwtTokenFilter() {
+ return new AuthTokenFilter();
+ }
+
+ @Bean
+ public DaoAuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService);
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
+ return authConfig.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/auth/**").permitAll()
+ .requestMatchers("/api/test/**").permitAll()
+ .anyRequest().authenticated()
+ );
+
+ http.authenticationProvider(authenticationProvider());
+ http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java b/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java
new file mode 100644
index 0000000..2e67d75
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/ArticleController.java
@@ -0,0 +1,81 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.Article;
+import com.rayhan.erp.repository.ArticleRepository;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/articles")
+public class ArticleController {
+
+ @Autowired
+ ArticleRepository articleRepository;
+
+ @GetMapping
+ @PreAuthorize("isAuthenticated()")
+ public List getAllArticles() {
+ return articleRepository.findByActifTrue();
+ }
+
+ @GetMapping("/{id}")
+ @PreAuthorize("isAuthenticated()")
+ public ResponseEntity getArticleById(@PathVariable Long id) {
+ return articleRepository.findById(id)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @GetMapping("/type/{type}")
+ @PreAuthorize("isAuthenticated()")
+ public List getArticlesByType(@PathVariable Article.TypeArticle type) {
+ return articleRepository.findByType(type);
+ }
+
+ @GetMapping("/alertes-stock")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public List getArticlesEnAlerte() {
+ return articleRepository.findAll().stream()
+ .filter(a -> a.isActif() && a.getStockActuel().compareTo(a.getStockMinimum()) <= 0)
+ .toList();
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION', 'ROLE_MAGASINIER')")
+ public Article createArticle(@Valid @RequestBody Article article) {
+ return articleRepository.save(article);
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public ResponseEntity updateArticle(@PathVariable Long id, @Valid @RequestBody Article details) {
+ return articleRepository.findById(id)
+ .map(article -> {
+ article.setDesignation(details.getDesignation());
+ article.setType(details.getType());
+ article.setUniteMesure(details.getUniteMesure());
+ article.setPrixUnitaire(details.getPrixUnitaire());
+ article.setStockMinimum(details.getStockMinimum());
+ return ResponseEntity.ok(articleRepository.save(article));
+ })
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ROLE_PDG')")
+ public ResponseEntity> deleteArticle(@PathVariable Long id) {
+ return articleRepository.findById(id)
+ .map(article -> {
+ article.setActif(false);
+ articleRepository.save(article);
+ return ResponseEntity.ok().build();
+ })
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/AuthController.java b/backend/src/main/java/com/rayhan/erp/controller/AuthController.java
new file mode 100644
index 0000000..1737949
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/AuthController.java
@@ -0,0 +1,115 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.dto.request.LoginRequest;
+import com.rayhan.erp.dto.request.SignupRequest;
+import com.rayhan.erp.dto.response.JwtResponse;
+import com.rayhan.erp.dto.response.MessageResponse;
+import com.rayhan.erp.model.ERole;
+import com.rayhan.erp.model.Role;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.RoleRepository;
+import com.rayhan.erp.repository.UserRepository;
+import com.rayhan.erp.security.jwt.JwtUtils;
+import com.rayhan.erp.security.services.UserDetailsImpl;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ @Autowired AuthenticationManager authenticationManager;
+ @Autowired UserRepository userRepository;
+ @Autowired RoleRepository roleRepository;
+ @Autowired PasswordEncoder encoder;
+ @Autowired JwtUtils jwtUtils;
+
+ /**
+ * POST /api/auth/signin
+ * Connexion d'un utilisateur — retourne un token JWT
+ */
+ @PostMapping("/signin")
+ public ResponseEntity> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ String jwt = jwtUtils.generateJwtToken(authentication);
+
+ UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
+ List roles = userDetails.getAuthorities().stream()
+ .map(item -> item.getAuthority())
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(new JwtResponse(jwt,
+ userDetails.getId(),
+ userDetails.getUsername(),
+ userDetails.getEmail(),
+ userDetails.getFirstName(),
+ userDetails.getLastName(),
+ roles));
+ }
+
+ /**
+ * POST /api/auth/signup
+ * Inscription d'un nouvel utilisateur (réservé au PDG en production)
+ */
+ @PostMapping("/signup")
+ public ResponseEntity> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
+ if (userRepository.existsByUsername(signUpRequest.getUsername())) {
+ return ResponseEntity.badRequest()
+ .body(new MessageResponse("Erreur : Ce nom d'utilisateur est déjà pris."));
+ }
+ if (userRepository.existsByEmail(signUpRequest.getEmail())) {
+ return ResponseEntity.badRequest()
+ .body(new MessageResponse("Erreur : Cet email est déjà utilisé."));
+ }
+
+ User user = new User(
+ signUpRequest.getUsername(),
+ signUpRequest.getEmail(),
+ encoder.encode(signUpRequest.getPassword()),
+ signUpRequest.getFirstName(),
+ signUpRequest.getLastName());
+
+ Set strRoles = signUpRequest.getRoles();
+ Set roles = new HashSet<>();
+
+ if (strRoles == null || strRoles.isEmpty()) {
+ Role magasinierRole = roleRepository.findByName(ERole.ROLE_MAGASINIER)
+ .orElseThrow(() -> new RuntimeException("Rôle introuvable en base."));
+ roles.add(magasinierRole);
+ } else {
+ strRoles.forEach(role -> {
+ ERole eRole = switch (role.toLowerCase()) {
+ case "pdg" -> ERole.ROLE_PDG;
+ case "vente" -> ERole.ROLE_RESPONSABLE_VENTE;
+ case "achat" -> ERole.ROLE_RESPONSABLE_ACHAT;
+ case "production" -> ERole.ROLE_RESPONSABLE_PRODUCTION;
+ case "rh" -> ERole.ROLE_RH;
+ default -> ERole.ROLE_MAGASINIER;
+ };
+ Role foundRole = roleRepository.findByName(eRole)
+ .orElseThrow(() -> new RuntimeException("Rôle introuvable : " + role));
+ roles.add(foundRole);
+ });
+ }
+
+ user.setRoles(roles);
+ userRepository.save(user);
+ return ResponseEntity.ok(new MessageResponse("Utilisateur créé avec succès !"));
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/ClientController.java b/backend/src/main/java/com/rayhan/erp/controller/ClientController.java
new file mode 100644
index 0000000..2c1b538
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/ClientController.java
@@ -0,0 +1,65 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.Client;
+import com.rayhan.erp.repository.ClientRepository;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/clients")
+public class ClientController {
+
+ @Autowired
+ ClientRepository clientRepository;
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public List getAllClients() {
+ return clientRepository.findByActifTrue();
+ }
+
+ @GetMapping("/search")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public List searchClients(@RequestParam String q) {
+ return clientRepository.findByRaisonSocialeContainingIgnoreCase(q);
+ }
+
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public ResponseEntity getClientById(@PathVariable Long id) {
+ return clientRepository.findById(id)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public Client createClient(@Valid @RequestBody Client client) {
+ return clientRepository.save(client);
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public ResponseEntity updateClient(@PathVariable Long id, @Valid @RequestBody Client details) {
+ return clientRepository.findById(id)
+ .map(client -> {
+ client.setRaisonSociale(details.getRaisonSociale());
+ client.setMatriculeFiscal(details.getMatriculeFiscal());
+ client.setAdresse(details.getAdresse());
+ client.setTelephone(details.getTelephone());
+ client.setEmail(details.getEmail());
+ client.setVille(details.getVille());
+ client.setTypeClient(details.getTypeClient());
+ client.setPlafondCredit(details.getPlafondCredit());
+ client.setDelaiPaiement(details.getDelaiPaiement());
+ return ResponseEntity.ok(clientRepository.save(client));
+ })
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java b/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java
new file mode 100644
index 0000000..0687c12
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/DashboardController.java
@@ -0,0 +1,84 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.Article;
+import com.rayhan.erp.model.ProductionOrder;
+import com.rayhan.erp.model.SalesOrder;
+import com.rayhan.erp.repository.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/dashboard")
+public class DashboardController {
+
+ @Autowired private SalesOrderRepository salesOrderRepository;
+ @Autowired private PurchaseOrderRepository purchaseOrderRepository;
+ @Autowired private ProductionOrderRepository productionOrderRepository;
+ @Autowired private ArticleRepository articleRepository;
+
+ /**
+ * GET /api/dashboard
+ * KPIs principaux pour le tableau de bord du PDG
+ */
+ @GetMapping
+ @PreAuthorize("hasRole('ROLE_PDG')")
+ public Map getDashboard() {
+ Map dashboard = new HashMap<>();
+
+ LocalDate debutMois = LocalDate.now().withDayOfMonth(1);
+ LocalDate finMois = LocalDate.now();
+
+ // KPIs Ventes
+ BigDecimal caMois = salesOrderRepository
+ .sumTotalTTCByDateCommandeBetween(debutMois, finMois);
+ long nbCommandesMois = salesOrderRepository
+ .countByDateCommandeBetween(debutMois, finMois);
+
+ Map ventes = new HashMap<>();
+ ventes.put("chiffreAffairesMois", caMois != null ? caMois : BigDecimal.ZERO);
+ ventes.put("nbCommandesMois", nbCommandesMois);
+ ventes.put("commandesEnCours",
+ salesOrderRepository.findByStatutOrderByDateCommandeDesc(SalesOrder.StatutCommande.CONFIRMEE).size());
+ dashboard.put("ventes", ventes);
+
+ // KPIs Achats
+ Map achats = new HashMap<>();
+ achats.put("commandesEnAttente",
+ purchaseOrderRepository.countByStatutIn(
+ List.of(PurchaseOrder.StatutCommande.CONFIRMEE,
+ PurchaseOrder.StatutCommande.PARTIELLEMENT_RECUE)));
+ dashboard.put("achats", achats);
+
+ // KPIs Production
+ Map production = new HashMap<>();
+ production.put("ofPlanifies",
+ productionOrderRepository.countByStatut(ProductionOrder.StatutOF.PLANIFIE));
+ production.put("ofEnCours",
+ productionOrderRepository.countByStatut(ProductionOrder.StatutOF.LANCE));
+ dashboard.put("production", production);
+
+ // KPIs Stock
+ List alertesStock = articleRepository.findAll().stream()
+ .filter(a -> a.isActif() && a.getStockActuel().compareTo(a.getStockMinimum()) <= 0)
+ .toList();
+ Map stock = new HashMap<>();
+ stock.put("articlesEnAlerte", alertesStock.size());
+ stock.put("articlesEnAlerteDetails", alertesStock.stream()
+ .map(a -> Map.of("reference", a.getReference(),
+ "designation", a.getDesignation(),
+ "stockActuel", a.getStockActuel(),
+ "stockMinimum", a.getStockMinimum()))
+ .toList());
+ dashboard.put("stock", stock);
+
+ return dashboard;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java b/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java
new file mode 100644
index 0000000..296e8b7
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/FournisseurController.java
@@ -0,0 +1,67 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.Fournisseur;
+import com.rayhan.erp.repository.FournisseurRepository;
+import jakarta.validation.Valid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/fournisseurs")
+public class FournisseurController {
+
+ @Autowired
+ FournisseurRepository fournisseurRepository;
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public List getAllFournisseurs() {
+ return fournisseurRepository.findByActifTrue();
+ }
+
+ @GetMapping("/search")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public List searchFournisseurs(@RequestParam String q) {
+ return fournisseurRepository.findByRaisonSocialeContainingIgnoreCase(q);
+ }
+
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public ResponseEntity getFournisseurById(@PathVariable Long id) {
+ return fournisseurRepository.findById(id)
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public Fournisseur createFournisseur(@Valid @RequestBody Fournisseur fournisseur) {
+ return fournisseurRepository.save(fournisseur);
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public ResponseEntity updateFournisseur(@PathVariable Long id,
+ @Valid @RequestBody Fournisseur details) {
+ return fournisseurRepository.findById(id)
+ .map(f -> {
+ f.setRaisonSociale(details.getRaisonSociale());
+ f.setMatriculeFiscal(details.getMatriculeFiscal());
+ f.setAdresse(details.getAdresse());
+ f.setTelephone(details.getTelephone());
+ f.setEmail(details.getEmail());
+ f.setVille(details.getVille());
+ f.setPays(details.getPays());
+ f.setCategorieProduit(details.getCategorieProduit());
+ f.setDelaiLivraison(details.getDelaiLivraison());
+ f.setModePaiement(details.getModePaiement());
+ return ResponseEntity.ok(fournisseurRepository.save(f));
+ })
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java
new file mode 100644
index 0000000..e48d9c9
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/ProductionOrderController.java
@@ -0,0 +1,87 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.BomLine;
+import com.rayhan.erp.model.ProductionOrder;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.BomLineRepository;
+import com.rayhan.erp.repository.UserRepository;
+import com.rayhan.erp.security.services.UserDetailsImpl;
+import com.rayhan.erp.service.ProductionOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/production")
+public class ProductionOrderController {
+
+ @Autowired private ProductionOrderService productionOrderService;
+ @Autowired private BomLineRepository bomLineRepository;
+ @Autowired private UserRepository userRepository;
+
+ // --- BOM (Nomenclatures) ---
+
+ @GetMapping("/bom/{produitFiniId}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public List getBom(@PathVariable Long produitFiniId) {
+ return bomLineRepository.findByProduitFiniId(produitFiniId);
+ }
+
+ @PostMapping("/bom")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public BomLine addBomLine(@RequestBody BomLine bomLine) {
+ return bomLineRepository.save(bomLine);
+ }
+
+ @DeleteMapping("/bom/{id}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public ResponseEntity> deleteBomLine(@PathVariable Long id) {
+ bomLineRepository.deleteById(id);
+ return ResponseEntity.ok().build();
+ }
+
+ // --- Ordres de Fabrication ---
+
+ @GetMapping("/orders")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public List getAllOFs() {
+ return productionOrderService.getAllOFs();
+ }
+
+ @PostMapping("/orders/plan")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public ResponseEntity planOF(@RequestBody Map request,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ Long produitId = Long.valueOf(request.get("produitFiniId").toString());
+ BigDecimal quantite = new BigDecimal(request.get("quantite").toString());
+ LocalDate date = LocalDate.parse(request.get("datePlanifiee").toString());
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ return ResponseEntity.ok(productionOrderService.planifierOF(produitId, quantite, date, user));
+ }
+
+ @PostMapping("/orders/{id}/launch")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public ResponseEntity launchOF(@PathVariable Long id,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ return ResponseEntity.ok(productionOrderService.lancerOF(id, user));
+ }
+
+ @PostMapping("/orders/{id}/complete")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public ResponseEntity completeOF(@PathVariable Long id,
+ @RequestBody Map request,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ BigDecimal quantiteRealisee = new BigDecimal(request.get("quantiteRealisee").toString());
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ return ResponseEntity.ok(productionOrderService.terminerOF(id, quantiteRealisee, user));
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java
new file mode 100644
index 0000000..5310a5e
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/PurchaseOrderController.java
@@ -0,0 +1,48 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.GoodsReceipt;
+import com.rayhan.erp.model.PurchaseOrder;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.UserRepository;
+import com.rayhan.erp.security.services.UserDetailsImpl;
+import com.rayhan.erp.service.PurchaseOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/purchase-orders")
+public class PurchaseOrderController {
+
+ @Autowired private PurchaseOrderService purchaseOrderService;
+ @Autowired private UserRepository userRepository;
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public List getAllOrders() {
+ return purchaseOrderService.getAllPurchaseOrders();
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT')")
+ public ResponseEntity createOrder(@RequestBody PurchaseOrder order,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ order.setCreePar(user);
+ return ResponseEntity.ok(purchaseOrderService.createPurchaseOrder(order));
+ }
+
+ @PostMapping("/{id}/receive")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_ACHAT', 'ROLE_MAGASINIER')")
+ public ResponseEntity receiveGoods(@PathVariable Long id,
+ @RequestBody GoodsReceipt reception,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ return ResponseEntity.ok(purchaseOrderService.receiveGoods(id, reception, user));
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java b/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java
new file mode 100644
index 0000000..312132a
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/SalesOrderController.java
@@ -0,0 +1,48 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.DeliveryNote;
+import com.rayhan.erp.model.SalesOrder;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.UserRepository;
+import com.rayhan.erp.security.services.UserDetailsImpl;
+import com.rayhan.erp.service.SalesOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/sales-orders")
+public class SalesOrderController {
+
+ @Autowired private SalesOrderService salesOrderService;
+ @Autowired private UserRepository userRepository;
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public List getAllOrders() {
+ return salesOrderService.getAllSalesOrders();
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE')")
+ public ResponseEntity createOrder(@RequestBody SalesOrder order,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ order.setCreePar(user);
+ return ResponseEntity.ok(salesOrderService.createSalesOrder(order));
+ }
+
+ @PostMapping("/{id}/deliver")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_RESPONSABLE_VENTE', 'ROLE_MAGASINIER')")
+ public ResponseEntity createDelivery(@PathVariable Long id,
+ @RequestBody DeliveryNote bonLivraison,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+ return ResponseEntity.ok(salesOrderService.createDeliveryNote(id, bonLivraison, user));
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/controller/StockController.java b/backend/src/main/java/com/rayhan/erp/controller/StockController.java
new file mode 100644
index 0000000..bc28313
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/controller/StockController.java
@@ -0,0 +1,57 @@
+package com.rayhan.erp.controller;
+
+import com.rayhan.erp.model.Article;
+import com.rayhan.erp.model.StockMovement;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.ArticleRepository;
+import com.rayhan.erp.repository.UserRepository;
+import com.rayhan.erp.security.services.UserDetailsImpl;
+import com.rayhan.erp.service.StockService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+@RequestMapping("/api/stock")
+public class StockController {
+
+ @Autowired private StockService stockService;
+ @Autowired private ArticleRepository articleRepository;
+ @Autowired private UserRepository userRepository;
+
+ @GetMapping("/historique/{articleId}")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER', 'ROLE_RESPONSABLE_PRODUCTION')")
+ public List getHistorique(@PathVariable Long articleId) {
+ return stockService.getHistoriqueArticle(articleId);
+ }
+
+ @PostMapping("/adjust")
+ @PreAuthorize("hasAnyRole('ROLE_PDG', 'ROLE_MAGASINIER')")
+ public ResponseEntity adjustStock(@RequestBody Map request,
+ @AuthenticationPrincipal UserDetailsImpl userDetails) {
+ Long articleId = Long.valueOf(request.get("articleId").toString());
+ BigDecimal quantite = new BigDecimal(request.get("quantite").toString());
+ String type = request.get("type").toString(); // "IN" ou "OUT"
+ String motif = request.getOrDefault("motif", "Ajustement manuel").toString();
+
+ Article article = articleRepository.findById(articleId)
+ .orElseThrow(() -> new RuntimeException("Article introuvable"));
+ User user = userRepository.findById(userDetails.getId()).orElse(null);
+
+ StockMovement mouvement;
+ if ("IN".equals(type)) {
+ mouvement = stockService.entreeStock(article, quantite, "AJUSTEMENT", "ADJ", motif, user);
+ } else {
+ mouvement = stockService.sortieStock(article, quantite, "AJUSTEMENT", "ADJ", motif, user);
+ }
+
+ return ResponseEntity.ok(mouvement);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java b/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java
new file mode 100644
index 0000000..64dd9ca
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/dto/request/LoginRequest.java
@@ -0,0 +1,15 @@
+package com.rayhan.erp.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class LoginRequest {
+ @NotBlank(message = "Le nom d'utilisateur est obligatoire")
+ private String username;
+
+ @NotBlank(message = "Le mot de passe est obligatoire")
+ private String password;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java b/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java
new file mode 100644
index 0000000..3c78247
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/dto/request/SignupRequest.java
@@ -0,0 +1,31 @@
+package com.rayhan.erp.dto.request;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Set;
+
+@Getter
+@Setter
+public class SignupRequest {
+ @NotBlank
+ @Size(min = 3, max = 20)
+ private String username;
+
+ @NotBlank
+ @Size(max = 50)
+ @Email
+ private String email;
+
+ @NotBlank
+ @Size(min = 6, max = 40)
+ private String password;
+
+ private String firstName;
+ private String lastName;
+
+ private Set roles;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java b/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java
new file mode 100644
index 0000000..50fa0a8
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/dto/response/JwtResponse.java
@@ -0,0 +1,30 @@
+package com.rayhan.erp.dto.response;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+public class JwtResponse {
+ private String token;
+ private String type = "Bearer";
+ private Long id;
+ private String username;
+ private String email;
+ private String firstName;
+ private String lastName;
+ private List roles;
+
+ public JwtResponse(String accessToken, Long id, String username, String email,
+ String firstName, String lastName, List roles) {
+ this.token = accessToken;
+ this.id = id;
+ this.username = username;
+ this.email = email;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.roles = roles;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java b/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java
new file mode 100644
index 0000000..c8f7128
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/dto/response/MessageResponse.java
@@ -0,0 +1,12 @@
+package com.rayhan.erp.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class MessageResponse {
+ private String message;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/Article.java b/backend/src/main/java/com/rayhan/erp/model/Article.java
new file mode 100644
index 0000000..bd6374f
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/Article.java
@@ -0,0 +1,50 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "articles")
+@Getter
+@Setter
+@NoArgsConstructor
+public class Article {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference;
+
+ @Column(nullable = false, length = 150)
+ private String designation;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 10)
+ private TypeArticle type; // MP, PF, PSF
+
+ @Column(length = 20)
+ private String uniteMesure; // kg, unité, rouleau, m
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal prixUnitaire = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal stockActuel = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal stockMinimum = BigDecimal.ZERO;
+
+ private boolean actif = true;
+
+ public enum TypeArticle {
+ MP, // Matière Première (HDPE, LDPE, colorants)
+ PSF, // Produit Semi-Fini (film tubulaire)
+ PF // Produit Fini (sacs, film rétractable)
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/BomLine.java b/backend/src/main/java/com/rayhan/erp/model/BomLine.java
new file mode 100644
index 0000000..fd5437d
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/BomLine.java
@@ -0,0 +1,41 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * Ligne de nomenclature (BOM — Bill of Materials).
+ * Définit quelle matière première (ou PSF) est nécessaire pour fabriquer un produit fini.
+ * Ex : Pour 1000 Sacs Bertel (PF), il faut 15 kg de HDPE (MP) + 0.5 kg de colorant.
+ */
+@Entity
+@Table(name = "bom_lines", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"produit_fini_id", "composant_id"})
+})
+@Getter
+@Setter
+@NoArgsConstructor
+public class BomLine {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "produit_fini_id")
+ private Article produitFini; // doit être de type PF ou PSF
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "composant_id")
+ private Article composant; // doit être de type MP ou PSF
+
+ @Column(nullable = false, precision = 15, scale = 6)
+ private BigDecimal quantiteParUnite; // quantité de composant par unité de produit fini
+
+ @Column(length = 20)
+ private String uniteMesure; // kg, g, L, unité
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/Client.java b/backend/src/main/java/com/rayhan/erp/model/Client.java
new file mode 100644
index 0000000..ed124f5
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/Client.java
@@ -0,0 +1,34 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "clients")
+@PrimaryKeyJoinColumn(name = "tiers_id")
+@Getter
+@Setter
+@NoArgsConstructor
+public class Client extends Tiers {
+
+ @Column(length = 30)
+ private String typeClient; // Grossiste, Détaillant, Industrie
+
+ private BigDecimal plafondCredit = BigDecimal.ZERO;
+
+ private Integer delaiPaiement = 30; // jours
+
+ @Column(length = 50)
+ private String representantNom;
+
+ @Column(length = 20)
+ private String representantTelephone;
+
+ public Client(String raisonSociale, String matriculeFiscal, String telephone) {
+ super(raisonSociale, matriculeFiscal, telephone);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java b/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java
new file mode 100644
index 0000000..cdf1e47
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/DeliveryNote.java
@@ -0,0 +1,53 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "delivery_notes")
+@Getter
+@Setter
+@NoArgsConstructor
+public class DeliveryNote {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference; // ex: BL-2024-001
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "sales_order_id")
+ private SalesOrder salesOrder;
+
+ @Column(nullable = false)
+ private LocalDate dateLivraison = LocalDate.now();
+
+ @Column(length = 200)
+ private String adresseLivraison;
+
+ @Column(length = 200)
+ private String notes;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 20)
+ private StatutLivraison statut = StatutLivraison.LIVRE;
+
+ @OneToMany(mappedBy = "deliveryNote", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List lignes = new ArrayList<>();
+
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User creePar;
+
+ public enum StatutLivraison {
+ EN_PREPARATION, LIVRE, RETOURNE_PARTIEL
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java b/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java
new file mode 100644
index 0000000..1f68c6b
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/DeliveryNoteLine.java
@@ -0,0 +1,35 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "delivery_note_lines")
+@Getter
+@Setter
+@NoArgsConstructor
+public class DeliveryNoteLine {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "delivery_note_id")
+ private DeliveryNote deliveryNote;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "sales_order_line_id")
+ private SalesOrderLine salesOrderLine;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantiteLivree;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/ERole.java b/backend/src/main/java/com/rayhan/erp/model/ERole.java
new file mode 100644
index 0000000..ff5d9c9
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/ERole.java
@@ -0,0 +1,10 @@
+package com.rayhan.erp.model;
+
+public enum ERole {
+ ROLE_PDG, // Gérant (accès complet)
+ ROLE_RESPONSABLE_VENTE, // Responsable Commercial
+ ROLE_RESPONSABLE_ACHAT, // Responsable Achats
+ ROLE_RESPONSABLE_PRODUCTION, // Responsable Production
+ ROLE_MAGASINIER, // Magasinier
+ ROLE_RH // Responsable RH
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java b/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java
new file mode 100644
index 0000000..daa43e3
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/Fournisseur.java
@@ -0,0 +1,30 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(name = "fournisseurs")
+@PrimaryKeyJoinColumn(name = "tiers_id")
+@Getter
+@Setter
+@NoArgsConstructor
+public class Fournisseur extends Tiers {
+
+ @Column(length = 100)
+ private String pays = "Tunisie";
+
+ @Column(length = 50)
+ private String categorieProduit; // Matières plastiques, Emballages...
+
+ private Integer delaiLivraison = 7; // jours
+
+ @Column(length = 30)
+ private String modePaiement; // Virement, Chèque, Espèces
+
+ public Fournisseur(String raisonSociale, String matriculeFiscal, String telephone) {
+ super(raisonSociale, matriculeFiscal, telephone);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java b/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java
new file mode 100644
index 0000000..ca2d7f6
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/GoodsReceipt.java
@@ -0,0 +1,42 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "goods_receipts")
+@Getter
+@Setter
+@NoArgsConstructor
+public class GoodsReceipt {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference; // ex: BR-2024-001
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "purchase_order_id")
+ private PurchaseOrder purchaseOrder;
+
+ @Column(nullable = false)
+ private LocalDate dateReception = LocalDate.now();
+
+ @Column(length = 200)
+ private String notes;
+
+ @OneToMany(mappedBy = "goodsReceipt", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List lignes = new ArrayList<>();
+
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User creePar;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java b/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java
new file mode 100644
index 0000000..c4c5667
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/GoodsReceiptLine.java
@@ -0,0 +1,38 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "goods_receipt_lines")
+@Getter
+@Setter
+@NoArgsConstructor
+public class GoodsReceiptLine {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "goods_receipt_id")
+ private GoodsReceipt goodsReceipt;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "purchase_order_line_id")
+ private PurchaseOrderLine purchaseOrderLine;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantiteRecue;
+
+ @Column(length = 200)
+ private String observations;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java b/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java
new file mode 100644
index 0000000..d0d2fda
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/ProductionOrder.java
@@ -0,0 +1,56 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "production_orders")
+@Getter
+@Setter
+@NoArgsConstructor
+public class ProductionOrder {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference; // ex: OF-2024-001
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "produit_fini_id")
+ private Article produitFini;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantitePlanifiee;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal quantiteRealisee = BigDecimal.ZERO;
+
+ @Column(nullable = false)
+ private LocalDate datePlanifiee;
+
+ private LocalDateTime dateLancement;
+ private LocalDateTime dateTerminaison;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 20)
+ private StatutOF statut = StatutOF.PLANIFIE;
+
+ @Column(length = 500)
+ private String notes;
+
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User creePar;
+
+ public enum StatutOF {
+ PLANIFIE, LANCE, EN_COURS, TERMINE, ANNULE
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java
new file mode 100644
index 0000000..ef11140
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrder.java
@@ -0,0 +1,62 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "purchase_orders")
+@Getter
+@Setter
+@NoArgsConstructor
+public class PurchaseOrder {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference; // ex: BC-2024-001
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "fournisseur_id")
+ private Fournisseur fournisseur;
+
+ @Column(nullable = false)
+ private LocalDate dateCommande = LocalDate.now();
+
+ private LocalDate dateLivraisonPrevue;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 20)
+ private StatutCommande statut = StatutCommande.BROUILLON;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalHT = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalTVA = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalTTC = BigDecimal.ZERO;
+
+ @Column(length = 500)
+ private String notes;
+
+ @OneToMany(mappedBy = "purchaseOrder", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List lignes = new ArrayList<>();
+
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User creePar;
+
+ public enum StatutCommande {
+ BROUILLON, CONFIRMEE, PARTIELLEMENT_RECUE, COMPLETEMENT_RECUE, ANNULEE
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java
new file mode 100644
index 0000000..3ac27b2
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/PurchaseOrderLine.java
@@ -0,0 +1,46 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "purchase_order_lines")
+@Getter
+@Setter
+@NoArgsConstructor
+public class PurchaseOrderLine {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "purchase_order_id")
+ private PurchaseOrder purchaseOrder;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantiteCommandee;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal quantiteRecue = BigDecimal.ZERO;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal prixUnitaireHT;
+
+ @Column(precision = 5, scale = 2)
+ private BigDecimal tauxTVA = new BigDecimal("19.00");
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal montantHT;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal montantTTC;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/Role.java b/backend/src/main/java/com/rayhan/erp/model/Role.java
new file mode 100644
index 0000000..14c98d6
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/Role.java
@@ -0,0 +1,26 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(name = "roles")
+@Getter
+@Setter
+@NoArgsConstructor
+public class Role {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 30, unique = true, nullable = false)
+ private ERole name;
+
+ public Role(ERole name) {
+ this.name = name;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java b/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java
new file mode 100644
index 0000000..63a4e7b
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/SalesOrder.java
@@ -0,0 +1,62 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "sales_orders")
+@Getter
+@Setter
+@NoArgsConstructor
+public class SalesOrder {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false, length = 30)
+ private String reference; // ex: CC-2024-001
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "client_id")
+ private Client client;
+
+ @Column(nullable = false)
+ private LocalDate dateCommande = LocalDate.now();
+
+ private LocalDate dateLivraisonSouhaitee;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 25)
+ private StatutCommande statut = StatutCommande.CONFIRMEE;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalHT = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalTVA = BigDecimal.ZERO;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal totalTTC = BigDecimal.ZERO;
+
+ @Column(length = 500)
+ private String notes;
+
+ @OneToMany(mappedBy = "salesOrder", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List lignes = new ArrayList<>();
+
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User creePar;
+
+ public enum StatutCommande {
+ CONFIRMEE, EN_PREPARATION, PARTIELLEMENT_LIVREE, COMPLETEMENT_LIVREE, ANNULEE
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java b/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java
new file mode 100644
index 0000000..397ec0c
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/SalesOrderLine.java
@@ -0,0 +1,46 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "sales_order_lines")
+@Getter
+@Setter
+@NoArgsConstructor
+public class SalesOrderLine {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "sales_order_id")
+ private SalesOrder salesOrder;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantiteCommandee;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal quantiteLivree = BigDecimal.ZERO;
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal prixUnitaireHT;
+
+ @Column(precision = 5, scale = 2)
+ private BigDecimal tauxTVA = new BigDecimal("19.00");
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal montantHT;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal montantTTC;
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/StockMovement.java b/backend/src/main/java/com/rayhan/erp/model/StockMovement.java
new file mode 100644
index 0000000..b286ba8
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/StockMovement.java
@@ -0,0 +1,59 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "stock_movements")
+@Getter
+@Setter
+@NoArgsConstructor
+public class StockMovement {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false, length = 10)
+ private TypeMouvement type; // IN = entrée, OUT = sortie
+
+ @Column(nullable = false, precision = 15, scale = 3)
+ private BigDecimal quantite;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal stockAvant;
+
+ @Column(precision = 15, scale = 3)
+ private BigDecimal stockApres;
+
+ @Column(length = 50)
+ private String sourceDocument; // BON_RECEPTION, BON_LIVRAISON, OF, AJUSTEMENT
+
+ @Column(length = 30)
+ private String referenceDocument; // ex: BR-2024-001
+
+ @Column(length = 200)
+ private String motif;
+
+ @Column(nullable = false)
+ private LocalDateTime dateHeure = LocalDateTime.now();
+
+ @ManyToOne
+ @JoinColumn(name = "user_id")
+ private User creePar;
+
+ public enum TypeMouvement {
+ IN, // Entrée stock
+ OUT // Sortie stock
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/Tiers.java b/backend/src/main/java/com/rayhan/erp/model/Tiers.java
new file mode 100644
index 0000000..cfbc671
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/Tiers.java
@@ -0,0 +1,45 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(name = "tiers")
+@Inheritance(strategy = InheritanceType.JOINED)
+@Getter
+@Setter
+@NoArgsConstructor
+public abstract class Tiers {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 100)
+ private String raisonSociale;
+
+ @Column(length = 20)
+ private String matriculeFiscal;
+
+ @Column(length = 200)
+ private String adresse;
+
+ @Column(length = 20)
+ private String telephone;
+
+ @Column(length = 100)
+ private String email;
+
+ @Column(length = 50)
+ private String ville;
+
+ private boolean actif = true;
+
+ public Tiers(String raisonSociale, String matriculeFiscal, String telephone) {
+ this.raisonSociale = raisonSociale;
+ this.matriculeFiscal = matriculeFiscal;
+ this.telephone = telephone;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/model/User.java b/backend/src/main/java/com/rayhan/erp/model/User.java
new file mode 100644
index 0000000..0e795d5
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/model/User.java
@@ -0,0 +1,55 @@
+package com.rayhan.erp.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+@Table(name = "users", uniqueConstraints = {
+ @UniqueConstraint(columnNames = "username"),
+ @UniqueConstraint(columnNames = "email")
+})
+@Getter
+@Setter
+@NoArgsConstructor
+public class User {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 50)
+ private String username;
+
+ @Column(nullable = false, length = 100)
+ private String email;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Column(length = 50)
+ private String firstName;
+
+ @Column(length = 50)
+ private String lastName;
+
+ private boolean enabled = true;
+
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(name = "user_roles",
+ joinColumns = @JoinColumn(name = "user_id"),
+ inverseJoinColumns = @JoinColumn(name = "role_id"))
+ private Set roles = new HashSet<>();
+
+ public User(String username, String email, String password, String firstName, String lastName) {
+ this.username = username;
+ this.email = email;
+ this.password = password;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java
new file mode 100644
index 0000000..2975786
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/ArticleRepository.java
@@ -0,0 +1,17 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.Article;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface ArticleRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ Boolean existsByReference(String reference);
+ List findByType(Article.TypeArticle type);
+ List findByActifTrue();
+ List findByStockActuelLessThanEqualAndActifTrue(java.math.BigDecimal seuil);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java b/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java
new file mode 100644
index 0000000..db5e60f
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/BomLineRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.BomLine;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface BomLineRepository extends JpaRepository {
+ List findByProduitFiniId(Long produitFiniId);
+ void deleteByProduitFiniId(Long produitFiniId);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java
new file mode 100644
index 0000000..f716794
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/ClientRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.Client;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ClientRepository extends JpaRepository {
+ List findByActifTrue();
+ List findByRaisonSocialeContainingIgnoreCase(String search);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java b/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java
new file mode 100644
index 0000000..2b74f60
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/DeliveryNoteRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.DeliveryNote;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface DeliveryNoteRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ boolean existsByReference(String reference);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java b/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java
new file mode 100644
index 0000000..ed8f3de
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/FournisseurRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.Fournisseur;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface FournisseurRepository extends JpaRepository {
+ List findByActifTrue();
+ List findByRaisonSocialeContainingIgnoreCase(String search);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java b/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java
new file mode 100644
index 0000000..146f5ca
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/GoodsReceiptRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.GoodsReceipt;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface GoodsReceiptRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ boolean existsByReference(String reference);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java
new file mode 100644
index 0000000..7a927ca
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/ProductionOrderRepository.java
@@ -0,0 +1,16 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.ProductionOrder;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface ProductionOrderRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ boolean existsByReference(String reference);
+ List findByStatutOrderByDatePlanifieeDesc(ProductionOrder.StatutOF statut);
+ long countByStatut(ProductionOrder.StatutOF statut);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java
new file mode 100644
index 0000000..81cfa1e
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/PurchaseOrderRepository.java
@@ -0,0 +1,16 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.PurchaseOrder;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface PurchaseOrderRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ boolean existsByReference(String reference);
+ List findByStatutOrderByDateCommandeDesc(PurchaseOrder.StatutCommande statut);
+ long countByStatutIn(List statuts);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java b/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java
new file mode 100644
index 0000000..84db17a
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/RoleRepository.java
@@ -0,0 +1,13 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.ERole;
+import com.rayhan.erp.model.Role;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RoleRepository extends JpaRepository {
+ Optional findByName(ERole name);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java b/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java
new file mode 100644
index 0000000..bcca810
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/SalesOrderRepository.java
@@ -0,0 +1,23 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.SalesOrder;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface SalesOrderRepository extends JpaRepository {
+ Optional findByReference(String reference);
+ boolean existsByReference(String reference);
+ List findByStatutOrderByDateCommandeDesc(SalesOrder.StatutCommande statut);
+
+ @Query("SELECT COALESCE(SUM(s.totalTTC), 0) FROM SalesOrder s WHERE s.dateCommande BETWEEN :debut AND :fin")
+ BigDecimal sumTotalTTCByDateCommandeBetween(LocalDate debut, LocalDate fin);
+
+ long countByDateCommandeBetween(LocalDate debut, LocalDate fin);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java b/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java
new file mode 100644
index 0000000..c45266f
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/StockMovementRepository.java
@@ -0,0 +1,14 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.StockMovement;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+public interface StockMovementRepository extends JpaRepository {
+ List findByArticleIdOrderByDateHeureDesc(Long articleId);
+ List findByDateHeureBetweenOrderByDateHeureDesc(LocalDateTime debut, LocalDateTime fin);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java b/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java
new file mode 100644
index 0000000..abcb647
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/repository/UserRepository.java
@@ -0,0 +1,14 @@
+package com.rayhan.erp.repository;
+
+import com.rayhan.erp.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+ Optional findByUsername(String username);
+ Boolean existsByUsername(String username);
+ Boolean existsByEmail(String email);
+}
diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java
new file mode 100644
index 0000000..bf8d5bc
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthEntryPointJwt.java
@@ -0,0 +1,39 @@
+package com.rayhan.erp.security.jwt;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class AuthEntryPointJwt implements AuthenticationEntryPoint {
+
+ private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
+
+ @Override
+ public void commence(HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException) throws IOException {
+ logger.error("Erreur d'accès non autorisé : {}", authException.getMessage());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+ final Map body = new HashMap<>();
+ body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
+ body.put("error", "Non autorisé");
+ body.put("message", authException.getMessage());
+ body.put("path", request.getServletPath());
+
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.writeValue(response.getOutputStream(), body);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java
new file mode 100644
index 0000000..3564f0d
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/security/jwt/AuthTokenFilter.java
@@ -0,0 +1,57 @@
+package com.rayhan.erp.security.jwt;
+
+import com.rayhan.erp.security.services.UserDetailsServiceImpl;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class AuthTokenFilter extends OncePerRequestFilter {
+
+ @Autowired
+ private JwtUtils jwtUtils;
+
+ @Autowired
+ private UserDetailsServiceImpl userDetailsService;
+
+ private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ try {
+ String jwt = parseJwt(request);
+ if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
+ String username = jwtUtils.getUsernameFromJwtToken(jwt);
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ } catch (Exception e) {
+ logger.error("Impossible de définir l'authentification : {}", e.getMessage());
+ }
+ filterChain.doFilter(request, response);
+ }
+
+ private String parseJwt(HttpServletRequest request) {
+ String headerAuth = request.getHeader("Authorization");
+ if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
+ return headerAuth.substring(7);
+ }
+ return null;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java b/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java
new file mode 100644
index 0000000..aca1c91
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/security/jwt/JwtUtils.java
@@ -0,0 +1,61 @@
+package com.rayhan.erp.security.jwt;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+
+@Component
+public class JwtUtils {
+
+ private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
+
+ @Value("${rayhan.erp.jwtSecret}")
+ private String jwtSecret;
+
+ @Value("${rayhan.erp.jwtExpirationMs}")
+ private int jwtExpirationMs;
+
+ private SecretKey key() {
+ return Keys.hmacShaKeyFor(jwtSecret.getBytes());
+ }
+
+ public String generateJwtToken(Authentication authentication) {
+ org.springframework.security.core.userdetails.UserDetails userPrincipal =
+ (org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal();
+ return Jwts.builder()
+ .subject(userPrincipal.getUsername())
+ .issuedAt(new Date())
+ .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
+ .signWith(key())
+ .compact();
+ }
+
+ public String getUsernameFromJwtToken(String token) {
+ return Jwts.parser().verifyWith(key()).build()
+ .parseSignedClaims(token).getPayload().getSubject();
+ }
+
+ public boolean validateJwtToken(String authToken) {
+ try {
+ Jwts.parser().verifyWith(key()).build().parseSignedClaims(authToken);
+ return true;
+ } catch (MalformedJwtException e) {
+ logger.error("Token JWT invalide : {}", e.getMessage());
+ } catch (ExpiredJwtException e) {
+ logger.error("Token JWT expiré : {}", e.getMessage());
+ } catch (UnsupportedJwtException e) {
+ logger.error("Token JWT non supporté : {}", e.getMessage());
+ } catch (IllegalArgumentException e) {
+ logger.error("JWT claims vide : {}", e.getMessage());
+ }
+ return false;
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java
new file mode 100644
index 0000000..414f8c6
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsImpl.java
@@ -0,0 +1,66 @@
+package com.rayhan.erp.security.services;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.rayhan.erp.model.User;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class UserDetailsImpl implements UserDetails {
+
+ private Long id;
+ private String username;
+ private String email;
+ private String firstName;
+ private String lastName;
+
+ @JsonIgnore
+ private String password;
+
+ private Collection extends GrantedAuthority> authorities;
+
+ public UserDetailsImpl(Long id, String username, String email,
+ String firstName, String lastName,
+ String password,
+ Collection extends GrantedAuthority> authorities) {
+ this.id = id;
+ this.username = username;
+ this.email = email;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.password = password;
+ this.authorities = authorities;
+ }
+
+ public static UserDetailsImpl build(User user) {
+ List authorities = user.getRoles().stream()
+ .map(role -> new SimpleGrantedAuthority(role.getName().name()))
+ .collect(Collectors.toList());
+
+ return new UserDetailsImpl(
+ user.getId(),
+ user.getUsername(),
+ user.getEmail(),
+ user.getFirstName(),
+ user.getLastName(),
+ user.getPassword(),
+ authorities);
+ }
+
+ public Long getId() { return id; }
+ public String getEmail() { return email; }
+ public String getFirstName() { return firstName; }
+ public String getLastName() { return lastName; }
+
+ @Override public String getUsername() { return username; }
+ @Override public String getPassword() { return password; }
+ @Override public Collection extends GrantedAuthority> getAuthorities() { return authorities; }
+ @Override public boolean isAccountNonExpired() { return true; }
+ @Override public boolean isAccountNonLocked() { return true; }
+ @Override public boolean isCredentialsNonExpired() { return true; }
+ @Override public boolean isEnabled() { return true; }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..7f3da00
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/security/services/UserDetailsServiceImpl.java
@@ -0,0 +1,26 @@
+package com.rayhan.erp.security.services;
+
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.UserRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ @Autowired
+ UserRepository userRepository;
+
+ @Override
+ @Transactional
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User user = userRepository.findByUsername(username)
+ .orElseThrow(() -> new UsernameNotFoundException(
+ "Utilisateur introuvable : " + username));
+ return UserDetailsImpl.build(user);
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java b/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java
new file mode 100644
index 0000000..d44903f
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/service/ProductionOrderService.java
@@ -0,0 +1,118 @@
+package com.rayhan.erp.service;
+
+import com.rayhan.erp.model.*;
+import com.rayhan.erp.repository.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class ProductionOrderService {
+
+ @Autowired private ProductionOrderRepository productionOrderRepository;
+ @Autowired private BomLineRepository bomLineRepository;
+ @Autowired private ArticleRepository articleRepository;
+ @Autowired private StockService stockService;
+
+ private static int seqOF = 1;
+
+ private String generateRefOF() {
+ return "OF-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqOF++);
+ }
+
+ /**
+ * Planifie un ordre de fabrication après vérification des matières premières.
+ */
+ @Transactional
+ public ProductionOrder planifierOF(Long produitFiniId, BigDecimal quantite,
+ LocalDate datePlanifiee, User user) {
+ Article produitFini = articleRepository.findById(produitFiniId)
+ .orElseThrow(() -> new RuntimeException("Produit introuvable : " + produitFiniId));
+
+ List bom = bomLineRepository.findByProduitFiniId(produitFiniId);
+ if (bom.isEmpty()) {
+ throw new RuntimeException("Aucune nomenclature définie pour " + produitFini.getDesignation());
+ }
+
+ // Vérification du stock matières premières
+ for (BomLine ligne : bom) {
+ BigDecimal qteNecessaire = ligne.getQuantiteParUnite().multiply(quantite);
+ Article composant = ligne.getComposant();
+ if (composant.getStockActuel().compareTo(qteNecessaire) < 0) {
+ throw new IllegalStateException(
+ "Stock insuffisant de " + composant.getDesignation() +
+ " : disponible " + composant.getStockActuel() +
+ ", nécessaire " + qteNecessaire);
+ }
+ }
+
+ ProductionOrder of = new ProductionOrder();
+ of.setReference(generateRefOF());
+ of.setProduitFini(produitFini);
+ of.setQuantitePlanifiee(quantite);
+ of.setDatePlanifiee(datePlanifiee);
+ of.setStatut(ProductionOrder.StatutOF.PLANIFIE);
+ of.setCreePar(user);
+
+ return productionOrderRepository.save(of);
+ }
+
+ /**
+ * Lance un OF : consomme les matières premières du stock.
+ */
+ @Transactional
+ public ProductionOrder lancerOF(Long ofId, User user) {
+ ProductionOrder of = productionOrderRepository.findById(ofId)
+ .orElseThrow(() -> new RuntimeException("OF introuvable : " + ofId));
+
+ if (of.getStatut() != ProductionOrder.StatutOF.PLANIFIE) {
+ throw new IllegalStateException("L'OF doit être à l'état PLANIFIE pour être lancé.");
+ }
+
+ List bom = bomLineRepository.findByProduitFiniId(of.getProduitFini().getId());
+
+ for (BomLine ligne : bom) {
+ BigDecimal qteConsommee = ligne.getQuantiteParUnite().multiply(of.getQuantitePlanifiee());
+ Article composant = articleRepository.findById(ligne.getComposant().getId()).get();
+ stockService.sortieStock(composant, qteConsommee,
+ "ORDRE_FABRICATION", of.getReference(),
+ "Lancement OF " + of.getReference(), user);
+ }
+
+ of.setStatut(ProductionOrder.StatutOF.LANCE);
+ of.setDateLancement(LocalDateTime.now());
+ return productionOrderRepository.save(of);
+ }
+
+ /**
+ * Termine un OF : entre le produit fini en stock.
+ */
+ @Transactional
+ public ProductionOrder terminerOF(Long ofId, BigDecimal quantiteRealisee, User user) {
+ ProductionOrder of = productionOrderRepository.findById(ofId)
+ .orElseThrow(() -> new RuntimeException("OF introuvable : " + ofId));
+
+ if (of.getStatut() != ProductionOrder.StatutOF.LANCE) {
+ throw new IllegalStateException("L'OF doit être à l'état LANCE pour être terminé.");
+ }
+
+ Article produitFini = articleRepository.findById(of.getProduitFini().getId()).get();
+ stockService.entreeStock(produitFini, quantiteRealisee,
+ "ORDRE_FABRICATION", of.getReference(),
+ "Production OF " + of.getReference(), user);
+
+ of.setQuantiteRealisee(quantiteRealisee);
+ of.setStatut(ProductionOrder.StatutOF.TERMINE);
+ of.setDateTerminaison(LocalDateTime.now());
+ return productionOrderRepository.save(of);
+ }
+
+ public List getAllOFs() {
+ return productionOrderRepository.findAll();
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java b/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java
new file mode 100644
index 0000000..eeaa602
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/service/PurchaseOrderService.java
@@ -0,0 +1,89 @@
+package com.rayhan.erp.service;
+
+import com.rayhan.erp.model.*;
+import com.rayhan.erp.repository.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Service
+public class PurchaseOrderService {
+
+ @Autowired private PurchaseOrderRepository purchaseOrderRepository;
+ @Autowired private GoodsReceiptRepository goodsReceiptRepository;
+ @Autowired private ArticleRepository articleRepository;
+ @Autowired private StockService stockService;
+
+ private static int seqBC = 1;
+ private static int seqBR = 1;
+
+ private String generateRefBC() {
+ return "BC-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBC++);
+ }
+
+ private String generateRefBR() {
+ return "BR-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBR++);
+ }
+
+ @Transactional
+ public PurchaseOrder createPurchaseOrder(PurchaseOrder order) {
+ order.setReference(generateRefBC());
+ order.setStatut(PurchaseOrder.StatutCommande.CONFIRMEE);
+
+ BigDecimal totalHT = BigDecimal.ZERO;
+ for (PurchaseOrderLine ligne : order.getLignes()) {
+ ligne.setPurchaseOrder(order);
+ BigDecimal montantHT = ligne.getPrixUnitaireHT()
+ .multiply(ligne.getQuantiteCommandee());
+ ligne.setMontantHT(montantHT);
+ BigDecimal tva = montantHT.multiply(ligne.getTauxTVA().divide(new BigDecimal("100")));
+ ligne.setMontantTTC(montantHT.add(tva));
+ totalHT = totalHT.add(montantHT);
+ }
+
+ order.setTotalHT(totalHT);
+ BigDecimal tvaGlobal = totalHT.multiply(new BigDecimal("0.19"));
+ order.setTotalTVA(tvaGlobal);
+ order.setTotalTTC(totalHT.add(tvaGlobal));
+
+ return purchaseOrderRepository.save(order);
+ }
+
+ @Transactional
+ public GoodsReceipt receiveGoods(Long purchaseOrderId, GoodsReceipt reception, User user) {
+ PurchaseOrder order = purchaseOrderRepository.findById(purchaseOrderId)
+ .orElseThrow(() -> new RuntimeException("Commande introuvable : " + purchaseOrderId));
+
+ reception.setReference(generateRefBR());
+ reception.setPurchaseOrder(order);
+ reception.setCreePar(user);
+
+ for (GoodsReceiptLine ligne : reception.getLignes()) {
+ ligne.setGoodsReceipt(reception);
+ Article article = articleRepository.findById(ligne.getArticle().getId())
+ .orElseThrow(() -> new RuntimeException("Article introuvable"));
+ ligne.setArticle(article);
+
+ stockService.entreeStock(article, ligne.getQuantiteRecue(),
+ "BON_RECEPTION", reception.getReference(),
+ "Réception commande " + order.getReference(), user);
+
+ ligne.getPurchaseOrderLine().setQuantiteRecue(
+ ligne.getPurchaseOrderLine().getQuantiteRecue().add(ligne.getQuantiteRecue()));
+ }
+
+ order.setStatut(PurchaseOrder.StatutCommande.COMPLETEMENT_RECUE);
+ purchaseOrderRepository.save(order);
+
+ return goodsReceiptRepository.save(reception);
+ }
+
+ public List getAllPurchaseOrders() {
+ return purchaseOrderRepository.findAll();
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java b/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java
new file mode 100644
index 0000000..870e053
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/service/SalesOrderService.java
@@ -0,0 +1,97 @@
+package com.rayhan.erp.service;
+
+import com.rayhan.erp.model.*;
+import com.rayhan.erp.repository.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+@Service
+public class SalesOrderService {
+
+ @Autowired private SalesOrderRepository salesOrderRepository;
+ @Autowired private DeliveryNoteRepository deliveryNoteRepository;
+ @Autowired private ArticleRepository articleRepository;
+ @Autowired private StockService stockService;
+
+ private static int seqCC = 1;
+ private static int seqBL = 1;
+
+ private String generateRefCC() {
+ return "CC-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqCC++);
+ }
+
+ private String generateRefBL() {
+ return "BL-" + LocalDate.now().getYear() + "-" + String.format("%03d", seqBL++);
+ }
+
+ @Transactional
+ public SalesOrder createSalesOrder(SalesOrder order) {
+ // Vérification du stock disponible avant validation
+ for (SalesOrderLine ligne : order.getLignes()) {
+ Article article = articleRepository.findById(ligne.getArticle().getId())
+ .orElseThrow(() -> new RuntimeException("Article introuvable"));
+ if (article.getStockActuel().compareTo(ligne.getQuantiteCommandee()) < 0) {
+ throw new IllegalStateException(
+ "Stock insuffisant pour " + article.getDesignation());
+ }
+ }
+
+ order.setReference(generateRefCC());
+ order.setStatut(SalesOrder.StatutCommande.CONFIRMEE);
+
+ BigDecimal totalHT = BigDecimal.ZERO;
+ for (SalesOrderLine ligne : order.getLignes()) {
+ ligne.setSalesOrder(order);
+ BigDecimal montantHT = ligne.getPrixUnitaireHT().multiply(ligne.getQuantiteCommandee());
+ ligne.setMontantHT(montantHT);
+ BigDecimal tva = montantHT.multiply(ligne.getTauxTVA().divide(new BigDecimal("100")));
+ ligne.setMontantTTC(montantHT.add(tva));
+ totalHT = totalHT.add(montantHT);
+ }
+
+ order.setTotalHT(totalHT);
+ BigDecimal tvaGlobal = totalHT.multiply(new BigDecimal("0.19"));
+ order.setTotalTVA(tvaGlobal);
+ order.setTotalTTC(totalHT.add(tvaGlobal));
+
+ return salesOrderRepository.save(order);
+ }
+
+ @Transactional
+ public DeliveryNote createDeliveryNote(Long salesOrderId, DeliveryNote bonLivraison, User user) {
+ SalesOrder order = salesOrderRepository.findById(salesOrderId)
+ .orElseThrow(() -> new RuntimeException("Commande introuvable : " + salesOrderId));
+
+ bonLivraison.setReference(generateRefBL());
+ bonLivraison.setSalesOrder(order);
+ bonLivraison.setCreePar(user);
+
+ for (DeliveryNoteLine ligne : bonLivraison.getLignes()) {
+ ligne.setDeliveryNote(bonLivraison);
+ Article article = articleRepository.findById(ligne.getArticle().getId())
+ .orElseThrow(() -> new RuntimeException("Article introuvable"));
+ ligne.setArticle(article);
+
+ stockService.sortieStock(article, ligne.getQuantiteLivree(),
+ "BON_LIVRAISON", bonLivraison.getReference(),
+ "Livraison commande " + order.getReference(), user);
+
+ ligne.getSalesOrderLine().setQuantiteLivree(
+ ligne.getSalesOrderLine().getQuantiteLivree().add(ligne.getQuantiteLivree()));
+ }
+
+ order.setStatut(SalesOrder.StatutCommande.COMPLETEMENT_LIVREE);
+ salesOrderRepository.save(order);
+
+ return deliveryNoteRepository.save(bonLivraison);
+ }
+
+ public List getAllSalesOrders() {
+ return salesOrderRepository.findAll();
+ }
+}
diff --git a/backend/src/main/java/com/rayhan/erp/service/StockService.java b/backend/src/main/java/com/rayhan/erp/service/StockService.java
new file mode 100644
index 0000000..078e5f6
--- /dev/null
+++ b/backend/src/main/java/com/rayhan/erp/service/StockService.java
@@ -0,0 +1,95 @@
+package com.rayhan.erp.service;
+
+import com.rayhan.erp.model.Article;
+import com.rayhan.erp.model.StockMovement;
+import com.rayhan.erp.model.User;
+import com.rayhan.erp.repository.ArticleRepository;
+import com.rayhan.erp.repository.StockMovementRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Service
+public class StockService {
+
+ @Autowired
+ private ArticleRepository articleRepository;
+
+ @Autowired
+ private StockMovementRepository stockMovementRepository;
+
+ /**
+ * Effectue une entrée en stock (réception achat, production terminée, ajustement +)
+ */
+ @Transactional
+ public StockMovement entreeStock(Article article, BigDecimal quantite,
+ String sourceDocument, String referenceDocument,
+ String motif, User user) {
+ if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("La quantité doit être positive.");
+ }
+
+ BigDecimal stockAvant = article.getStockActuel();
+ BigDecimal stockApres = stockAvant.add(quantite);
+
+ article.setStockActuel(stockApres);
+ articleRepository.save(article);
+
+ StockMovement mouvement = new StockMovement();
+ mouvement.setArticle(article);
+ mouvement.setType(StockMovement.TypeMouvement.IN);
+ mouvement.setQuantite(quantite);
+ mouvement.setStockAvant(stockAvant);
+ mouvement.setStockApres(stockApres);
+ mouvement.setSourceDocument(sourceDocument);
+ mouvement.setReferenceDocument(referenceDocument);
+ mouvement.setMotif(motif);
+ mouvement.setCreePar(user);
+
+ return stockMovementRepository.save(mouvement);
+ }
+
+ /**
+ * Effectue une sortie de stock (livraison client, lancement OF, ajustement -)
+ * Lance une exception si stock insuffisant.
+ */
+ @Transactional
+ public StockMovement sortieStock(Article article, BigDecimal quantite,
+ String sourceDocument, String referenceDocument,
+ String motif, User user) {
+ if (quantite.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("La quantité doit être positive.");
+ }
+ if (article.getStockActuel().compareTo(quantite) < 0) {
+ throw new IllegalStateException(
+ "Stock insuffisant pour " + article.getDesignation() +
+ " : disponible " + article.getStockActuel() + ", demandé " + quantite);
+ }
+
+ BigDecimal stockAvant = article.getStockActuel();
+ BigDecimal stockApres = stockAvant.subtract(quantite);
+
+ article.setStockActuel(stockApres);
+ articleRepository.save(article);
+
+ StockMovement mouvement = new StockMovement();
+ mouvement.setArticle(article);
+ mouvement.setType(StockMovement.TypeMouvement.OUT);
+ mouvement.setQuantite(quantite);
+ mouvement.setStockAvant(stockAvant);
+ mouvement.setStockApres(stockApres);
+ mouvement.setSourceDocument(sourceDocument);
+ mouvement.setReferenceDocument(referenceDocument);
+ mouvement.setMotif(motif);
+ mouvement.setCreePar(user);
+
+ return stockMovementRepository.save(mouvement);
+ }
+
+ public List getHistoriqueArticle(Long articleId) {
+ return stockMovementRepository.findByArticleIdOrderByDateHeureDesc(articleId);
+ }
+}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
new file mode 100644
index 0000000..a84128e
--- /dev/null
+++ b/backend/src/main/resources/application.properties
@@ -0,0 +1,27 @@
+# ===================================
+# Configuration de l'application ERP
+# SUARL Rayhan — PFE Ali Guennari
+# ===================================
+
+# Serveur
+server.port=8080
+
+# Base de données MySQL
+spring.datasource.url=jdbc:mysql://localhost:3306/rayhan_erp_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Africa/Tunis
+spring.datasource.username=root
+spring.datasource.password=rayhan_erp_2024
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+# JPA / Hibernate
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=false
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
+spring.jpa.properties.hibernate.format_sql=true
+
+# JWT
+rayhan.erp.jwtSecret=RayhanERP_SecretKey_PFE_AliGuennari_2024_TunisiePlasturgie_SUARL
+rayhan.erp.jwtExpirationMs=86400000
+
+# Logging
+logging.level.com.rayhan.erp=DEBUG
+logging.level.org.springframework.security=INFO
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..519b2d0
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,54 @@
+version: '3.8'
+
+# ==========================================
+# Docker Compose — Rayhan ERP
+# Backend Spring Boot + MySQL
+# ==========================================
+
+services:
+
+ mysql:
+ image: mysql:8.0
+ container_name: rayhan-mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: rayhan_erp_2024
+ MYSQL_DATABASE: rayhan_erp_db
+ MYSQL_USER: rayhan_user
+ MYSQL_PASSWORD: rayhan_erp_2024
+ volumes:
+ - mysql_data:/var/lib/mysql
+ ports:
+ - "3307:3306"
+ networks:
+ - rayhan-net
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prayhan_erp_2024"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ backend:
+ build: ./backend
+ container_name: rayhan-backend
+ restart: unless-stopped
+ depends_on:
+ mysql:
+ condition: service_healthy
+ environment:
+ SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/rayhan_erp_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Africa/Tunis
+ SPRING_DATASOURCE_USERNAME: root
+ SPRING_DATASOURCE_PASSWORD: rayhan_erp_2024
+ RAYHAN_ERP_JWTSECRET: RayhanERP_SecretKey_PFE_AliGuennari_2024_TunisiePlasturgie_SUARL
+ RAYHAN_ERP_JWTEXPIRATIONMS: 86400000
+ ports:
+ - "8080:8080"
+ networks:
+ - rayhan-net
+
+volumes:
+ mysql_data:
+
+networks:
+ rayhan-net:
+ driver: bridge