diff --git a/.env b/.env index 3b2245b..a94444b 100644 --- a/.env +++ b/.env @@ -34,7 +34,7 @@ DEFAULT_URI=http://localhost # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### -DATABASE_URL="mysql://appcontrib:123abc@127.0.0.1:3306/contribV2?serverVersion=8.0.32&charset=utf8mb4" +DATABASE_URL="mysql://appcontrib:123abc@127.0.0.1:3307/contribV2?serverVersion=8.0.32&charset=utf8mb4" ###> symfony/messenger ### # Choose one of the transports below # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3610f19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.tabCompletion": "on", + "diffEditor.codeLens": true, + "MutableAI.upsell": true +} \ No newline at end of file diff --git a/INLINE_EDITING.md b/INLINE_EDITING.md new file mode 100644 index 0000000..d282ccc --- /dev/null +++ b/INLINE_EDITING.md @@ -0,0 +1,205 @@ +# Système d'Édition Inline avec Verrouillage + +## Vue d'ensemble + +Ce système permet l'édition directe des données dans les listes avec un mécanisme de verrouillage pour éviter les modifications concurrentes. + +## Fonctionnalités + +### ✅ Édition Inline +- **Clic pour éditer** : Cliquez sur une cellule éditables pour la modifier +- **Sauvegarde automatique** : Les modifications sont sauvegardées après 2 secondes d'inactivité +- **Validation en temps réel** : Vérification des données avant sauvegarde +- **Raccourcis clavier** : + - `Entrée` : Sauvegarder + - `Échap` : Annuler + +### ✅ Système de Verrouillage +- **Verrous automatiques** : Acquisition automatique lors de l'édition +- **Expiration** : Les verrous expirent après 30 minutes +- **Prolongation** : Les verrous sont automatiquement prolongés +- **Protection concurrente** : Empêche les modifications simultanées + +### ✅ Interface Utilisateur +- **Indicateurs visuels** : Cellules verrouillées avec icône 🔒 +- **Messages d'état** : Notifications de succès/erreur +- **Styles adaptatifs** : Couleurs différentes selon l'état + +## Architecture + +### Entités +- **Lock** : Gestion des verrous avec expiration +- **Membre** : Entité principale avec édition inline + +### Services +- **LockService** : Gestion des verrous (création, suppression, vérification) +- **MembreApiController** : API REST pour l'édition inline + +### JavaScript +- **InlineEditing** : Classe principale pour l'édition inline +- **Gestion des verrous** : Acquisition, prolongation, libération +- **Interface utilisateur** : Création d'inputs, validation, sauvegarde + +## Utilisation + +### 1. Édition d'un Membre +```javascript +// Clic sur une cellule éditables +// → Acquisition automatique du verrou +// → Création d'un input +// → Sauvegarde automatique après 2s +``` + +### 2. Gestion des Verrous +```bash +# Nettoyer les verrous expirés +php bin/console app:cleanup-locks + +# Voir les statistiques +/lock/stats +``` + +### 3. API Endpoints +``` +POST /api/membre/{id}/lock # Acquérir un verrou +POST /api/membre/{id}/unlock # Libérer un verrou +POST /api/membre/{id}/extend-lock # Prolonger un verrou +POST /api/membre/{id}/update-field # Mettre à jour un champ +GET /api/membre/{id}/lock-status # Statut du verrou +``` + +## Configuration + +### Durée des Verrous +```php +// Dans Lock.php +$this->expiresAt = new \DateTime('+30 minutes'); +``` + +### Vérification des Verrous +```javascript +// Dans inline-editing.js +this.lockCheckInterval = setInterval(() => { + this.checkLocks(); +}, 30000); // Toutes les 30 secondes +``` + +### Sauvegarde Automatique +```javascript +// Dans inline-editing.js +input.addEventListener('input', () => { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(() => { + this.saveField(entityType, entityId, field, input.value); + }, 2000); // 2 secondes +}); +``` + +## Sécurité + +### Protection des Données +- **Validation côté serveur** : Vérification des données avant sauvegarde +- **Vérification des verrous** : Contrôle d'accès basé sur les verrous +- **Nettoyage automatique** : Suppression des verrous expirés + +### Gestion des Conflits +- **Détection de verrous** : Vérification avant édition +- **Messages d'erreur** : Notification si élément verrouillé +- **Libération automatique** : Nettoyage à la fermeture de page + +## Monitoring + +### Statistiques +- **Verrous actifs** : Nombre de verrous en cours +- **Verrous expirés** : Éléments à nettoyer +- **Utilisateurs** : Qui modifie quoi + +### Commandes de Maintenance +```bash +# Nettoyer les verrous expirés +php bin/console app:cleanup-locks + +# Voir les statistiques +curl /lock/stats +``` + +## Extension + +### Ajouter l'Édition Inline à d'Autres Entités + +1. **Créer l'API Controller** : +```php +// src/Controller/Api/ProjetApiController.php +#[Route('/api/projet')] +class ProjetApiController extends AbstractController +{ + // Implémenter les mêmes méthodes que MembreApiController +} +``` + +2. **Mettre à jour le Template** : +```html + + {{ projet.nom }} + +``` + +3. **Ajouter les Styles** : +```css +.editable-cell { + cursor: pointer; + transition: background-color 0.2s; +} +``` + +## Dépannage + +### Problèmes Courants + +1. **Verrous non libérés** : + - Vérifier la console JavaScript + - Utiliser `/lock/stats` pour voir les verrous actifs + - Exécuter `php bin/console app:cleanup-locks` + +2. **Édition ne fonctionne pas** : + - Vérifier que le JavaScript est chargé + - Contrôler les erreurs dans la console + - Vérifier les routes API + +3. **Conflits de verrous** : + - Attendre l'expiration (30 minutes) + - Utiliser "Libérer tous mes verrous" dans `/lock/stats` + +### Logs +```bash +# Voir les logs Symfony +tail -f var/log/dev.log + +# Voir les erreurs JavaScript +# Ouvrir la console du navigateur (F12) +``` + +## Performance + +### Optimisations +- **Vérification périodique** : Toutes les 30 secondes +- **Nettoyage automatique** : Commande cron recommandée +- **Cache des verrous** : Éviter les requêtes répétées + +### Recommandations +```bash +# Ajouter au crontab +*/5 * * * * php /path/to/project/bin/console app:cleanup-locks +``` + +## Support + +Pour toute question ou problème : +1. Vérifier les logs Symfony +2. Contrôler la console JavaScript +3. Utiliser les outils de diagnostic dans `/lock/stats` + + diff --git a/_baseScripts/create_admin.sql b/_baseScripts/create_admin.sql new file mode 100644 index 0000000..0064215 --- /dev/null +++ b/_baseScripts/create_admin.sql @@ -0,0 +1,2 @@ +INSERT INTO membre (nom, prenom, email, roles, password) VALUES +('Admin', 'System', 'admin@system.com', '["ROLE_ADMIN"]', '$2y$13$QJ7LSDls6TRywbxLOtRz9uMeNS0IlEAJ8IDy4o7hnZ1.9RSdKX5Ee'); \ No newline at end of file diff --git a/_baseScripts/jeu_essai.sql b/_baseScripts/jeu_essai.sql index c2e1179..967cc48 100644 --- a/_baseScripts/jeu_essai.sql +++ b/_baseScripts/jeu_essai.sql @@ -16,9 +16,6 @@ TRUNCATE TABLE projet; TRUNCATE TABLE membre; SET FOREIGN_KEY_CHECKS = 1; --- ============================================ --- Insertion des membres (10 développeurs) --- ============================================ INSERT INTO membre (nom, prenom, email) VALUES ('Dupont', 'Alice', 'alice.dupont@tech-corp.fr'), ('Martin', 'Bob', 'bob.martin@tech-corp.fr'), @@ -30,6 +27,8 @@ INSERT INTO membre (nom, prenom, email) VALUES ('Michel', 'Hugo', 'hugo.michel@tech-corp.fr'), ('Laurent', 'Iris', 'iris.laurent@tech-corp.fr'), ('Garcia', 'Jean', 'jean.garcia@tech-corp.fr'); +-- Ajout des rôles et mots de passe si nécessaire +-- Préparation pour l'ajout d'un administrateur via commande console -- ============================================ -- Insertion des projets (3 projets) diff --git a/_baseScripts/new bdd connexion.sql b/_baseScripts/new bdd connexion.sql new file mode 100644 index 0000000..b5bbe35 --- /dev/null +++ b/_baseScripts/new bdd connexion.sql @@ -0,0 +1,160 @@ +DROP DATABASE IF EXISTS ContribV2; +CREATE DATABASE IF NOT EXISTS ContribV2; +USE ContribV2; + +-- ============================================ +-- Création d'une base de données sécurisée : contribV2 +-- ============================================ + +DROP DATABASE IF EXISTS contribV2; +CREATE DATABASE contribV2 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +USE contribV2; + +-- ============================================ +-- Création des tables +-- ============================================ + +CREATE TABLE membre ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL, + prenom VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + roles JSON NOT NULL DEFAULT '[]', + password VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE projet ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL, + commentaire TEXT, + date_lancement DATE, + date_cloture DATE, + statut VARCHAR(20) NOT NULL +); + +CREATE TABLE contribution ( + id INT AUTO_INCREMENT PRIMARY KEY, + membre_id INT NOT NULL, + projet_id INT NOT NULL, + date_contribution DATE NOT NULL, + commentaire TEXT, + duree INT DEFAULT 0, + FOREIGN KEY (membre_id) REFERENCES membre(id), + FOREIGN KEY (projet_id) REFERENCES projet(id) +); + +CREATE TABLE assistant_ia ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL +); + +CREATE TABLE contrib_ia ( + id INT AUTO_INCREMENT PRIMARY KEY, + assistant_ia_id INT NOT NULL, + contribution_id INT NOT NULL, + evaluation_pertinence INT CHECK (evaluation_pertinence >= 1 AND evaluation_pertinence <= 5), + evaluation_temps INT CHECK (evaluation_temps >= 1 AND evaluation_temps <= 5), + commentaire TEXT, + FOREIGN KEY (assistant_ia_id) REFERENCES assistant_ia(id), + FOREIGN KEY (contribution_id) REFERENCES contribution(id) +); + +-- ============================================ +-- Configuration de la sécurité et des utilisateurs +-- ============================================ + +-- Supprimer l’utilisateur existant s’il existe déjà +DROP USER IF EXISTS 'appcontrib'@'%'; +DROP USER IF EXISTS 'admincontrib'@'%'; + +-- Création d’un utilisateur applicatif avec accès restreint +CREATE USER 'appcontrib'@'%' IDENTIFIED BY '123abc'; + +-- Création d’un utilisateur administrateur (pour la maintenance) +CREATE USER 'admincontrib'@'%' IDENTIFIED BY 'Adm!nStrongPass2025'; + +-- Droits : l’utilisateur applicatif peut uniquement lire/écrire/modifier les données +GRANT SELECT, INSERT, UPDATE, DELETE ON contribV2.* TO 'appcontrib'@'%'; + +-- Droits : l’administrateur a tous les privilèges +GRANT ALL PRIVILEGES ON contribV2.* TO 'admincontrib'@'%'; + +FLUSH PRIVILEGES; + +-- ============================================ +-- Jeu d’essai +-- ============================================ + +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE contrib_ia; +TRUNCATE TABLE contribution; +TRUNCATE TABLE assistant_ia; +TRUNCATE TABLE projet; +TRUNCATE TABLE membre; +SET FOREIGN_KEY_CHECKS = 1; + +-- Membres (10) +INSERT INTO membre (nom, prenom, email) VALUES +('Dupont', 'Alice', 'alice.dupont@tech-corp.fr'), +('Martin', 'Bob', 'bob.martin@tech-corp.fr'), +('Bernard', 'Claire', 'claire.bernard@tech-corp.fr'), +('Durand', 'David', 'david.durand@tech-corp.fr'), +('Leroy', 'Emma', 'emma.leroy@tech-corp.fr'), +('Moreau', 'Frank', 'frank.moreau@tech-corp.fr'), +('Simon', 'Grace', 'grace.simon@tech-corp.fr'), +('Michel', 'Hugo', 'hugo.michel@tech-corp.fr'), +('Laurent', 'Iris', 'iris.laurent@tech-corp.fr'), +('Garcia', 'Jean', 'jean.garcia@tech-corp.fr'); + +-- Projets (3) +INSERT INTO projet (nom, commentaire, date_lancement, date_cloture, statut) VALUES +('E-Commerce Platform', 'Développement d''une nouvelle plateforme e-commerce avec microservices', '2024-09-01', NULL, 'en_cours'), +('Mobile Banking App', 'Application mobile de gestion bancaire pour iOS et Android', '2024-10-15', '2025-03-31', 'en_cours'), +('Data Analytics Dashboard', 'Tableau de bord analytique temps réel pour le département marketing', '2024-08-01', '2024-12-20', 'termine'); + +-- Assistants IA (5) +INSERT INTO assistant_ia (nom) VALUES +('GitHub Copilot'), +('Claude 3.5'), +('ChatGPT-4'), +('Cursor AI'), +('Amazon CodeWhisperer'); + +-- Contributions +INSERT INTO contribution (membre_id, projet_id, date_contribution, commentaire, duree) VALUES +(1, 1, '2024-09-05', 'Architecture initiale et setup du projet', 480), +(2, 1, '2024-09-08', 'Configuration Docker et environnement de développement', 360), +(3, 1, '2024-09-12', 'Développement du service authentification', 420), +(1, 1, '2024-09-15', 'API Gateway et routing', 300), +(4, 1, '2024-09-20', 'Service de gestion des produits', 540), +(5, 1, '2024-09-25', 'Intégration système de paiement Stripe', 480), +(2, 1, '2024-10-02', 'Tests unitaires service authentification', 240), +(3, 1, '2024-10-10', 'Optimisation des requêtes base de données', 360), + +(6, 2, '2024-10-16', 'Setup React Native et architecture mobile', 420), +(7, 2, '2024-10-18', 'Interface utilisateur - écrans de connexion', 360), +(8, 2, '2024-10-22', 'Système de notifications push', 300), +(9, 2, '2024-10-25', 'Module de virement bancaire', 480), +(10, 2, '2024-10-28', 'Sécurisation avec biométrie', 420), +(6, 2, '2024-11-02', 'Intégration API bancaire', 540), +(7, 2, '2024-11-05', 'Tests d''interface utilisateur', 240), + +(1, 3, '2024-08-05', 'Architecture backend Node.js et Express', 480), +(3, 3, '2024-08-10', 'Configuration base de données PostgreSQL', 360), +(6, 3, '2024-08-15', 'Dashboard React avec graphiques D3.js', 540), +(8, 3, '2024-08-22', 'WebSocket pour données temps réel', 420), +(1, 3, '2024-09-01', 'Optimisation des performances', 300), +(3, 3, '2024-09-10', 'Documentation et déploiement', 240); + +-- Contributions avec IA +INSERT INTO contrib_ia (assistant_ia_id, contribution_id, evaluation_pertinence, evaluation_temps, commentaire) VALUES +(1, 1, 4, 5, 'Copilot très utile pour générer la structure de base du projet'), +(2, 3, 5, 4, 'Claude excellent pour implémenter la logique d''authentification JWT'), +(3, 5, 3, 3, 'ChatGPT-4 a aidé mais nécessitait des ajustements pour le service produits'), +(1, 7, 4, 4, 'Bonne génération des tests unitaires avec Copilot'), +(4, 9, 5, 5, 'Cursor AI excellent pour le développement React Native'), +(2, 11, 4, 4, 'Claude très pertinent pour les algorithmes de chiffrement'), +(5, 13, 3, 4, 'CodeWhisperer rapide mais code nécessitant refactoring'), +(1, 15, 4, 5, 'Copilot efficace pour le setup Node.js'), +(3, 17, 5, 3, 'ChatGPT-4 excellent pour les visualisations D3.js mais un peu lent'), +(2, 19, 4, 4, 'Claude bon pour l''optimisation des requêtes SQL'); diff --git a/_baseScripts/structure.sql b/_baseScripts/structure.sql index 639af80..a767248 100644 --- a/_baseScripts/structure.sql +++ b/_baseScripts/structure.sql @@ -2,7 +2,9 @@ create table membre( id int auto_increment primary key, nom varchar(50) not null, prenom varchar(50) not null, - email varchar(100) not null unique + email varchar(100) not null unique, + roles json not null default '[]', + password varchar(255) default null ); create table projet( diff --git a/_baseScripts/structure_corrigee.sql b/_baseScripts/structure_corrigee.sql new file mode 100644 index 0000000..a3694a9 --- /dev/null +++ b/_baseScripts/structure_corrigee.sql @@ -0,0 +1,143 @@ +DROP DATABASE IF EXISTS ContribV2; +CREATE DATABASE IF NOT EXISTS ContribV2; +USE ContribV2; + +-- ============================================ +-- Base sécurisée pour gestion de contributions +-- ============================================ + +-- Réinitialisation +SET FOREIGN_KEY_CHECKS = 0; +DROP TABLE IF EXISTS contrib_ia, contribution, assistant_ia, projet, membre; +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================ +-- Table des membres (utilisateurs sécurisés) +-- ============================================ +CREATE TABLE membre ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL, + prenom VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + roles JSON NOT NULL, -- Colonne requise par Symfony Security + password VARCHAR(255) DEFAULT NULL, -- Colonne requise par Symfony Security + date_creation TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + actif BOOLEAN DEFAULT TRUE, + CONSTRAINT chk_email_format CHECK (email LIKE '%@%.%') +); + +-- ============================================ +-- Table des projets +-- ============================================ +CREATE TABLE projet ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL, + commentaire TEXT, + date_lancement DATE, + date_cloture DATE, + statut ENUM('en_cours', 'termine', 'annule') NOT NULL DEFAULT 'en_cours' +); + +-- ============================================ +-- Table des contributions +-- ============================================ +CREATE TABLE contribution ( + id INT AUTO_INCREMENT PRIMARY KEY, + membre_id INT NOT NULL, + projet_id INT NOT NULL, + date_contribution DATE NOT NULL, + commentaire TEXT, + duree INT DEFAULT 0 CHECK (duree >= 0), + FOREIGN KEY (membre_id) REFERENCES membre(id) ON DELETE CASCADE, + FOREIGN KEY (projet_id) REFERENCES projet(id) ON DELETE CASCADE +); + +-- ============================================ +-- Table des assistants IA +-- ============================================ +CREATE TABLE assistant_ia ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(50) NOT NULL UNIQUE +); + +-- ============================================ +-- Table des interactions IA / contributions +-- ============================================ +CREATE TABLE contrib_ia ( + id INT AUTO_INCREMENT PRIMARY KEY, + assistant_ia_id INT NOT NULL, + contribution_id INT NOT NULL, + evaluation_pertinence INT CHECK (evaluation_pertinence BETWEEN 1 AND 5), + evaluation_temps INT CHECK (evaluation_temps BETWEEN 1 AND 5), + commentaire TEXT, + FOREIGN KEY (assistant_ia_id) REFERENCES assistant_ia(id) ON DELETE CASCADE, + FOREIGN KEY (contribution_id) REFERENCES contribution(id) ON DELETE CASCADE +); + +-- ============================================ +-- Jeu d'essai +-- ============================================ + +-- Membres avec mots de passe hachés (mot de passe: "password") +INSERT INTO membre (nom, prenom, email, roles, password) VALUES +('Dupont', 'Alice', 'alice.dupont@tech-corp.fr', '["ROLE_ADMIN"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Martin', 'Bob', 'bob.martin@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Bernard', 'Claire', 'claire.bernard@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Durand', 'David', 'david.durand@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Leroy', 'Emma', 'emma.leroy@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Moreau', 'Frank', 'frank.moreau@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Simon', 'Grace', 'grace.simon@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Michel', 'Hugo', 'hugo.michel@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Laurent', 'Iris', 'iris.laurent@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'), +('Garcia', 'Jean', 'jean.garcia@tech-corp.fr', '["ROLE_DEV"]', '$2y$13$G5y.TE7Vfw.7tGEOfYpCgeqC4zxlzhrBb6TViUINyZC0hYtSXaWeu'); +-- R!d93xT#pWq7Zb2@ +-- Projets +INSERT INTO projet (nom, commentaire, date_lancement, date_cloture, statut) VALUES +('E-Commerce Platform', 'Développement d''une nouvelle plateforme e-commerce avec microservices', '2024-09-01', NULL, 'en_cours'), +('Mobile Banking App', 'Application mobile de gestion bancaire pour iOS et Android', '2024-10-15', '2025-03-31', 'en_cours'), +('Data Analytics Dashboard', 'Tableau de bord analytique temps réel pour le département marketing', '2024-08-01', '2024-12-20', 'termine'); + +-- Assistants IA +INSERT INTO assistant_ia (nom) VALUES +('GitHub Copilot'), +('Claude 3.5'), +('ChatGPT-4'), +('Cursor AI'), +('Amazon CodeWhisperer'); + +-- Contributions +INSERT INTO contribution (membre_id, projet_id, date_contribution, commentaire, duree) VALUES +(1, 1, '2024-09-05', 'Architecture initiale et setup du projet', 480), +(2, 1, '2024-09-08', 'Configuration Docker et environnement de développement', 360), +(3, 1, '2024-09-12', 'Développement du service authentification', 420), +(1, 1, '2024-09-15', 'API Gateway et routing', 300), +(4, 1, '2024-09-20', 'Service de gestion des produits', 540), +(5, 1, '2024-09-25', 'Intégration système de paiement Stripe', 480), +(2, 1, '2024-10-02', 'Tests unitaires service authentification', 240), +(3, 1, '2024-10-10', 'Optimisation des requêtes base de données', 360), +(6, 2, '2024-10-16', 'Setup React Native et architecture mobile', 420), +(7, 2, '2024-10-18', 'Interface utilisateur - écrans de connexion', 360), +(8, 2, '2024-10-22', 'Système de notifications push', 300), +(9, 2, '2024-10-25', 'Module de virement bancaire', 480), +(10, 2, '2024-10-28', 'Sécurisation avec biométrie', 420), +(6, 2, '2024-11-02', 'Intégration API bancaire', 540), +(7, 2, '2024-11-05', 'Tests d''interface utilisateur', 240), +(1, 3, '2024-08-05', 'Architecture backend Node.js et Express', 480), +(3, 3, '2024-08-10', 'Configuration base de données PostgreSQL', 360), +(6, 3, '2024-08-15', 'Dashboard React avec graphiques D3.js', 540), +(8, 3, '2024-08-22', 'WebSocket pour données temps réel', 420), +(1, 3, '2024-09-01', 'Optimisation des performances', 300), +(3, 3, '2024-09-10', 'Documentation et déploiement', 240); + +-- Contributions IA (~50%) +INSERT INTO contrib_ia (assistant_ia_id, contribution_id, evaluation_pertinence, evaluation_temps, commentaire) VALUES +(1, 1, 4, 5, 'Copilot très utile pour générer la structure de base du projet'), +(2, 3, 5, 4, 'Claude excellent pour implémenter la logique d''authentification JWT'), +(3, 5, 3, 3, 'ChatGPT-4 a aidé mais nécessitait des ajustements pour le service produits'), +(1, 7, 4, 4, 'Bonne génération des tests unitaires avec Copilot'), +(4, 9, 5, 5, 'Cursor AI excellent pour le développement React Native'), +(2, 11, 4, 4, 'Claude très pertinent pour les algorithmes de chiffrement'), +(5, 13, 3, 4, 'CodeWhisperer rapide mais code nécessitant refactoring'), +(1, 15, 4, 5, 'Copilot efficace pour le setup Node.js'), +(3, 17, 5, 3, 'ChatGPT-4 excellent pour les visualisations D3.js mais un peu lent'), +(2, 19, 4, 4, 'Claude bon pour l''optimisation des requêtes SQL'); \ No newline at end of file diff --git a/assets/js/inline-editing.js b/assets/js/inline-editing.js new file mode 100644 index 0000000..cd37238 --- /dev/null +++ b/assets/js/inline-editing.js @@ -0,0 +1,315 @@ +/** + * Système d'édition inline avec verrouillage + */ +class InlineEditing { + constructor() { + this.locks = new Map(); + this.lockCheckInterval = null; + this.init(); + } + + init() { + this.setupEventListeners(); + this.startLockCheck(); + } + + setupEventListeners() { + // Détecter les clics sur les cellules éditables + document.addEventListener('click', (e) => { + if (e.target.classList.contains('editable-cell')) { + this.startEditing(e.target); + } + }); + + // Détecter les clics en dehors pour sauvegarder + document.addEventListener('click', (e) => { + if (!e.target.closest('.editable-cell, .inline-edit-input')) { + this.saveAllPending(); + } + }); + + // Détecter les touches pour sauvegarder + document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + this.saveCurrentEditing(); + } else if (e.key === 'Escape') { + this.cancelCurrentEditing(); + } + }); + } + + startEditing(cell) { + if (cell.classList.contains('editing')) { + return; + } + + const entityType = cell.dataset.entityType; + const entityId = cell.dataset.entityId; + const field = cell.dataset.field; + const currentValue = cell.textContent.trim(); + + // Vérifier si déjà en cours d'édition + if (this.isEditing(entityType, entityId, field)) { + return; + } + + // Acquérir le verrou + this.acquireLock(entityType, entityId).then((success) => { + if (success) { + this.createEditInput(cell, entityType, entityId, field, currentValue); + } else { + this.showLockMessage(cell); + } + }); + } + + createEditInput(cell, entityType, entityId, field, currentValue) { + cell.classList.add('editing'); + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentValue; + input.className = 'inline-edit-input form-control'; + input.dataset.entityType = entityType; + input.dataset.entityId = entityId; + input.dataset.field = field; + + // Remplacer le contenu + cell.innerHTML = ''; + cell.appendChild(input); + input.focus(); + input.select(); + + // Sauvegarder automatiquement après 2 secondes d'inactivité + let saveTimeout; + input.addEventListener('input', () => { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(() => { + this.saveField(entityType, entityId, field, input.value); + }, 2000); + }); + } + + async acquireLock(entityType, entityId) { + try { + const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/lock`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + const data = await response.json(); + + if (response.ok) { + this.locks.set(`${entityType}_${entityId}`, { + entityType, + entityId, + acquiredAt: new Date(), + expiresAt: new Date(data.expiresAt) + }); + return true; + } else { + console.error('Erreur lors de l\'acquisition du verrou:', data.error); + return false; + } + } catch (error) { + console.error('Erreur réseau:', error); + return false; + } + } + + async releaseLock(entityType, entityId) { + try { + await fetch(`/api/${entityType.toLowerCase()}/${entityId}/unlock`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + this.locks.delete(`${entityType}_${entityId}`); + } catch (error) { + console.error('Erreur lors de la libération du verrou:', error); + } + } + + async extendLock(entityType, entityId) { + try { + const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/extend-lock`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + const data = await response.json(); + const lock = this.locks.get(`${entityType}_${entityId}`); + if (lock) { + lock.expiresAt = new Date(data.expiresAt); + } + } + } catch (error) { + console.error('Erreur lors de la prolongation du verrou:', error); + } + } + + async saveField(entityType, entityId, field, value) { + try { + const response = await fetch(`/api/${entityType.toLowerCase()}/${entityId}/update-field`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + field: field, + value: value + }) + }); + + const data = await response.json(); + + if (response.ok) { + // Mettre à jour l'affichage + this.updateCellDisplay(entityType, entityId, field, value); + this.showSuccessMessage(`Champ ${field} mis à jour`); + } else { + this.showErrorMessage(data.error || 'Erreur lors de la sauvegarde'); + } + } catch (error) { + console.error('Erreur lors de la sauvegarde:', error); + this.showErrorMessage('Erreur réseau lors de la sauvegarde'); + } + } + + updateCellDisplay(entityType, entityId, field, value) { + const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); + if (cell) { + cell.textContent = value; + cell.classList.remove('editing'); + } + } + + isEditing(entityType, entityId, field) { + const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); + return cell && cell.classList.contains('editing'); + } + + saveCurrentEditing() { + const editingInput = document.querySelector('.inline-edit-input'); + if (editingInput) { + const entityType = editingInput.dataset.entityType; + const entityId = editingInput.dataset.entityId; + const field = editingInput.dataset.field; + const value = editingInput.value; + + this.saveField(entityType, entityId, field, value); + } + } + + cancelCurrentEditing() { + const editingInput = document.querySelector('.inline-edit-input'); + if (editingInput) { + const cell = editingInput.parentElement; + const originalValue = cell.dataset.originalValue || ''; + + cell.innerHTML = originalValue; + cell.classList.remove('editing'); + } + } + + saveAllPending() { + const editingInputs = document.querySelectorAll('.inline-edit-input'); + editingInputs.forEach(input => { + const entityType = input.dataset.entityType; + const entityId = input.dataset.entityId; + const field = input.dataset.field; + const value = input.value; + + this.saveField(entityType, entityId, field, value); + }); + } + + startLockCheck() { + // Vérifier les verrous toutes les 30 secondes + this.lockCheckInterval = setInterval(() => { + this.checkLocks(); + }, 30000); + } + + async checkLocks() { + for (const [key, lock] of this.locks) { + if (new Date() > lock.expiresAt) { + // Verrou expiré, le libérer + this.locks.delete(key); + } else { + // Prolonger le verrou + await this.extendLock(lock.entityType, lock.entityId); + } + } + } + + showLockMessage(cell) { + const message = document.createElement('div'); + message.className = 'alert alert-warning lock-message'; + message.textContent = 'Cet élément est en cours de modification par un autre utilisateur'; + + cell.appendChild(message); + + setTimeout(() => { + message.remove(); + }, 3000); + } + + showSuccessMessage(message) { + this.showMessage(message, 'success'); + } + + showErrorMessage(message) { + this.showMessage(message, 'danger'); + } + + showMessage(message, type) { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show position-fixed`; + alert.style.top = '20px'; + alert.style.right = '20px'; + alert.style.zIndex = '9999'; + alert.innerHTML = ` + ${message} + + `; + + document.body.appendChild(alert); + + setTimeout(() => { + alert.remove(); + }, 5000); + } + + destroy() { + if (this.lockCheckInterval) { + clearInterval(this.lockCheckInterval); + } + + // Libérer tous les verrous + for (const [key, lock] of this.locks) { + this.releaseLock(lock.entityType, lock.entityId); + } + } +} + +// Initialiser l'édition inline quand le DOM est prêt +document.addEventListener('DOMContentLoaded', () => { + window.inlineEditing = new InlineEditing(); +}); + +// Nettoyer à la fermeture de la page +window.addEventListener('beforeunload', () => { + if (window.inlineEditing) { + window.inlineEditing.destroy(); + } +}); + + diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..b9dacdc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,17 +4,26 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + app_user_provider: + entity: + class: App\Entity\Membre + property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall + form_login: + login_path: app_login + check_path: app_login + username_parameter: email + password_parameter: password + logout: + path: app_logout + target: app_home # https://symfony.com/doc/current/security/impersonating_user.html # switch_user: true @@ -22,8 +31,15 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # pages publiques + - { path: ^/login$, roles: PUBLIC_ACCESS } + - { path: ^/_profiler, roles: PUBLIC_ACCESS } + - { path: ^/_wdt, roles: PUBLIC_ACCESS } + # pages sécurisées + - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/projet, roles: ROLE_DEV } + # tout le reste nécessite d'être authentifié + - { path: ^/, roles: IS_AUTHENTICATED_FULLY } when@test: security: diff --git a/migrations/Version20251024091840.php b/migrations/Version20251024091840.php new file mode 100644 index 0000000..58edfed --- /dev/null +++ b/migrations/Version20251024091840.php @@ -0,0 +1,49 @@ +addSql('CREATE TABLE lock_entity (id INT AUTO_INCREMENT NOT NULL, entity_type VARCHAR(100) NOT NULL, entity_id INT NOT NULL, user_id VARCHAR(100) NOT NULL, session_id VARCHAR(100) NOT NULL, locked_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, user_agent VARCHAR(50) DEFAULT NULL, ip_address VARCHAR(45) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE contrib_ia CHANGE commentaire commentaire LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX assistant_ia_id TO IDX_2D4BB1127581ACBD'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX contribution_id TO IDX_2D4BB112FE5E5FBD'); + $this->addSql('ALTER TABLE contribution CHANGE commentaire commentaire LONGTEXT DEFAULT NULL, CHANGE duree duree INT DEFAULT 0 NOT NULL'); + $this->addSql('ALTER TABLE contribution RENAME INDEX membre_id TO IDX_EA351E156A99F74A'); + $this->addSql('ALTER TABLE contribution RENAME INDEX projet_id TO IDX_EA351E15C18272'); + $this->addSql('ALTER TABLE membre RENAME INDEX email TO UNIQ_F6B4FB29E7927C74'); + $this->addSql('ALTER TABLE projet CHANGE commentaire commentaire LONGTEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE lock_entity'); + $this->addSql('DROP TABLE messenger_messages'); + $this->addSql('ALTER TABLE contrib_ia CHANGE commentaire commentaire TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX idx_2d4bb1127581acbd TO assistant_ia_id'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX idx_2d4bb112fe5e5fbd TO contribution_id'); + $this->addSql('ALTER TABLE projet CHANGE commentaire commentaire TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE membre RENAME INDEX uniq_f6b4fb29e7927c74 TO email'); + $this->addSql('ALTER TABLE contribution CHANGE commentaire commentaire TEXT DEFAULT NULL, CHANGE duree duree INT DEFAULT 0'); + $this->addSql('ALTER TABLE contribution RENAME INDEX idx_ea351e156a99f74a TO membre_id'); + $this->addSql('ALTER TABLE contribution RENAME INDEX idx_ea351e15c18272 TO projet_id'); + } +} diff --git a/migrations/Version20251024131225.php b/migrations/Version20251024131225.php new file mode 100644 index 0000000..a86ae76 --- /dev/null +++ b/migrations/Version20251024131225.php @@ -0,0 +1,51 @@ +addSql('CREATE TABLE lock_entity (id INT AUTO_INCREMENT NOT NULL, entity_type VARCHAR(100) NOT NULL, entity_id INT NOT NULL, user_id VARCHAR(100) NOT NULL, session_id VARCHAR(100) NOT NULL, locked_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, user_agent VARCHAR(50) DEFAULT NULL, ip_address VARCHAR(45) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE contrib_ia CHANGE commentaire commentaire LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX assistant_ia_id TO IDX_2D4BB1127581ACBD'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX contribution_id TO IDX_2D4BB112FE5E5FBD'); + $this->addSql('ALTER TABLE contribution CHANGE commentaire commentaire LONGTEXT DEFAULT NULL, CHANGE duree duree INT DEFAULT 0 NOT NULL'); + $this->addSql('ALTER TABLE contribution RENAME INDEX membre_id TO IDX_EA351E156A99F74A'); + $this->addSql('ALTER TABLE contribution RENAME INDEX projet_id TO IDX_EA351E15C18272'); + $this->addSql('ALTER TABLE membre ADD roles JSON NOT NULL, ADD password VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE membre RENAME INDEX email TO UNIQ_F6B4FB29E7927C74'); + $this->addSql('ALTER TABLE projet CHANGE commentaire commentaire LONGTEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE lock_entity'); + $this->addSql('DROP TABLE messenger_messages'); + $this->addSql('ALTER TABLE projet CHANGE commentaire commentaire TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE membre DROP roles, DROP password'); + $this->addSql('ALTER TABLE membre RENAME INDEX uniq_f6b4fb29e7927c74 TO email'); + $this->addSql('ALTER TABLE contrib_ia CHANGE commentaire commentaire TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX idx_2d4bb1127581acbd TO assistant_ia_id'); + $this->addSql('ALTER TABLE contrib_ia RENAME INDEX idx_2d4bb112fe5e5fbd TO contribution_id'); + $this->addSql('ALTER TABLE contribution CHANGE commentaire commentaire TEXT DEFAULT NULL, CHANGE duree duree INT DEFAULT 0'); + $this->addSql('ALTER TABLE contribution RENAME INDEX idx_ea351e156a99f74a TO membre_id'); + $this->addSql('ALTER TABLE contribution RENAME INDEX idx_ea351e15c18272 TO projet_id'); + } +} diff --git a/public/js/inline-editing.js b/public/js/inline-editing.js new file mode 100644 index 0000000..0280439 --- /dev/null +++ b/public/js/inline-editing.js @@ -0,0 +1,362 @@ +/** + * Système d'édition inline avec verrouillage - Version corrigée + */ +class InlineEditing { + constructor() { + this.locks = new Map(); + this.lockCheckInterval = null; + this.init(); + console.log('InlineEditing initialisé'); + } + + init() { + this.setupEventListeners(); + this.startLockCheck(); + } + + setupEventListeners() { + console.log('Configuration des événements...'); + + // Détecter les clics sur les cellules éditables + document.addEventListener('click', (e) => { + console.log('Clic détecté sur:', e.target); + if (e.target.classList.contains('editable-cell')) { + console.log('Cellule éditables cliquée'); + this.startEditing(e.target); + } + }); + + // Détecter les clics en dehors pour sauvegarder + document.addEventListener('click', (e) => { + if (!e.target.closest('.editable-cell, .inline-edit-input')) { + this.saveAllPending(); + } + }); + + // Détecter les touches pour sauvegarder + document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + this.saveCurrentEditing(); + } else if (e.key === 'Escape') { + this.cancelCurrentEditing(); + } + }); + } + + startEditing(cell) { + console.log('Début de l\'édition pour:', cell); + + if (cell.classList.contains('editing')) { + console.log('Déjà en cours d\'édition'); + return; + } + + const entityType = cell.dataset.entityType; + const entityId = cell.dataset.entityId; + const field = cell.dataset.field; + const currentValue = cell.textContent.trim(); + + console.log('Données:', { entityType, entityId, field, currentValue }); + + // Vérifier si déjà en cours d'édition + if (this.isEditing(entityType, entityId, field)) { + console.log('Déjà en cours d\'édition pour ce champ'); + return; + } + + // Acquérir le verrou + this.acquireLock(entityType, entityId).then((success) => { + console.log('Résultat de l\'acquisition du verrou:', success); + if (success) { + this.createEditInput(cell, entityType, entityId, field, currentValue); + } else { + this.showLockMessage(cell); + } + }).catch(error => { + console.error('Erreur lors de l\'acquisition du verrou:', error); + this.showErrorMessage('Erreur lors de l\'acquisition du verrou'); + }); + } + + createEditInput(cell, entityType, entityId, field, currentValue) { + console.log('Création de l\'input pour:', { entityType, entityId, field, currentValue }); + + cell.classList.add('editing'); + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentValue; + input.className = 'inline-edit-input form-control'; + input.dataset.entityType = entityType; + input.dataset.entityId = entityId; + input.dataset.field = field; + + // Remplacer le contenu + cell.innerHTML = ''; + cell.appendChild(input); + input.focus(); + input.select(); + + console.log('Input créé et ajouté'); + + // Sauvegarder automatiquement après 3 secondes d'inactivité + let saveTimeout; + input.addEventListener('input', () => { + console.log('Changement détecté dans l\'input'); + clearTimeout(saveTimeout); + saveTimeout = setTimeout(() => { + console.log('Sauvegarde automatique déclenchée'); + this.saveField(entityType, entityId, field, input.value); + }, 3000); + }); + } + + async acquireLock(entityType, entityId) { + console.log('Tentative d\'acquisition du verrou pour:', entityType, entityId); + + try { + const url = `/api/${entityType.toLowerCase()}/${entityId}/lock`; + console.log('URL de verrouillage:', url); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + console.log('Réponse du serveur:', response.status); + const data = await response.json(); + console.log('Données reçues:', data); + + if (response.ok) { + this.locks.set(`${entityType}_${entityId}`, { + entityType, + entityId, + acquiredAt: new Date(), + expiresAt: new Date(data.expiresAt) + }); + console.log('Verrou acquis avec succès'); + return true; + } else { + console.error('Erreur lors de l\'acquisition du verrou:', data.error); + return false; + } + } catch (error) { + console.error('Erreur réseau lors de l\'acquisition du verrou:', error); + return false; + } + } + + async releaseLock(entityType, entityId) { + console.log('Libération du verrou pour:', entityType, entityId); + + try { + const url = `/api/${entityType.toLowerCase()}/${entityId}/unlock`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + console.log('Réponse de libération:', response.status); + this.locks.delete(`${entityType}_${entityId}`); + } catch (error) { + console.error('Erreur lors de la libération du verrou:', error); + } + } + + async extendLock(entityType, entityId) { + try { + const url = `/api/${entityType.toLowerCase()}/${entityId}/extend-lock`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.ok) { + const data = await response.json(); + const lock = this.locks.get(`${entityType}_${entityId}`); + if (lock) { + lock.expiresAt = new Date(data.expiresAt); + } + } + } catch (error) { + console.error('Erreur lors de la prolongation du verrou:', error); + } + } + + async saveField(entityType, entityId, field, value) { + console.log('Sauvegarde du champ:', { entityType, entityId, field, value }); + + try { + const url = `/api/${entityType.toLowerCase()}/${entityId}/update-field`; + console.log('URL de sauvegarde:', url); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + field: field, + value: value + }) + }); + + console.log('Réponse de sauvegarde:', response.status); + const data = await response.json(); + console.log('Données de sauvegarde:', data); + + if (response.ok) { + // Mettre à jour l'affichage + this.updateCellDisplay(entityType, entityId, field, value); + this.showSuccessMessage(`Champ ${field} mis à jour`); + console.log('Sauvegarde réussie'); + } else { + console.error('Erreur de sauvegarde:', data.error); + this.showErrorMessage(data.error || 'Erreur lors de la sauvegarde'); + } + } catch (error) { + console.error('Erreur réseau lors de la sauvegarde:', error); + this.showErrorMessage('Erreur réseau lors de la sauvegarde'); + } + } + + updateCellDisplay(entityType, entityId, field, value) { + console.log('Mise à jour de l\'affichage:', { entityType, entityId, field, value }); + + const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); + if (cell) { + cell.textContent = value; + cell.classList.remove('editing'); + console.log('Affichage mis à jour'); + } else { + console.error('Cellule non trouvée pour la mise à jour'); + } + } + + isEditing(entityType, entityId, field) { + const cell = document.querySelector(`[data-entity-type="${entityType}"][data-entity-id="${entityId}"][data-field="${field}"]`); + return cell && cell.classList.contains('editing'); + } + + saveCurrentEditing() { + const editingInput = document.querySelector('.inline-edit-input'); + if (editingInput) { + const entityType = editingInput.dataset.entityType; + const entityId = editingInput.dataset.entityId; + const field = editingInput.dataset.field; + const value = editingInput.value; + + this.saveField(entityType, entityId, field, value); + } + } + + cancelCurrentEditing() { + const editingInput = document.querySelector('.inline-edit-input'); + if (editingInput) { + const cell = editingInput.parentElement; + const originalValue = cell.dataset.originalValue || ''; + + cell.innerHTML = originalValue; + cell.classList.remove('editing'); + } + } + + saveAllPending() { + const editingInputs = document.querySelectorAll('.inline-edit-input'); + editingInputs.forEach(input => { + const entityType = input.dataset.entityType; + const entityId = input.dataset.entityId; + const field = input.dataset.field; + const value = input.value; + + this.saveField(entityType, entityId, field, value); + }); + } + + startLockCheck() { + // Vérifier les verrous toutes les 30 secondes + this.lockCheckInterval = setInterval(() => { + this.checkLocks(); + }, 30000); + } + + async checkLocks() { + for (const [key, lock] of this.locks) { + if (new Date() > lock.expiresAt) { + // Verrou expiré, le libérer + this.locks.delete(key); + } else { + // Prolonger le verrou + await this.extendLock(lock.entityType, lock.entityId); + } + } + } + + showLockMessage(cell) { + const message = document.createElement('div'); + message.className = 'alert alert-warning lock-message'; + message.textContent = 'Cet élément est en cours de modification par un autre utilisateur'; + + cell.appendChild(message); + + setTimeout(() => { + message.remove(); + }, 3000); + } + + showSuccessMessage(message) { + this.showMessage(message, 'success'); + } + + showErrorMessage(message) { + this.showMessage(message, 'danger'); + } + + showMessage(message, type) { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show position-fixed`; + alert.style.top = '20px'; + alert.style.right = '20px'; + alert.style.zIndex = '9999'; + alert.innerHTML = ` + ${message} + + `; + + document.body.appendChild(alert); + + setTimeout(() => { + alert.remove(); + }, 5000); + } + + destroy() { + if (this.lockCheckInterval) { + clearInterval(this.lockCheckInterval); + } + + // Libérer tous les verrous + for (const [key, lock] of this.locks) { + this.releaseLock(lock.entityType, lock.entityId); + } + } +} + +// Initialiser l'édition inline quand le DOM est prêt +document.addEventListener('DOMContentLoaded', () => { + console.log('DOM chargé, initialisation de l\'édition inline...'); + window.inlineEditing = new InlineEditing(); +}); + +// Nettoyer à la fermeture de la page +window.addEventListener('beforeunload', () => { + if (window.inlineEditing) { + window.inlineEditing.destroy(); + } +}); \ No newline at end of file diff --git a/public/js/test-inline.js b/public/js/test-inline.js new file mode 100644 index 0000000..fd7501f --- /dev/null +++ b/public/js/test-inline.js @@ -0,0 +1,92 @@ +/** + * Test simple d'édition inline + */ +console.log('Script de test chargé'); + +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM chargé, initialisation du test...'); + + // Détecter les clics sur les cellules éditables + document.addEventListener('click', function(e) { + if (e.target.classList.contains('editable-cell')) { + console.log('Cellule cliquée:', e.target); + startEditing(e.target); + } + }); + + function startEditing(cell) { + console.log('Début de l\'édition pour:', cell); + + if (cell.classList.contains('editing')) { + console.log('Déjà en cours d\'édition'); + return; + } + + const currentValue = cell.textContent.trim(); + console.log('Valeur actuelle:', currentValue); + + // Créer un input + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentValue; + input.className = 'inline-edit-input form-control'; + + // Remplacer le contenu + cell.innerHTML = ''; + cell.appendChild(input); + cell.classList.add('editing'); + + input.focus(); + input.select(); + + console.log('Input créé et ajouté'); + + // Sauvegarder sur Enter + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + saveEditing(cell, input.value); + } else if (e.key === 'Escape') { + cancelEditing(cell, currentValue); + } + }); + + // Sauvegarder automatiquement après 3 secondes + setTimeout(() => { + if (cell.classList.contains('editing')) { + saveEditing(cell, input.value); + } + }, 3000); + } + + function saveEditing(cell, newValue) { + console.log('Sauvegarde:', newValue); + cell.textContent = newValue; + cell.classList.remove('editing'); + + // Afficher un message de succès + showMessage('Valeur sauvegardée: ' + newValue, 'success'); + } + + function cancelEditing(cell, originalValue) { + console.log('Annulation'); + cell.textContent = originalValue; + cell.classList.remove('editing'); + } + + function showMessage(message, type) { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show`; + alert.innerHTML = ` + ${message} + + `; + + document.body.appendChild(alert); + + setTimeout(() => { + alert.remove(); + }, 3000); + } + + console.log('Test d\'édition inline initialisé'); +}); diff --git a/src/Command/CleanupLocksCommand.php b/src/Command/CleanupLocksCommand.php new file mode 100644 index 0000000..e182602 --- /dev/null +++ b/src/Command/CleanupLocksCommand.php @@ -0,0 +1,42 @@ +title('Nettoyage des verrous expirés'); + + $removedCount = $this->lockService->cleanupExpiredLocks(); + + if ($removedCount > 0) { + $io->success(sprintf('%d verrous expirés ont été supprimés.', $removedCount)); + } else { + $io->info('Aucun verrou expiré trouvé.'); + } + + return Command::SUCCESS; + } +} + + diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php new file mode 100644 index 0000000..32f49f2 --- /dev/null +++ b/src/Command/CreateUserCommand.php @@ -0,0 +1,59 @@ +setDescription('Create a Membre user') + ->addArgument('email', InputArgument::REQUIRED, 'Email') + ->addArgument('password', InputArgument::REQUIRED, 'Password') + ->addArgument('role', InputArgument::OPTIONAL, 'Role (ROLE_ADMIN or ROLE_DEV)', 'ROLE_DEV') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $email = $input->getArgument('email'); + $password = $input->getArgument('password'); + $role = $input->getArgument('role'); + + $user = new Membre(); + // split email into prenom/nom if possible + $parts = explode('@', $email); + $user->setEmail($email); + $user->setPrenom($parts[0]); + $user->setNom(''); + $user->setRoles([$role]); + + $hashed = $this->hasher->hashPassword($user, $password); + $user->setPassword($hashed); + + $this->em->persist($user); + $this->em->flush(); + + $io->success(sprintf('User %s created with role %s', $email, $role)); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/Api/AssistantIaApiController.php b/src/Controller/Api/AssistantIaApiController.php new file mode 100644 index 0000000..168eb0e --- /dev/null +++ b/src/Controller/Api/AssistantIaApiController.php @@ -0,0 +1,68 @@ +assistantIaRepository->find($id); + if (!$assistant) { + return new JsonResponse(['error' => 'Assistant IA non trouvé'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('AssistantIa', $id, $request)) { + return new JsonResponse(['error' => 'Vous devez posséder le verrou pour modifier'], 403); + } + + $data = json_decode($request->getContent(), true); + $field = $data['field'] ?? null; + $value = $data['value'] ?? null; + + if (!$field) { + return new JsonResponse(['error' => 'Champ invalide'], 400); + } + + $setter = 'set' . ucfirst($field); + if (method_exists($assistant, $setter)) { + $assistant->$setter($value); + } + + $errors = $this->validator->validate($assistant); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return new JsonResponse(['error' => 'Erreur de validation', 'details' => $errorMessages], 400); + } + + try { + $this->entityManager->flush(); + $this->lockService->extendLock('AssistantIa', $id, $request); + + return new JsonResponse(['message' => 'Champ mis à jour avec succès', 'field' => $field, 'value' => $value]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Erreur lors de la mise à jour'], 500); + } + } +} diff --git a/src/Controller/Api/ContribIaApiController.php b/src/Controller/Api/ContribIaApiController.php new file mode 100644 index 0000000..eb93b03 --- /dev/null +++ b/src/Controller/Api/ContribIaApiController.php @@ -0,0 +1,68 @@ +contribIaRepository->find($id); + if (!$contrib) { + return new JsonResponse(['error' => 'Contrib IA non trouvée'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('ContribIa', $id, $request)) { + return new JsonResponse(['error' => 'Vous devez posséder le verrou pour modifier'], 403); + } + + $data = json_decode($request->getContent(), true); + $field = $data['field'] ?? null; + $value = $data['value'] ?? null; + + if (!$field) { + return new JsonResponse(['error' => 'Champ invalide'], 400); + } + + $setter = 'set' . ucfirst($field); + if (method_exists($contrib, $setter)) { + $contrib->$setter($value); + } + + $errors = $this->validator->validate($contrib); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return new JsonResponse(['error' => 'Erreur de validation', 'details' => $errorMessages], 400); + } + + try { + $this->entityManager->flush(); + $this->lockService->extendLock('ContribIa', $id, $request); + + return new JsonResponse(['message' => 'Champ mis à jour avec succès', 'field' => $field, 'value' => $value]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Erreur lors de la mise à jour'], 500); + } + } +} diff --git a/src/Controller/Api/ContributionApiController.php b/src/Controller/Api/ContributionApiController.php new file mode 100644 index 0000000..5c9e6da --- /dev/null +++ b/src/Controller/Api/ContributionApiController.php @@ -0,0 +1,68 @@ +contributionRepository->find($id); + if (!$contrib) { + return new JsonResponse(['error' => 'Contribution non trouvée'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('Contribution', $id, $request)) { + return new JsonResponse(['error' => 'Vous devez posséder le verrou pour modifier'], 403); + } + + $data = json_decode($request->getContent(), true); + $field = $data['field'] ?? null; + $value = $data['value'] ?? null; + + if (!$field) { + return new JsonResponse(['error' => 'Champ invalide'], 400); + } + + $setter = 'set' . ucfirst($field); + if (method_exists($contrib, $setter)) { + $contrib->$setter($value); + } + + $errors = $this->validator->validate($contrib); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return new JsonResponse(['error' => 'Erreur de validation', 'details' => $errorMessages], 400); + } + + try { + $this->entityManager->flush(); + $this->lockService->extendLock('Contribution', $id, $request); + + return new JsonResponse(['message' => 'Champ mis à jour avec succès', 'field' => $field, 'value' => $value]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Erreur lors de la mise à jour'], 500); + } + } +} diff --git a/src/Controller/Api/MembreApiController.php b/src/Controller/Api/MembreApiController.php new file mode 100644 index 0000000..d19e6cd --- /dev/null +++ b/src/Controller/Api/MembreApiController.php @@ -0,0 +1,165 @@ +membreRepository->find($id); + if (!$membre) { + return new JsonResponse(['error' => 'Membre non trouvé'], 404); + } + + // Vérifier si déjà verrouillé + if ($this->lockService->isLocked('Membre', $id)) { + if (!$this->lockService->isLockedByCurrentUser('Membre', $id, $request)) { + return new JsonResponse([ + 'error' => 'Ce membre est en cours de modification par un autre utilisateur', + 'locked' => true + ], 409); + } + return new JsonResponse(['message' => 'Verrou déjà acquis', 'locked' => true]); + } + + $lock = $this->lockService->createLock('Membre', $id, $request); + if (!$lock) { + return new JsonResponse(['error' => 'Impossible de créer le verrou'], 500); + } + + return new JsonResponse([ + 'message' => 'Verrou acquis avec succès', + 'locked' => true, + 'expiresAt' => $lock->getExpiresAt()->format('Y-m-d H:i:s') + ]); + } + + #[Route('/{id}/unlock', name: 'api_membre_unlock', methods: ['POST'])] + public function unlock(int $id, Request $request): JsonResponse + { + $membre = $this->membreRepository->find($id); + if (!$membre) { + return new JsonResponse(['error' => 'Membre non trouvé'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('Membre', $id, $request)) { + return new JsonResponse(['error' => 'Vous ne possédez pas le verrou'], 403); + } + + $this->lockService->removeLock('Membre', $id, $request); + + return new JsonResponse(['message' => 'Verrou libéré avec succès', 'locked' => false]); + } + + #[Route('/{id}/extend-lock', name: 'api_membre_extend_lock', methods: ['POST'])] + public function extendLock(int $id, Request $request): JsonResponse + { + $membre = $this->membreRepository->find($id); + if (!$membre) { + return new JsonResponse(['error' => 'Membre non trouvé'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('Membre', $id, $request)) { + return new JsonResponse(['error' => 'Vous ne possédez pas le verrou'], 403); + } + + $extended = $this->lockService->extendLock('Membre', $id, $request); + if (!$extended) { + return new JsonResponse(['error' => 'Impossible de prolonger le verrou'], 500); + } + + return new JsonResponse(['message' => 'Verrou prolongé avec succès']); + } + + #[Route('/{id}/update-field', name: 'api_membre_update_field', methods: ['POST'])] + public function updateField(int $id, Request $request): JsonResponse + { + $membre = $this->membreRepository->find($id); + if (!$membre) { + return new JsonResponse(['error' => 'Membre non trouvé'], 404); + } + + // Vérifier le verrou + if (!$this->lockService->isLockedByCurrentUser('Membre', $id, $request)) { + return new JsonResponse(['error' => 'Vous devez posséder le verrou pour modifier'], 403); + } + + $data = json_decode($request->getContent(), true); + $field = $data['field'] ?? null; + $value = $data['value'] ?? null; + + if (!$field || !in_array($field, ['nom', 'prenom', 'email'])) { + return new JsonResponse(['error' => 'Champ invalide'], 400); + } + + // Mettre à jour le champ + $setter = 'set' . ucfirst($field); + if (method_exists($membre, $setter)) { + $membre->$setter($value); + } + + // Valider + $errors = $this->validator->validate($membre); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return new JsonResponse(['error' => 'Erreur de validation', 'details' => $errorMessages], 400); + } + + try { + $this->entityManager->flush(); + + // Prolonger le verrou + $this->lockService->extendLock('Membre', $id, $request); + + return new JsonResponse([ + 'message' => 'Champ mis à jour avec succès', + 'field' => $field, + 'value' => $value + ]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Erreur lors de la mise à jour'], 500); + } + } + + #[Route('/{id}/lock-status', name: 'api_membre_lock_status', methods: ['GET'])] + public function lockStatus(int $id, Request $request): JsonResponse + { + $membre = $this->membreRepository->find($id); + if (!$membre) { + return new JsonResponse(['error' => 'Membre non trouvé'], 404); + } + + $lockInfo = $this->lockService->getLockInfo('Membre', $id); + + return new JsonResponse([ + 'locked' => $lockInfo !== null, + 'lockInfo' => $lockInfo, + 'isLockedByCurrentUser' => $this->lockService->isLockedByCurrentUser('Membre', $id, $request) + ]); + } +} + + diff --git a/src/Controller/Api/ProjetApiController.php b/src/Controller/Api/ProjetApiController.php new file mode 100644 index 0000000..f5da7ac --- /dev/null +++ b/src/Controller/Api/ProjetApiController.php @@ -0,0 +1,68 @@ +projetRepository->find($id); + if (!$projet) { + return new JsonResponse(['error' => 'Projet non trouvé'], 404); + } + + if (!$this->lockService->isLockedByCurrentUser('Projet', $id, $request)) { + return new JsonResponse(['error' => 'Vous devez posséder le verrou pour modifier'], 403); + } + + $data = json_decode($request->getContent(), true); + $field = $data['field'] ?? null; + $value = $data['value'] ?? null; + + if (!$field) { + return new JsonResponse(['error' => 'Champ invalide'], 400); + } + + $setter = 'set' . ucfirst($field); + if (method_exists($projet, $setter)) { + $projet->$setter($value); + } + + $errors = $this->validator->validate($projet); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return new JsonResponse(['error' => 'Erreur de validation', 'details' => $errorMessages], 400); + } + + try { + $this->entityManager->flush(); + $this->lockService->extendLock('Projet', $id, $request); + + return new JsonResponse(['message' => 'Champ mis à jour avec succès', 'field' => $field, 'value' => $value]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Erreur lors de la mise à jour'], 500); + } + } +} diff --git a/src/Controller/AssistantIaController.php b/src/Controller/AssistantIaController.php new file mode 100644 index 0000000..7339335 --- /dev/null +++ b/src/Controller/AssistantIaController.php @@ -0,0 +1,85 @@ +render('assistant_ia/index.html.twig', [ + 'assistant_ias' => $assistantIaRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_assistant_ia_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $assistantIa = new AssistantIa(); + $form = $this->createForm(AssistantIaType::class, $assistantIa); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($assistantIa); + $entityManager->flush(); + + $this->addFlash('success', 'Assistant IA créé avec succès.'); + return $this->redirectToRoute('app_assistant_ia_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('assistant_ia/new.html.twig', [ + 'assistant_ia' => $assistantIa, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_assistant_ia_show', methods: ['GET'])] + public function show(AssistantIa $assistantIa): Response + { + return $this->render('assistant_ia/show.html.twig', [ + 'assistant_ia' => $assistantIa, + ]); + } + + #[Route('/{id}/edit', name: 'app_assistant_ia_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, AssistantIa $assistantIa, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(AssistantIaType::class, $assistantIa); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + $this->addFlash('success', 'Assistant IA modifié avec succès.'); + return $this->redirectToRoute('app_assistant_ia_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('assistant_ia/edit.html.twig', [ + 'assistant_ia' => $assistantIa, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_assistant_ia_delete', methods: ['POST'])] + public function delete(Request $request, AssistantIa $assistantIa, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$assistantIa->getId(), $request->request->get('_token'))) { + $entityManager->remove($assistantIa); + $entityManager->flush(); + + $this->addFlash('success', 'Assistant IA supprimé avec succès.'); + } + + return $this->redirectToRoute('app_assistant_ia_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/ContribIaController.php b/src/Controller/ContribIaController.php new file mode 100644 index 0000000..8c060e5 --- /dev/null +++ b/src/Controller/ContribIaController.php @@ -0,0 +1,85 @@ +render('contrib_ia/index.html.twig', [ + 'contrib_ias' => $contribIaRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_contrib_ia_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $contribIa = new ContribIa(); + $form = $this->createForm(ContribIaType::class, $contribIa); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($contribIa); + $entityManager->flush(); + + $this->addFlash('success', 'Contribution IA créée avec succès.'); + return $this->redirectToRoute('app_contrib_ia_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('contrib_ia/new.html.twig', [ + 'contrib_ia' => $contribIa, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_contrib_ia_show', methods: ['GET'])] + public function show(ContribIa $contribIa): Response + { + return $this->render('contrib_ia/show.html.twig', [ + 'contrib_ia' => $contribIa, + ]); + } + + #[Route('/{id}/edit', name: 'app_contrib_ia_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, ContribIa $contribIa, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(ContribIaType::class, $contribIa); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + $this->addFlash('success', 'Contribution IA modifiée avec succès.'); + return $this->redirectToRoute('app_contrib_ia_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('contrib_ia/edit.html.twig', [ + 'contrib_ia' => $contribIa, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_contrib_ia_delete', methods: ['POST'])] + public function delete(Request $request, ContribIa $contribIa, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$contribIa->getId(), $request->request->get('_token'))) { + $entityManager->remove($contribIa); + $entityManager->flush(); + + $this->addFlash('success', 'Contribution IA supprimée avec succès.'); + } + + return $this->redirectToRoute('app_contrib_ia_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/ContributionController.php b/src/Controller/ContributionController.php new file mode 100644 index 0000000..c8089b4 --- /dev/null +++ b/src/Controller/ContributionController.php @@ -0,0 +1,85 @@ +render('contribution/index.html.twig', [ + 'contributions' => $contributionRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_contribution_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $contribution = new Contribution(); + $form = $this->createForm(ContributionType::class, $contribution); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($contribution); + $entityManager->flush(); + + $this->addFlash('success', 'Contribution créée avec succès.'); + return $this->redirectToRoute('app_contribution_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('contribution/new.html.twig', [ + 'contribution' => $contribution, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_contribution_show', methods: ['GET'])] + public function show(Contribution $contribution): Response + { + return $this->render('contribution/show.html.twig', [ + 'contribution' => $contribution, + ]); + } + + #[Route('/{id}/edit', name: 'app_contribution_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Contribution $contribution, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(ContributionType::class, $contribution); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + $this->addFlash('success', 'Contribution modifiée avec succès.'); + return $this->redirectToRoute('app_contribution_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('contribution/edit.html.twig', [ + 'contribution' => $contribution, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_contribution_delete', methods: ['POST'])] + public function delete(Request $request, Contribution $contribution, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$contribution->getId(), $request->request->get('_token'))) { + $entityManager->remove($contribution); + $entityManager->flush(); + + $this->addFlash('success', 'Contribution supprimée avec succès.'); + } + + return $this->redirectToRoute('app_contribution_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php new file mode 100644 index 0000000..4c3b40d --- /dev/null +++ b/src/Controller/HomeController.php @@ -0,0 +1,16 @@ +render('home/index.html.twig'); + } +} diff --git a/src/Controller/LockController.php b/src/Controller/LockController.php new file mode 100644 index 0000000..6c9e8fb --- /dev/null +++ b/src/Controller/LockController.php @@ -0,0 +1,55 @@ +lockService->cleanupExpiredLocks(); + + return new JsonResponse([ + 'message' => "Nettoyage terminé", + 'removedLocks' => $removedCount + ]); + } + + #[Route('/user-locks', name: 'app_lock_user_locks', methods: ['GET'])] + public function getUserLocks(Request $request): JsonResponse + { + // Cette méthode pourrait être utilisée pour afficher les verrous de l'utilisateur + return new JsonResponse([ + 'message' => 'Fonctionnalité à implémenter' + ]); + } + + #[Route('/release-all', name: 'app_lock_release_all', methods: ['POST'])] + public function releaseAll(Request $request): JsonResponse + { + $removedCount = $this->lockService->removeUserLocks($request); + + return new JsonResponse([ + 'message' => "Tous vos verrous ont été libérés", + 'removedLocks' => $removedCount + ]); + } + + #[Route('/stats', name: 'app_lock_stats', methods: ['GET'])] + public function stats(): Response + { + return $this->render('lock/stats.html.twig'); + } +} diff --git a/src/Controller/MembreController.php b/src/Controller/MembreController.php new file mode 100644 index 0000000..f27d944 --- /dev/null +++ b/src/Controller/MembreController.php @@ -0,0 +1,122 @@ +render('membre/index.html.twig', [ + 'membres' => $membreRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_membre_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $membre = new Membre(); + $form = $this->createForm(MembreType::class, $membre); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($membre); + $entityManager->flush(); + + return $this->redirectToRoute('app_membre_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('membre/new.html.twig', [ + 'membre' => $membre, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_membre_show', methods: ['GET'])] + public function show(Membre $membre): Response + { + return $this->render('membre/show.html.twig', [ + 'membre' => $membre, + ]); + } + + #[Route('/{id}/edit', name: 'app_membre_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Membre $membre, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(MembreType::class, $membre); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_membre_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('membre/edit.html.twig', [ + 'membre' => $membre, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_membre_delete', methods: ['POST'])] + public function delete(Request $request, Membre $membre, EntityManagerInterface $entityManager): Response + { + // Use the POST parameters bag to retrieve the CSRF token (consistent with other controllers) + if ($this->isCsrfTokenValid('delete'.$membre->getId(), $request->request->get('_token'))) { + $entityManager->remove($membre); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_membre_index', [], Response::HTTP_SEE_OTHER); + } + + #[Route('/update-field', name: 'app_membre_update_field', methods: ['POST'])] + public function updateField(Request $request, EntityManagerInterface $entityManager): JsonResponse + { + $data = json_decode($request->getContent(), true); + + if (!isset($data['id'], $data['field'], $data['value'])) { + return new JsonResponse(['success' => false, 'message' => 'Données invalides'], 400); + } + + $membre = $entityManager->getRepository(Membre::class)->find($data['id']); + if (!$membre) { + return new JsonResponse(['success' => false, 'message' => 'Membre non trouvé'], 404); + } + + try { + switch ($data['field']) { + case 'nom': + $membre->setNom($data['value']); + break; + case 'prenom': + $membre->setPrenom($data['value']); + break; + case 'email': + if (!filter_var($data['value'], FILTER_VALIDATE_EMAIL)) { + return new JsonResponse(['success' => false, 'message' => 'Email invalide'], 400); + } + $membre->setEmail($data['value']); + break; + default: + return new JsonResponse(['success' => false, 'message' => 'Champ non modifiable'], 400); + } + + $entityManager->flush(); + return new JsonResponse(['success' => true]); + } catch (\Exception $e) { + return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 500); + } + } +} \ No newline at end of file diff --git a/src/Controller/ProjetController.php b/src/Controller/ProjetController.php new file mode 100644 index 0000000..2c3b61d --- /dev/null +++ b/src/Controller/ProjetController.php @@ -0,0 +1,85 @@ +render('projet/index.html.twig', [ + 'projets' => $projetRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_projet_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $projet = new Projet(); + $form = $this->createForm(ProjetType::class, $projet); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($projet); + $entityManager->flush(); + + $this->addFlash('success', 'Projet créé avec succès.'); + return $this->redirectToRoute('app_projet_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('projet/new.html.twig', [ + 'projet' => $projet, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_projet_show', methods: ['GET'])] + public function show(Projet $projet): Response + { + return $this->render('projet/show.html.twig', [ + 'projet' => $projet, + ]); + } + + #[Route('/{id}/edit', name: 'app_projet_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Projet $projet, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(ProjetType::class, $projet); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + $this->addFlash('success', 'Projet modifié avec succès.'); + return $this->redirectToRoute('app_projet_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('projet/edit.html.twig', [ + 'projet' => $projet, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_projet_delete', methods: ['POST'])] + public function delete(Request $request, Projet $projet, EntityManagerInterface $entityManager): Response + { + if ($this->isCsrfTokenValid('delete'.$projet->getId(), $request->request->get('_token'))) { + $entityManager->remove($projet); + $entityManager->flush(); + + $this->addFlash('success', 'Projet supprimé avec succès.'); + } + + return $this->redirectToRoute('app_projet_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..3d49bd0 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,30 @@ +getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void + { + // This method can be empty - it will be intercepted by the logout key on your firewall + } +} diff --git a/src/Controller/TestController.php b/src/Controller/TestController.php new file mode 100644 index 0000000..17f7bc0 --- /dev/null +++ b/src/Controller/TestController.php @@ -0,0 +1,34 @@ + 'Test réussi', 'timestamp' => date('Y-m-d H:i:s')]); + } + + #[Route('/api/test', name: 'api_test', methods: ['GET', 'POST'])] + public function apiTest(Request $request): JsonResponse + { + return new JsonResponse([ + 'message' => 'API Test réussi', + 'method' => $request->getMethod(), + 'timestamp' => date('Y-m-d H:i:s') + ]); + } + + #[Route('/test-page', name: 'app_test_page', methods: ['GET'])] + public function testPage(): Response + { + return $this->render('test.html.twig'); + } +} diff --git a/src/Controller/app_membre_update_field.php b/src/Controller/app_membre_update_field.php new file mode 100644 index 0000000..461f05e --- /dev/null +++ b/src/Controller/app_membre_update_field.php @@ -0,0 +1,7 @@ +lockedAt = new \DateTime(); + $this->expiresAt = new \DateTime('+30 minutes'); // Verrou expire après 30 minutes + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEntityType(): ?string + { + return $this->entityType; + } + + public function setEntityType(string $entityType): static + { + $this->entityType = $entityType; + + return $this; + } + + public function getEntityId(): ?int + { + return $this->entityId; + } + + public function setEntityId(int $entityId): static + { + $this->entityId = $entityId; + + return $this; + } + + public function getUserId(): ?string + { + return $this->userId; + } + + public function setUserId(string $userId): static + { + $this->userId = $userId; + + return $this; + } + + public function getSessionId(): ?string + { + return $this->sessionId; + } + + public function setSessionId(string $sessionId): static + { + $this->sessionId = $sessionId; + + return $this; + } + + public function getLockedAt(): ?\DateTimeInterface + { + return $this->lockedAt; + } + + public function setLockedAt(\DateTimeInterface $lockedAt): static + { + $this->lockedAt = $lockedAt; + + return $this; + } + + public function getExpiresAt(): ?\DateTimeInterface + { + return $this->expiresAt; + } + + public function setExpiresAt(\DateTimeInterface $expiresAt): static + { + $this->expiresAt = $expiresAt; + + return $this; + } + + public function getUserAgent(): ?string + { + return $this->userAgent; + } + + public function setUserAgent(?string $userAgent): static + { + $this->userAgent = $userAgent; + + return $this; + } + + public function getIpAddress(): ?string + { + return $this->ipAddress; + } + + public function setIpAddress(?string $ipAddress): static + { + $this->ipAddress = $ipAddress; + + return $this; + } + + /** + * Vérifie si le verrou est encore valide + */ + public function isValid(): bool + { + return $this->expiresAt > new \DateTime(); + } + + /** + * Vérifie si le verrou appartient à l'utilisateur + */ + public function belongsToUser(string $userId, string $sessionId): bool + { + return $this->userId === $userId && $this->sessionId === $sessionId; + } + + /** + * Prolonge le verrou + */ + public function extend(): static + { + $this->expiresAt = new \DateTime('+30 minutes'); + return $this; + } + + /** + * Génère une clé unique pour l'entité + */ + public function getEntityKey(): string + { + return $this->entityType . '_' . $this->entityId; + } + + public function __toString(): string + { + return sprintf( + 'Verrou: %s #%d par %s', + $this->entityType, + $this->entityId, + $this->userId + ); + } +} + + diff --git a/src/Entity/Membre.php b/src/Entity/Membre.php index a8d20c1..65aff5b 100644 --- a/src/Entity/Membre.php +++ b/src/Entity/Membre.php @@ -8,11 +8,13 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; #[ORM\Entity(repositoryClass: MembreRepository::class)] #[ORM\Table(name: 'membre')] #[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')] -class Membre +class Membre implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] @@ -35,6 +37,12 @@ class Membre #[Assert\Length(max: 100, maxMessage: 'L\'email ne peut pas dépasser {{ limit }} caractères.')] private ?string $email = null; + #[ORM\Column(type: 'json')] + private array $roles = []; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $password = null; + #[ORM\OneToMany(targetEntity: Contribution::class, mappedBy: 'membre', cascade: ['persist'], orphanRemoval: true)] private Collection $contributions; @@ -84,6 +92,53 @@ class Membre return $this; } + /** + * A visual identifier that represents this user. + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @return array + */ + public function getRoles(): array + { + $roles = $this->roles; + + // guarantee every user at least has ROLE_DEV + if (empty($roles)) { + $roles[] = 'ROLE_DEV'; + } + + return array_unique($roles); + } + + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(?string $password): static + { + $this->password = $password; + + return $this; + } + + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + } + /** * @return Collection */ diff --git a/src/Form/AssistantIaType.php b/src/Form/AssistantIaType.php new file mode 100644 index 0000000..78b8705 --- /dev/null +++ b/src/Form/AssistantIaType.php @@ -0,0 +1,32 @@ +add('nom', TextType::class, [ + 'label' => 'Nom de l\'assistant IA', + 'attr' => [ + 'class' => 'form-control', + 'placeholder' => 'Entrez le nom de l\'assistant IA' + ] + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => AssistantIa::class, + ]); + } +} diff --git a/src/Form/ContribIaType.php b/src/Form/ContribIaType.php new file mode 100644 index 0000000..28e9b72 --- /dev/null +++ b/src/Form/ContribIaType.php @@ -0,0 +1,78 @@ +add('assistantIa', EntityType::class, [ + 'class' => AssistantIa::class, + 'choice_label' => 'nom', + 'label' => 'Assistant IA', + 'attr' => ['class' => 'form-control'] + ]) + ->add('contribution', EntityType::class, [ + 'class' => Contribution::class, + 'choice_label' => function(Contribution $contribution) { + return $contribution->getMembre() . ' - ' . $contribution->getProjet() . ' (' . $contribution->getDateContribution()->format('d/m/Y') . ')'; + }, + 'label' => 'Contribution', + 'attr' => ['class' => 'form-control'] + ]) + ->add('evaluationPertinence', ChoiceType::class, [ + 'choices' => [ + 'Très faible' => 1, + 'Faible' => 2, + 'Moyen' => 3, + 'Bon' => 4, + 'Excellent' => 5, + ], + 'label' => 'Évaluation de pertinence', + 'required' => false, + 'placeholder' => 'Sélectionnez une évaluation', + 'attr' => ['class' => 'form-control'] + ]) + ->add('evaluationTemps', ChoiceType::class, [ + 'choices' => [ + 'Très lent' => 1, + 'Lent' => 2, + 'Moyen' => 3, + 'Rapide' => 4, + 'Très rapide' => 5, + ], + 'label' => 'Évaluation de temps', + 'required' => false, + 'placeholder' => 'Sélectionnez une évaluation', + 'attr' => ['class' => 'form-control'] + ]) + ->add('commentaire', TextareaType::class, [ + 'label' => 'Commentaire', + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + 'rows' => 3, + 'placeholder' => 'Ajoutez un commentaire...' + ] + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ContribIa::class, + ]); + } +} diff --git a/src/Form/ContributionType.php b/src/Form/ContributionType.php new file mode 100644 index 0000000..d95cba6 --- /dev/null +++ b/src/Form/ContributionType.php @@ -0,0 +1,66 @@ +add('membre', EntityType::class, [ + 'class' => Membre::class, + 'choice_label' => function(Membre $membre) { + return $membre->getPrenom() . ' ' . $membre->getNom(); + }, + 'label' => 'Membre', + 'attr' => ['class' => 'form-control'] + ]) + ->add('projet', EntityType::class, [ + 'class' => Projet::class, + 'choice_label' => 'nom', + 'label' => 'Projet', + 'attr' => ['class' => 'form-control'] + ]) + ->add('dateContribution', DateType::class, [ + 'widget' => 'single_text', + 'label' => 'Date de contribution', + 'attr' => ['class' => 'form-control'] + ]) + ->add('duree', IntegerType::class, [ + 'label' => 'Durée (en minutes)', + 'attr' => [ + 'class' => 'form-control', + 'min' => 0, + 'placeholder' => 'Durée en minutes' + ] + ]) + ->add('commentaire', TextareaType::class, [ + 'label' => 'Commentaire', + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + 'rows' => 3, + 'placeholder' => 'Ajoutez un commentaire...' + ] + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Contribution::class, + ]); + } +} diff --git a/src/Form/MembreType.php b/src/Form/MembreType.php new file mode 100644 index 0000000..36bc84c --- /dev/null +++ b/src/Form/MembreType.php @@ -0,0 +1,27 @@ +add('nom') + ->add('prenom') + ->add('email') + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Membre::class, + ]); + } +} diff --git a/src/Form/ProjetType.php b/src/Form/ProjetType.php new file mode 100644 index 0000000..25b4d65 --- /dev/null +++ b/src/Form/ProjetType.php @@ -0,0 +1,61 @@ +add('nom', TextType::class, [ + 'label' => 'Nom du projet', + 'attr' => [ + 'class' => 'form-control', + 'placeholder' => 'Entrez le nom du projet' + ] + ]) + ->add('commentaire', TextareaType::class, [ + 'label' => 'Commentaire', + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + 'rows' => 3, + 'placeholder' => 'Ajoutez un commentaire...' + ] + ]) + ->add('dateLancement', DateType::class, [ + 'widget' => 'single_text', + 'label' => 'Date de lancement', + 'required' => false, + 'attr' => ['class' => 'form-control'] + ]) + ->add('dateCloture', DateType::class, [ + 'widget' => 'single_text', + 'label' => 'Date de clôture', + 'required' => false, + 'attr' => ['class' => 'form-control'] + ]) + ->add('statut', ChoiceType::class, [ + 'choices' => Projet::getStatutChoices(), + 'label' => 'Statut', + 'attr' => ['class' => 'form-control'] + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Projet::class, + ]); + } +} diff --git a/src/Repository/AssistantIaRepository.php b/src/Repository/AssistantIaRepository.php new file mode 100644 index 0000000..14fb981 --- /dev/null +++ b/src/Repository/AssistantIaRepository.php @@ -0,0 +1,110 @@ + + * + * @method AssistantIa|null find($id, $lockMode = null, $lockVersion = null) + * @method AssistantIa|null findOneBy(array $criteria, array $orderBy = null) + * @method AssistantIa[] findAll() + * @method AssistantIa[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class AssistantIaRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AssistantIa::class); + } + + public function save(AssistantIa $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AssistantIa $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve les assistants IA triés par moyenne de pertinence + */ + public function findByMoyennePertinence(): array + { + return $this->createQueryBuilder('a') + ->leftJoin('a.contribIas', 'c') + ->addSelect('AVG(c.evaluationPertinence) as moyenne_pertinence') + ->groupBy('a.id') + ->orderBy('moyenne_pertinence', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les assistants IA triés par moyenne de temps + */ + public function findByMoyenneTemps(): array + { + return $this->createQueryBuilder('a') + ->leftJoin('a.contribIas', 'c') + ->addSelect('AVG(c.evaluationTemps) as moyenne_temps') + ->groupBy('a.id') + ->orderBy('moyenne_temps', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les assistants IA avec le plus de contributions + */ + public function findByNombreContributions(): array + { + return $this->createQueryBuilder('a') + ->leftJoin('a.contribIas', 'c') + ->addSelect('COUNT(c.id) as nombre_contributions') + ->groupBy('a.id') + ->orderBy('nombre_contributions', 'DESC') + ->getQuery() + ->getResult(); + } + +// /** +// * @return AssistantIa[] Returns an array of AssistantIa objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('a.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?AssistantIa +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Repository/ContribIaRepository.php b/src/Repository/ContribIaRepository.php new file mode 100644 index 0000000..142da8e --- /dev/null +++ b/src/Repository/ContribIaRepository.php @@ -0,0 +1,138 @@ + + * + * @method ContribIa|null find($id, $lockMode = null, $lockVersion = null) + * @method ContribIa|null findOneBy(array $criteria, array $orderBy = null) + * @method ContribIa[] findAll() + * @method ContribIa[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ContribIaRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ContribIa::class); + } + + public function save(ContribIa $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(ContribIa $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve les contributions IA triées par moyenne d'évaluation + */ + public function findByMoyenneEvaluation(): array + { + return $this->createQueryBuilder('c') + ->addSelect('(c.evaluationPertinence + c.evaluationTemps) / 2 as moyenne') + ->where('c.evaluationPertinence IS NOT NULL') + ->andWhere('c.evaluationTemps IS NOT NULL') + ->orderBy('moyenne', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions IA par assistant IA + */ + public function findByAssistantIa(int $assistantIaId): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.assistantIa = :assistantIaId') + ->setParameter('assistantIaId', $assistantIaId) + ->orderBy('c.id', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions IA par contribution + */ + public function findByContribution(int $contributionId): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.contribution = :contributionId') + ->setParameter('contributionId', $contributionId) + ->orderBy('c.id', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions IA avec évaluation de pertinence + */ + public function findByEvaluationPertinence(int $minValue = 1, int $maxValue = 5): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.evaluationPertinence >= :min') + ->andWhere('c.evaluationPertinence <= :max') + ->setParameter('min', $minValue) + ->setParameter('max', $maxValue) + ->orderBy('c.evaluationPertinence', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions IA avec évaluation de temps + */ + public function findByEvaluationTemps(int $minValue = 1, int $maxValue = 5): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.evaluationTemps >= :min') + ->andWhere('c.evaluationTemps <= :max') + ->setParameter('min', $minValue) + ->setParameter('max', $maxValue) + ->orderBy('c.evaluationTemps', 'DESC') + ->getQuery() + ->getResult(); + } + +// /** +// * @return ContribIa[] Returns an array of ContribIa objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('c.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?ContribIa +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Repository/ContributionRepository.php b/src/Repository/ContributionRepository.php new file mode 100644 index 0000000..860bab1 --- /dev/null +++ b/src/Repository/ContributionRepository.php @@ -0,0 +1,164 @@ + + * + * @method Contribution|null find($id, $lockMode = null, $lockVersion = null) + * @method Contribution|null findOneBy(array $criteria, array $orderBy = null) + * @method Contribution[] findAll() + * @method Contribution[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ContributionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Contribution::class); + } + + public function save(Contribution $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Contribution $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve les contributions par membre + */ + public function findByMembre(int $membreId): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.membre = :membreId') + ->setParameter('membreId', $membreId) + ->orderBy('c.dateContribution', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions par projet + */ + public function findByProjet(int $projetId): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.projet = :projetId') + ->setParameter('projetId', $projetId) + ->orderBy('c.dateContribution', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions par période + */ + public function findByPeriode(\DateTimeInterface $dateDebut, \DateTimeInterface $dateFin): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.dateContribution >= :dateDebut') + ->andWhere('c.dateContribution <= :dateFin') + ->setParameter('dateDebut', $dateDebut) + ->setParameter('dateFin', $dateFin) + ->orderBy('c.dateContribution', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions avec une durée minimale + */ + public function findByDureeMinimale(int $dureeMinimale): array + { + return $this->createQueryBuilder('c') + ->andWhere('c.duree >= :dureeMinimale') + ->setParameter('dureeMinimale', $dureeMinimale) + ->orderBy('c.duree', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les contributions récentes + */ + public function findRecent(int $limit = 10): array + { + return $this->createQueryBuilder('c') + ->orderBy('c.dateContribution', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * Calcule la durée totale des contributions par membre + */ + public function getDureeTotaleParMembre(int $membreId): int + { + $result = $this->createQueryBuilder('c') + ->select('SUM(c.duree)') + ->andWhere('c.membre = :membreId') + ->setParameter('membreId', $membreId) + ->getQuery() + ->getSingleScalarResult(); + + return $result ?? 0; + } + + /** + * Calcule la durée totale des contributions par projet + */ + public function getDureeTotaleParProjet(int $projetId): int + { + $result = $this->createQueryBuilder('c') + ->select('SUM(c.duree)') + ->andWhere('c.projet = :projetId') + ->setParameter('projetId', $projetId) + ->getQuery() + ->getSingleScalarResult(); + + return $result ?? 0; + } + +// /** +// * @return Contribution[] Returns an array of Contribution objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('c.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Contribution +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Repository/LockRepository.php b/src/Repository/LockRepository.php new file mode 100644 index 0000000..0082377 --- /dev/null +++ b/src/Repository/LockRepository.php @@ -0,0 +1,218 @@ + + * + * @method Lock|null find($id, $lockMode = null, $lockVersion = null) + * @method Lock|null findOneBy(array $criteria, array $orderBy = null) + * @method Lock[] findAll() + * @method Lock[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class LockRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Lock::class); + } + + public function save(Lock $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Lock $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve un verrou pour une entité spécifique + */ + public function findByEntity(string $entityType, int $entityId): ?Lock + { + return $this->createQueryBuilder('l') + ->andWhere('l.entityType = :entityType') + ->andWhere('l.entityId = :entityId') + ->andWhere('l.expiresAt > :now') + ->setParameter('entityType', $entityType) + ->setParameter('entityId', $entityId) + ->setParameter('now', new \DateTime()) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * Trouve tous les verrous d'un utilisateur + */ + public function findByUser(string $userId, string $sessionId): array + { + return $this->createQueryBuilder('l') + ->andWhere('l.userId = :userId') + ->andWhere('l.sessionId = :sessionId') + ->andWhere('l.expiresAt > :now') + ->setParameter('userId', $userId) + ->setParameter('sessionId', $sessionId) + ->setParameter('now', new \DateTime()) + ->orderBy('l.lockedAt', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve tous les verrous expirés + */ + public function findExpired(): array + { + return $this->createQueryBuilder('l') + ->andWhere('l.expiresAt <= :now') + ->setParameter('now', new \DateTime()) + ->getQuery() + ->getResult(); + } + + /** + * Supprime tous les verrous expirés + */ + public function removeExpired(): int + { + $expiredLocks = $this->findExpired(); + $count = count($expiredLocks); + + foreach ($expiredLocks as $lock) { + $this->getEntityManager()->remove($lock); + } + + if ($count > 0) { + $this->getEntityManager()->flush(); + } + + return $count; + } + + /** + * Supprime un verrou spécifique + */ + public function removeByEntity(string $entityType, int $entityId): bool + { + $lock = $this->findByEntity($entityType, $entityId); + if ($lock) { + $this->getEntityManager()->remove($lock); + $this->getEntityManager()->flush(); + return true; + } + return false; + } + + /** + * Supprime tous les verrous d'un utilisateur + */ + public function removeByUser(string $userId, string $sessionId): int + { + $userLocks = $this->findByUser($userId, $sessionId); + $count = count($userLocks); + + foreach ($userLocks as $lock) { + $this->getEntityManager()->remove($lock); + } + + if ($count > 0) { + $this->getEntityManager()->flush(); + } + + return $count; + } + + /** + * Vérifie si une entité est verrouillée + */ + public function isLocked(string $entityType, int $entityId): bool + { + return $this->findByEntity($entityType, $entityId) !== null; + } + + /** + * Vérifie si une entité est verrouillée par un utilisateur spécifique + */ + public function isLockedByUser(string $entityType, int $entityId, string $userId, string $sessionId): bool + { + $lock = $this->findByEntity($entityType, $entityId); + return $lock && $lock->belongsToUser($userId, $sessionId); + } + + /** + * Crée un nouveau verrou + */ + public function createLock(string $entityType, int $entityId, string $userId, string $sessionId, ?string $userAgent = null, ?string $ipAddress = null): ?Lock + { + // Vérifier si l'entité est déjà verrouillée + if ($this->isLocked($entityType, $entityId)) { + return null; + } + + $lock = new Lock(); + $lock->setEntityType($entityType) + ->setEntityId($entityId) + ->setUserId($userId) + ->setSessionId($sessionId) + ->setUserAgent($userAgent) + ->setIpAddress($ipAddress); + + $this->save($lock, true); + return $lock; + } + + /** + * Prolonge un verrou existant + */ + public function extendLock(string $entityType, int $entityId, string $userId, string $sessionId): bool + { + $lock = $this->findByEntity($entityType, $entityId); + if ($lock && $lock->belongsToUser($userId, $sessionId)) { + $lock->extend(); + $this->save($lock, true); + return true; + } + return false; + } + +// /** +// * @return Lock[] Returns an array of Lock objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('l') +// ->andWhere('l.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('l.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Lock +// { +// return $this->createQueryBuilder('l') +// ->andWhere('l.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Repository/MembreRepository.php b/src/Repository/MembreRepository.php new file mode 100644 index 0000000..116837a --- /dev/null +++ b/src/Repository/MembreRepository.php @@ -0,0 +1,185 @@ + + * + * @method Membre|null find($id, $lockMode = null, $lockVersion = null) + * @method Membre|null findOneBy(array $criteria, array $orderBy = null) + * @method Membre[] findAll() + * @method Membre[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class MembreRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Membre::class); + } + + public function save(Membre $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Membre $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve les membres par nom + */ + public function findByNom(string $nom): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.nom LIKE :nom') + ->setParameter('nom', '%' . $nom . '%') + ->orderBy('m.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les membres par prénom + */ + public function findByPrenom(string $prenom): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.prenom LIKE :prenom') + ->setParameter('prenom', '%' . $prenom . '%') + ->orderBy('m.prenom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les membres par email + */ + public function findByEmail(string $email): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.email LIKE :email') + ->setParameter('email', '%' . $email . '%') + ->orderBy('m.email', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les membres avec le plus de contributions + */ + public function findByNombreContributions(): array + { + return $this->createQueryBuilder('m') + ->leftJoin('m.contributions', 'c') + ->addSelect('COUNT(c.id) as nombre_contributions') + ->groupBy('m.id') + ->orderBy('nombre_contributions', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les membres actifs (avec au moins une contribution) + */ + public function findActifs(): array + { + return $this->createQueryBuilder('m') + ->leftJoin('m.contributions', 'c') + ->andWhere('c.id IS NOT NULL') + ->groupBy('m.id') + ->orderBy('m.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les membres inactifs (sans contribution) + */ + public function findInactifs(): array + { + return $this->createQueryBuilder('m') + ->leftJoin('m.contributions', 'c') + ->andWhere('c.id IS NULL') + ->orderBy('m.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Recherche globale par nom, prénom ou email + */ + public function search(string $searchTerm): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.nom LIKE :search OR m.prenom LIKE :search OR m.email LIKE :search') + ->setParameter('search', '%' . $searchTerm . '%') + ->orderBy('m.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve un membre par email exact + */ + public function findOneByEmail(string $email): ?Membre + { + return $this->createQueryBuilder('m') + ->andWhere('m.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * Trouve les membres récents (créés récemment) + */ + public function findRecents(int $limit = 10): array + { + return $this->createQueryBuilder('m') + ->orderBy('m.id', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + +// /** +// * @return Membre[] Returns an array of Membre objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('m') +// ->andWhere('m.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('m.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Membre +// { +// return $this->createQueryBuilder('m') +// ->andWhere('m.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Repository/ProjetRepository.php b/src/Repository/ProjetRepository.php new file mode 100644 index 0000000..1ddf0f7 --- /dev/null +++ b/src/Repository/ProjetRepository.php @@ -0,0 +1,195 @@ + + * + * @method Projet|null find($id, $lockMode = null, $lockVersion = null) + * @method Projet|null findOneBy(array $criteria, array $orderBy = null) + * @method Projet[] findAll() + * @method Projet[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ProjetRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Projet::class); + } + + public function save(Projet $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Projet $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Trouve les projets par statut + */ + public function findByStatut(string $statut): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.statut = :statut') + ->setParameter('statut', $statut) + ->orderBy('p.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets en cours + */ + public function findEnCours(): array + { + return $this->findByStatut(Projet::STATUT_EN_COURS); + } + + /** + * Trouve les projets terminés + */ + public function findTermines(): array + { + return $this->findByStatut(Projet::STATUT_TERMINE); + } + + /** + * Trouve les projets en attente + */ + public function findEnAttente(): array + { + return $this->findByStatut(Projet::STATUT_EN_ATTENTE); + } + + /** + * Trouve les projets annulés + */ + public function findAnnules(): array + { + return $this->findByStatut(Projet::STATUT_ANNULE); + } + + /** + * Trouve les projets par période de lancement + */ + public function findByPeriodeLancement(\DateTimeInterface $dateDebut, \DateTimeInterface $dateFin): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.dateLancement >= :dateDebut') + ->andWhere('p.dateLancement <= :dateFin') + ->setParameter('dateDebut', $dateDebut) + ->setParameter('dateFin', $dateFin) + ->orderBy('p.dateLancement', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets par période de clôture + */ + public function findByPeriodeCloture(\DateTimeInterface $dateDebut, \DateTimeInterface $dateFin): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.dateCloture >= :dateDebut') + ->andWhere('p.dateCloture <= :dateFin') + ->setParameter('dateDebut', $dateDebut) + ->setParameter('dateFin', $dateFin) + ->orderBy('p.dateCloture', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets actifs (en cours ou en attente) + */ + public function findActifs(): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.statut IN (:statuts)') + ->setParameter('statuts', [Projet::STATUT_EN_COURS, Projet::STATUT_EN_ATTENTE]) + ->orderBy('p.nom', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets avec le plus de contributions + */ + public function findByNombreContributions(): array + { + return $this->createQueryBuilder('p') + ->leftJoin('p.contributions', 'c') + ->addSelect('COUNT(c.id) as nombre_contributions') + ->groupBy('p.id') + ->orderBy('nombre_contributions', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets récents + */ + public function findRecents(int $limit = 10): array + { + return $this->createQueryBuilder('p') + ->orderBy('p.dateLancement', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * Trouve les projets par nom (recherche) + */ + public function findByNom(string $nom): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.nom LIKE :nom') + ->setParameter('nom', '%' . $nom . '%') + ->orderBy('p.nom', 'ASC') + ->getQuery() + ->getResult(); + } + +// /** +// * @return Projet[] Returns an array of Projet objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('p.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Projet +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} + + diff --git a/src/Service/LockService.php b/src/Service/LockService.php new file mode 100644 index 0000000..93febd1 --- /dev/null +++ b/src/Service/LockService.php @@ -0,0 +1,128 @@ +getUserId($request); + $sessionId = $request->getSession()->getId(); + $userAgent = $request->headers->get('User-Agent'); + $ipAddress = $request->getClientIp(); + + return $this->lockRepository->createLock( + $entityType, + $entityId, + $userId, + $sessionId, + $userAgent, + $ipAddress + ); + } + + /** + * Vérifie si une entité est verrouillée + */ + public function isLocked(string $entityType, int $entityId): bool + { + return $this->lockRepository->isLocked($entityType, $entityId); + } + + /** + * Vérifie si une entité est verrouillée par l'utilisateur actuel + */ + public function isLockedByCurrentUser(string $entityType, int $entityId, Request $request): bool + { + $userId = $this->getUserId($request); + $sessionId = $request->getSession()->getId(); + + return $this->lockRepository->isLockedByUser($entityType, $entityId, $userId, $sessionId); + } + + /** + * Prolonge un verrou + */ + public function extendLock(string $entityType, int $entityId, Request $request): bool + { + $userId = $this->getUserId($request); + $sessionId = $request->getSession()->getId(); + + return $this->lockRepository->extendLock($entityType, $entityId, $userId, $sessionId); + } + + /** + * Supprime un verrou + */ + public function removeLock(string $entityType, int $entityId, Request $request): bool + { + $userId = $this->getUserId($request); + $sessionId = $request->getSession()->getId(); + + return $this->lockRepository->removeByEntity($entityType, $entityId); + } + + /** + * Supprime tous les verrous de l'utilisateur actuel + */ + public function removeUserLocks(Request $request): int + { + $userId = $this->getUserId($request); + $sessionId = $request->getSession()->getId(); + + return $this->lockRepository->removeByUser($userId, $sessionId); + } + + /** + * Nettoie les verrous expirés + */ + public function cleanupExpiredLocks(): int + { + return $this->lockRepository->removeExpired(); + } + + /** + * Obtient l'ID de l'utilisateur (pour l'instant, on utilise l'IP + User-Agent) + */ + private function getUserId(Request $request): string + { + // Pour l'instant, on utilise l'IP + User-Agent comme identifiant unique + // Dans un vrai système, vous utiliseriez l'utilisateur connecté + return md5($request->getClientIp() . $request->headers->get('User-Agent')); + } + + /** + * Obtient les informations de verrou pour une entité + */ + public function getLockInfo(string $entityType, int $entityId): ?array + { + $lock = $this->lockRepository->findByEntity($entityType, $entityId); + + if (!$lock) { + return null; + } + + return [ + 'id' => $lock->getId(), + 'userId' => $lock->getUserId(), + 'lockedAt' => $lock->getLockedAt()->format('Y-m-d H:i:s'), + 'expiresAt' => $lock->getExpiresAt()->format('Y-m-d H:i:s'), + 'userAgent' => $lock->getUserAgent(), + 'ipAddress' => $lock->getIpAddress(), + 'isValid' => $lock->isValid(), + ]; + } +} + + diff --git a/templates/assistant_ia/edit.html.twig b/templates/assistant_ia/edit.html.twig new file mode 100644 index 0000000..c754593 --- /dev/null +++ b/templates/assistant_ia/edit.html.twig @@ -0,0 +1,40 @@ +{% extends 'base.html.twig' %} + +{% block title %}Modifier Assistant IA - {{ assistant_ia.nom }}{% endblock %} + +{% block body %} +
+

Modifier Assistant IA

+
+ + Voir + + + Retour à la liste + +
+
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/assistant_ia/index.html.twig b/templates/assistant_ia/index.html.twig new file mode 100644 index 0000000..b73562c --- /dev/null +++ b/templates/assistant_ia/index.html.twig @@ -0,0 +1,62 @@ +{% extends 'base.html.twig' %} + +{% block title %}Assistants IA{% endblock %} + +{% block body %} +
+

Assistants IA

+ + Nouvel Assistant IA + +
+ +
+ + + + + + + + + + + + + {% for assistant_ia in assistant_ias %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdNomNombre de contributionsMoyenne pertinenceMoyenne tempsActions
{{ assistant_ia.id }}{{ assistant_ia.nom }}{{ assistant_ia.nombreContributions }} + {% if assistant_ia.moyennePertinence %} + {{ assistant_ia.moyennePertinence }}/5 + {% else %} + Non évalué + {% endif %} + + {% if assistant_ia.moyenneTemps %} + {{ assistant_ia.moyenneTemps }}/5 + {% else %} + Non évalué + {% endif %} + + + Voir + + + Modifier + +
Aucun assistant IA trouvé
+
+{% endblock %} diff --git a/templates/assistant_ia/new.html.twig b/templates/assistant_ia/new.html.twig new file mode 100644 index 0000000..9756590 --- /dev/null +++ b/templates/assistant_ia/new.html.twig @@ -0,0 +1,35 @@ +{% extends 'base.html.twig' %} + +{% block title %}Nouvel Assistant IA{% endblock %} + +{% block body %} +
+

Nouvel Assistant IA

+ + Retour à la liste + +
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/assistant_ia/show.html.twig b/templates/assistant_ia/show.html.twig new file mode 100644 index 0000000..70535aa --- /dev/null +++ b/templates/assistant_ia/show.html.twig @@ -0,0 +1,116 @@ +{% extends 'base.html.twig' %} + +{% block title %}Assistant IA - {{ assistant_ia.nom }}{% endblock %} + +{% block body %} +
+

{{ assistant_ia.nom }}

+
+ + Modifier + + + Retour à la liste + +
+
+ +
+
+
+
+
Informations générales
+
+
+

ID : {{ assistant_ia.id }}

+

Nom : {{ assistant_ia.nom }}

+

Nombre de contributions : {{ assistant_ia.nombreContributions }}

+
+
+
+ +
+
+
+
Statistiques
+
+
+

Moyenne pertinence : + {% if assistant_ia.moyennePertinence %} + {{ assistant_ia.moyennePertinence }}/5 + {% else %} + Non évalué + {% endif %} +

+

Moyenne temps : + {% if assistant_ia.moyenneTemps %} + {{ assistant_ia.moyenneTemps }}/5 + {% else %} + Non évalué + {% endif %} +

+
+
+
+
+ +{% if assistant_ia.contribIas|length > 0 %} +
+
+
+
+
Contributions IA
+
+
+
+ + + + + + + + + + + + {% for contrib_ia in assistant_ia.contribIas %} + + + + + + + + {% endfor %} + +
ContributionPertinenceTempsMoyenneCommentaire
+ + {{ contrib_ia.contribution }} + + + {% if contrib_ia.evaluationPertinence %} + {{ contrib_ia.libellePertinence }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.evaluationTemps %} + {{ contrib_ia.libelleTemps }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.moyenneEvaluation %} + {{ contrib_ia.moyenneEvaluation }}/5 + {% else %} + Non évalué + {% endif %} + {{ contrib_ia.commentaire|default('') }}
+
+
+
+
+
+{% endif %} +{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index 3cda30f..56b3928 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -2,8 +2,65 @@ - {% block title %}Welcome!{% endblock %} + {% block title %}Gestion de Projet IA{% endblock %} + + + {% block stylesheets %} {% endblock %} @@ -12,6 +69,82 @@ {% endblock %} - {% block body %}{% endblock %} + + +
+ {% for type, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} + + {% block body %}{% endblock %} +
+ + + + diff --git a/templates/contrib_ia/edit.html.twig b/templates/contrib_ia/edit.html.twig new file mode 100644 index 0000000..ed0eb98 --- /dev/null +++ b/templates/contrib_ia/edit.html.twig @@ -0,0 +1,69 @@ +{% extends 'base.html.twig' %} + +{% block title %}Modifier Contribution IA{% endblock %} + +{% block body %} +
+

Modifier Contribution IA

+
+ + Voir + + + Retour à la liste + +
+
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.assistantIa) }} + {{ form_widget(form.assistantIa) }} + {{ form_errors(form.assistantIa) }} +
+ +
+ {{ form_label(form.contribution) }} + {{ form_widget(form.contribution) }} + {{ form_errors(form.contribution) }} +
+ +
+
+
+ {{ form_label(form.evaluationPertinence) }} + {{ form_widget(form.evaluationPertinence) }} + {{ form_errors(form.evaluationPertinence) }} +
+
+
+
+ {{ form_label(form.evaluationTemps) }} + {{ form_widget(form.evaluationTemps) }} + {{ form_errors(form.evaluationTemps) }} +
+
+
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/contrib_ia/index.html.twig b/templates/contrib_ia/index.html.twig new file mode 100644 index 0000000..284c93e --- /dev/null +++ b/templates/contrib_ia/index.html.twig @@ -0,0 +1,70 @@ +{% extends 'base.html.twig' %} + +{% block title %}Contributions IA{% endblock %} + +{% block body %} +
+

Contributions IA

+ + Nouvelle Contribution IA + +
+ +
+ + + + + + + + + + + + + + {% for contrib_ia in contrib_ias %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdAssistant IAContributionPertinenceTempsMoyenneActions
{{ contrib_ia.id }}{{ contrib_ia.assistantIa.nom }}{{ contrib_ia.contribution }} + {% if contrib_ia.evaluationPertinence %} + {{ contrib_ia.libellePertinence }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.evaluationTemps %} + {{ contrib_ia.libelleTemps }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.moyenneEvaluation %} + {{ contrib_ia.moyenneEvaluation }}/5 + {% else %} + Non évalué + {% endif %} + + + Voir + + + Modifier + +
Aucune contribution IA trouvée
+
+{% endblock %} diff --git a/templates/contrib_ia/new.html.twig b/templates/contrib_ia/new.html.twig new file mode 100644 index 0000000..25816de --- /dev/null +++ b/templates/contrib_ia/new.html.twig @@ -0,0 +1,64 @@ +{% extends 'base.html.twig' %} + +{% block title %}Nouvelle Contribution IA{% endblock %} + +{% block body %} +
+

Nouvelle Contribution IA

+ + Retour à la liste + +
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.assistantIa) }} + {{ form_widget(form.assistantIa) }} + {{ form_errors(form.assistantIa) }} +
+ +
+ {{ form_label(form.contribution) }} + {{ form_widget(form.contribution) }} + {{ form_errors(form.contribution) }} +
+ +
+
+
+ {{ form_label(form.evaluationPertinence) }} + {{ form_widget(form.evaluationPertinence) }} + {{ form_errors(form.evaluationPertinence) }} +
+
+
+
+ {{ form_label(form.evaluationTemps) }} + {{ form_widget(form.evaluationTemps) }} + {{ form_errors(form.evaluationTemps) }} +
+
+
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/contrib_ia/show.html.twig b/templates/contrib_ia/show.html.twig new file mode 100644 index 0000000..89a0029 --- /dev/null +++ b/templates/contrib_ia/show.html.twig @@ -0,0 +1,86 @@ +{% extends 'base.html.twig' %} + +{% block title %}Contribution IA - {{ contrib_ia.assistantIa.nom }}{% endblock %} + +{% block body %} +
+

Contribution IA

+
+ + Modifier + + + Retour à la liste + +
+
+ +
+
+
+
+
Informations générales
+
+
+

ID : {{ contrib_ia.id }}

+

Assistant IA : + + {{ contrib_ia.assistantIa.nom }} + +

+

Contribution : + + {{ contrib_ia.contribution }} + +

+
+
+
+ +
+
+
+
Évaluations
+
+
+

Pertinence : + {% if contrib_ia.evaluationPertinence %} + {{ contrib_ia.libellePertinence }} ({{ contrib_ia.evaluationPertinence }}/5) + {% else %} + Non évalué + {% endif %} +

+

Temps : + {% if contrib_ia.evaluationTemps %} + {{ contrib_ia.libelleTemps }} ({{ contrib_ia.evaluationTemps }}/5) + {% else %} + Non évalué + {% endif %} +

+

Moyenne : + {% if contrib_ia.moyenneEvaluation %} + {{ contrib_ia.moyenneEvaluation }}/5 + {% else %} + Non évalué + {% endif %} +

+
+
+
+
+ +{% if contrib_ia.commentaire %} +
+
+
+
+
Commentaire
+
+
+

{{ contrib_ia.commentaire }}

+
+
+
+
+{% endif %} +{% endblock %} diff --git a/templates/contribution/edit.html.twig b/templates/contribution/edit.html.twig new file mode 100644 index 0000000..faa7472 --- /dev/null +++ b/templates/contribution/edit.html.twig @@ -0,0 +1,64 @@ +{% extends 'base.html.twig' %} + +{% block title %}Modifier Contribution{% endblock %} + +{% block body %} +
+

Modifier Contribution

+
+ + Voir + + + Retour à la liste + +
+
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.membre) }} + {{ form_widget(form.membre) }} + {{ form_errors(form.membre) }} +
+ +
+ {{ form_label(form.projet) }} + {{ form_widget(form.projet) }} + {{ form_errors(form.projet) }} +
+ +
+ {{ form_label(form.dateContribution) }} + {{ form_widget(form.dateContribution) }} + {{ form_errors(form.dateContribution) }} +
+ +
+ {{ form_label(form.duree) }} + {{ form_widget(form.duree) }} + {{ form_errors(form.duree) }} +
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/contribution/index.html.twig b/templates/contribution/index.html.twig new file mode 100644 index 0000000..9513aa0 --- /dev/null +++ b/templates/contribution/index.html.twig @@ -0,0 +1,52 @@ +{% extends 'base.html.twig' %} + +{% block title %}Contributions{% endblock %} + +{% block body %} +
+

Contributions

+ + Nouvelle Contribution + +
+ +
+ + + + + + + + + + + + + + {% for contribution in contributions %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdMembreProjetDateDuréeCommentaireActions
{{ contribution.id }}{{ contribution.membre }}{{ contribution.projet.nom }}{{ contribution.dateContribution|date('d/m/Y') }}{{ contribution.dureeFormatee }}{{ contribution.commentaire|default('')|slice(0, 50) }}{% if contribution.commentaire|length > 50 %}...{% endif %} + + Voir + + + Modifier + +
Aucune contribution trouvée
+
+{% endblock %} diff --git a/templates/contribution/new.html.twig b/templates/contribution/new.html.twig new file mode 100644 index 0000000..8fc3400 --- /dev/null +++ b/templates/contribution/new.html.twig @@ -0,0 +1,59 @@ +{% extends 'base.html.twig' %} + +{% block title %}Nouvelle Contribution{% endblock %} + +{% block body %} +
+

Nouvelle Contribution

+ + Retour à la liste + +
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.membre) }} + {{ form_widget(form.membre) }} + {{ form_errors(form.membre) }} +
+ +
+ {{ form_label(form.projet) }} + {{ form_widget(form.projet) }} + {{ form_errors(form.projet) }} +
+ +
+ {{ form_label(form.dateContribution) }} + {{ form_widget(form.dateContribution) }} + {{ form_errors(form.dateContribution) }} +
+ +
+ {{ form_label(form.duree) }} + {{ form_widget(form.duree) }} + {{ form_errors(form.duree) }} +
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/contribution/show.html.twig b/templates/contribution/show.html.twig new file mode 100644 index 0000000..4c32de8 --- /dev/null +++ b/templates/contribution/show.html.twig @@ -0,0 +1,117 @@ +{% extends 'base.html.twig' %} + +{% block title %}Contribution - {{ contribution.membre }}{% endblock %} + +{% block body %} + + +
+
+
+
+
Informations générales
+
+
+

ID : {{ contribution.id }}

+

Membre : + + {{ contribution.membre }} + +

+

Projet : + + {{ contribution.projet.nom }} + +

+

Date : {{ contribution.dateContribution|date('d/m/Y') }}

+

Durée : {{ contribution.dureeFormatee }}

+
+
+
+ +
+
+
+
Commentaire
+
+
+ {% if contribution.commentaire %} +

{{ contribution.commentaire }}

+ {% else %} +

Aucun commentaire

+ {% endif %} +
+
+
+
+ +{% if contribution.contribIas|length > 0 %} +
+
+
+
+
Évaluations IA
+
+
+
+ + + + + + + + + + + + {% for contrib_ia in contribution.contribIas %} + + + + + + + + {% endfor %} + +
Assistant IAPertinenceTempsMoyenneCommentaire
+ + {{ contrib_ia.assistantIa.nom }} + + + {% if contrib_ia.evaluationPertinence %} + {{ contrib_ia.libellePertinence }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.evaluationTemps %} + {{ contrib_ia.libelleTemps }} + {% else %} + Non évalué + {% endif %} + + {% if contrib_ia.moyenneEvaluation %} + {{ contrib_ia.moyenneEvaluation }}/5 + {% else %} + Non évalué + {% endif %} + {{ contrib_ia.commentaire|default('') }}
+
+
+
+
+
+{% endif %} +{% endblock %} diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig new file mode 100644 index 0000000..b31f813 --- /dev/null +++ b/templates/home/index.html.twig @@ -0,0 +1,74 @@ +{% extends 'base.html.twig' %} + +{% block title %}Accueil - Gestion de Projet IA{% endblock %} + +{% block body %} +
+
+

Gestion de Projet IA

+

Bienvenue dans votre système de gestion de projet avec intelligence artificielle.

+
+
+ +
+
+
+
+
+ Membres +
+

Gérez les membres de votre équipe et leurs informations.

+ Voir les membres +
+
+
+ +
+
+
+
+ Projets +
+

Créez et gérez vos projets avec leurs statuts et dates.

+ Voir les projets +
+
+
+ +
+
+
+
+ Contributions +
+

Suivez les contributions des membres aux projets.

+ Voir les contributions +
+
+
+ +
+
+
+
+ Assistants IA +
+

Configurez et gérez vos assistants d'intelligence artificielle.

+ Voir les assistants IA +
+
+
+ +
+
+
+
+ Contributions IA +
+

Évaluez les contributions des assistants IA.

+ Voir les contributions IA +
+
+
+
+{% endblock %} diff --git a/templates/lock/_lock_info.html.twig b/templates/lock/_lock_info.html.twig new file mode 100644 index 0000000..9fbcff3 --- /dev/null +++ b/templates/lock/_lock_info.html.twig @@ -0,0 +1,14 @@ +
+
+ +
+ Élément verrouillé + + Modifié par {{ lockInfo.userId }} depuis {{ lockInfo.lockedAt }} + {% if lockInfo.expiresAt %} + - Expire à {{ lockInfo.expiresAt }} + {% endif %} + +
+
+
diff --git a/templates/lock/stats.html.twig b/templates/lock/stats.html.twig new file mode 100644 index 0000000..e174c12 --- /dev/null +++ b/templates/lock/stats.html.twig @@ -0,0 +1,124 @@ +{% extends 'base.html.twig' %} + +{% block title %}Statistiques des Verrous{% endblock %} + +{% block body %} +
+

Statistiques des Verrous

+
+ + +
+
+ +
+
+
+
+
Actions rapides
+
+
+
+ + +
+
+
+
+ +
+
+
+
Informations système
+
+
+

Verrous actifs : -

+

Dernière vérification : -

+

Verrous expirés : -

+
+
+
+
+ +
+
+
+
+
Verrous actifs
+
+
+
+

Chargement...

+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/membre/_delete_form.html.twig b/templates/membre/_delete_form.html.twig new file mode 100644 index 0000000..92fdb61 --- /dev/null +++ b/templates/membre/_delete_form.html.twig @@ -0,0 +1,6 @@ +
+ + +
diff --git a/templates/membre/_form.html.twig b/templates/membre/_form.html.twig new file mode 100644 index 0000000..46334f0 --- /dev/null +++ b/templates/membre/_form.html.twig @@ -0,0 +1,26 @@ +{{ form_start(form) }} +
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+ +
+ {{ form_label(form.prenom) }} + {{ form_widget(form.prenom) }} + {{ form_errors(form.prenom) }} +
+ +
+ {{ form_label(form.email) }} + {{ form_widget(form.email) }} + {{ form_errors(form.email) }} +
+ +
+ + Annuler +
+{{ form_end(form) }} diff --git a/templates/membre/edit.html.twig b/templates/membre/edit.html.twig new file mode 100644 index 0000000..5e2da75 --- /dev/null +++ b/templates/membre/edit.html.twig @@ -0,0 +1,27 @@ +{% extends 'base.html.twig' %} + +{% block title %}Modifier Membre - {{ membre }}{% endblock %} + +{% block body %} +
+

Modifier Membre

+
+ + Voir + + + Retour à la liste + +
+
+ +
+
+
+
+ {{ include('membre/_form.html.twig', {'button_label': 'Enregistrer'}) }} +
+
+
+
+{% endblock %} diff --git a/templates/membre/index.html.twig b/templates/membre/index.html.twig new file mode 100644 index 0000000..cb24c63 --- /dev/null +++ b/templates/membre/index.html.twig @@ -0,0 +1,169 @@ +{% extends 'base.html.twig' %} + +{% block title %}Membres{% endblock %} + +{% block body %} + + +
+ Édition inline : Cliquez sur les cellules nom, prénom ou email pour les modifier directement. +
Les modifications sont enregistrées automatiquement. Vérifiez la console (F12) pour les logs. +
+ +
+ + + + + + + + + + + + + {% for membre in membres %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdNomPrénomEmailContributionsActions
{{ membre.id }} + {{ membre.nom|default('') }} + + {{ membre.prenom|default('') }} + + {{ membre.email|default('') }} + {{ membre.contributions|length }} + + Voir + + + Modifier + +
Aucun membre trouvé
+
+ +{% block javascripts %} + {{ parent() }} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/membre/new.html.twig b/templates/membre/new.html.twig new file mode 100644 index 0000000..5132511 --- /dev/null +++ b/templates/membre/new.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block title %}Nouveau Membre{% endblock %} + +{% block body %} +
+

Nouveau Membre

+ + Retour à la liste + +
+ +
+
+
+
+ {{ include('membre/_form.html.twig') }} +
+
+
+
+{% endblock %} diff --git a/templates/membre/show.html.twig b/templates/membre/show.html.twig new file mode 100644 index 0000000..3a6139b --- /dev/null +++ b/templates/membre/show.html.twig @@ -0,0 +1,92 @@ +{% extends 'base.html.twig' %} + +{% block title %}Membre - {{ membre }}{% endblock %} + +{% block body %} + + +
+
+
+
+
Informations générales
+
+
+

ID : {{ membre.id }}

+

Nom : {{ membre.nom }}

+

Prénom : {{ membre.prenom }}

+

Email : {{ membre.email }}

+

Nombre de contributions : {{ membre.contributions|length }}

+
+
+
+
+ +{% if membre.contributions|length > 0 %} +
+
+
+
+
Contributions ({{ membre.contributions|length }})
+
+
+
+ + + + + + + + + + + + {% for contribution in membre.contributions %} + + + + + + + + {% endfor %} + +
ProjetDateDuréeCommentaireActions
{{ contribution.projet.nom }}{{ contribution.dateContribution|date('d/m/Y') }}{{ contribution.dureeFormatee }}{{ contribution.commentaire|default('')|slice(0, 30) }}{% if contribution.commentaire|length > 30 %}...{% endif %} + + Voir + +
+
+
+
+
+
+{% endif %} + +
+
+
+
+
Actions
+
+ + Modifier + + {{ include('membre/_delete_form.html.twig') }} +
+
+
+
+
+{% endblock %} diff --git a/templates/projet/edit.html.twig b/templates/projet/edit.html.twig new file mode 100644 index 0000000..7de9a27 --- /dev/null +++ b/templates/projet/edit.html.twig @@ -0,0 +1,69 @@ +{% extends 'base.html.twig' %} + +{% block title %}Modifier Projet - {{ projet.nom }}{% endblock %} + +{% block body %} +
+

Modifier Projet

+
+ + Voir + + + Retour à la liste + +
+
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+
+
+ {{ form_label(form.dateLancement) }} + {{ form_widget(form.dateLancement) }} + {{ form_errors(form.dateLancement) }} +
+
+
+
+ {{ form_label(form.dateCloture) }} + {{ form_widget(form.dateCloture) }} + {{ form_errors(form.dateCloture) }} +
+
+
+ +
+ {{ form_label(form.statut) }} + {{ form_widget(form.statut) }} + {{ form_errors(form.statut) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/projet/index.html.twig b/templates/projet/index.html.twig new file mode 100644 index 0000000..dcbbc14 --- /dev/null +++ b/templates/projet/index.html.twig @@ -0,0 +1,64 @@ +{% extends 'base.html.twig' %} + +{% block title %}Projets{% endblock %} + +{% block body %} + + +
+ + + + + + + + + + + + + + {% for projet in projets %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdNomStatutDate lancementDate clôtureCommentaireActions
{{ projet.id }}{{ projet.nom }} + {% set statutClass = { + 'en_attente': 'warning', + 'en_cours': 'info', + 'termine': 'success', + 'annule': 'danger' + } %} + + {% for label, value in projet.getStatutChoices() %} + {% if value == projet.statut %}{{ label }}{% endif %} + {% endfor %} + + {{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : '-' }}{{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : '-' }}{{ projet.commentaire|default('')|slice(0, 50) }}{% if projet.commentaire|length > 50 %}...{% endif %} + + Voir + + + Modifier + +
Aucun projet trouvé
+
+{% endblock %} diff --git a/templates/projet/new.html.twig b/templates/projet/new.html.twig new file mode 100644 index 0000000..7193c1f --- /dev/null +++ b/templates/projet/new.html.twig @@ -0,0 +1,64 @@ +{% extends 'base.html.twig' %} + +{% block title %}Nouveau Projet{% endblock %} + +{% block body %} +
+

Nouveau Projet

+ + Retour à la liste + +
+ +
+
+
+
+ {{ form_start(form) }} +
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+ +
+ {{ form_label(form.commentaire) }} + {{ form_widget(form.commentaire) }} + {{ form_errors(form.commentaire) }} +
+ +
+
+
+ {{ form_label(form.dateLancement) }} + {{ form_widget(form.dateLancement) }} + {{ form_errors(form.dateLancement) }} +
+
+
+
+ {{ form_label(form.dateCloture) }} + {{ form_widget(form.dateCloture) }} + {{ form_errors(form.dateCloture) }} +
+
+
+ +
+ {{ form_label(form.statut) }} + {{ form_widget(form.statut) }} + {{ form_errors(form.statut) }} +
+ +
+ + Annuler +
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/projet/show.html.twig b/templates/projet/show.html.twig new file mode 100644 index 0000000..40cd953 --- /dev/null +++ b/templates/projet/show.html.twig @@ -0,0 +1,103 @@ +{% extends 'base.html.twig' %} + +{% block title %}Projet - {{ projet.nom }}{% endblock %} + +{% block body %} +
+

{{ projet.nom }}

+
+ + Modifier + + + Retour à la liste + +
+
+ +
+
+
+
+
Informations générales
+
+
+

ID : {{ projet.id }}

+

Nom : {{ projet.nom }}

+

Statut : + {% set statutClass = { + 'en_attente': 'warning', + 'en_cours': 'info', + 'termine': 'success', + 'annule': 'danger' + } %} + + {% for label, value in projet.getStatutChoices() %} + {% if value == projet.statut %}{{ label }}{% endif %} + {% endfor %} + +

+

Date de lancement : {{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : 'Non définie' }}

+

Date de clôture : {{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : 'Non définie' }}

+
+
+
+ +
+
+
+
Commentaire
+
+
+ {% if projet.commentaire %} +

{{ projet.commentaire }}

+ {% else %} +

Aucun commentaire

+ {% endif %} +
+
+
+
+ +{% if projet.contributions|length > 0 %} +
+
+
+
+
Contributions ({{ projet.contributions|length }})
+
+
+
+ + + + + + + + + + + + {% for contribution in projet.contributions %} + + + + + + + + {% endfor %} + +
MembreDateDuréeCommentaireActions
{{ contribution.membre }}{{ contribution.dateContribution|date('d/m/Y') }}{{ contribution.dureeFormatee }}{{ contribution.commentaire|default('')|slice(0, 30) }}{% if contribution.commentaire|length > 30 %}...{% endif %} + + Voir + +
+
+
+
+
+
+{% endif %} +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..3122c1d --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,30 @@ +{% extends 'base.html.twig' %} + +{% block title %}Connexion{% endblock %} + +{% block body %} +
+
+
+
+

Connexion

+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+{% endblock %} diff --git a/templates/test.html.twig b/templates/test.html.twig new file mode 100644 index 0000000..5e00626 --- /dev/null +++ b/templates/test.html.twig @@ -0,0 +1,90 @@ + + + + Test Édition Inline + + + + +
+

Test d'Édition Inline

+ +
+ Instructions : Cliquez sur les cellules pour les modifier. +
+ + + + + + + + + + + + + + + + +
NomPrénomEmail
+ Jean + + Dupont + + jean.dupont@example.com +
+ +
+

Informations de Débogage

+
+
+
+ + + + +