Réalisation finale
This commit is contained in:
2
.env
2
.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
|
||||
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.tabCompletion": "on",
|
||||
"diffEditor.codeLens": true,
|
||||
"MutableAI.upsell": true
|
||||
}
|
||||
205
INLINE_EDITING.md
Normal file
205
INLINE_EDITING.md
Normal file
@@ -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
|
||||
<td class="editable-cell"
|
||||
data-entity-type="Projet"
|
||||
data-entity-id="{{ projet.id }}"
|
||||
data-field="nom">
|
||||
{{ projet.nom }}
|
||||
</td>
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
|
||||
2
_baseScripts/create_admin.sql
Normal file
2
_baseScripts/create_admin.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO membre (nom, prenom, email, roles, password) VALUES
|
||||
('Admin', 'System', 'admin@system.com', '["ROLE_ADMIN"]', '$2y$13$QJ7LSDls6TRywbxLOtRz9uMeNS0IlEAJ8IDy4o7hnZ1.9RSdKX5Ee');
|
||||
@@ -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)
|
||||
|
||||
160
_baseScripts/new bdd connexion.sql
Normal file
160
_baseScripts/new bdd connexion.sql
Normal file
@@ -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');
|
||||
@@ -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(
|
||||
|
||||
143
_baseScripts/structure_corrigee.sql
Normal file
143
_baseScripts/structure_corrigee.sql
Normal file
@@ -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');
|
||||
315
assets/js/inline-editing.js
Normal file
315
assets/js/inline-editing.js
Normal file
@@ -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}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
49
migrations/Version20251024091840.php
Normal file
49
migrations/Version20251024091840.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251024091840 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
51
migrations/Version20251024131225.php
Normal file
51
migrations/Version20251024131225.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251024131225 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
362
public/js/inline-editing.js
Normal file
362
public/js/inline-editing.js
Normal file
@@ -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}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
92
public/js/test-inline.js
Normal file
92
public/js/test-inline.js
Normal file
@@ -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}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
alert.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
console.log('Test d\'édition inline initialisé');
|
||||
});
|
||||
42
src/Command/CleanupLocksCommand.php
Normal file
42
src/Command/CleanupLocksCommand.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\LockService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:cleanup-locks',
|
||||
description: 'Nettoie les verrous expirés',
|
||||
)]
|
||||
class CleanupLocksCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
src/Command/CreateUserCommand.php
Normal file
59
src/Command/CreateUserCommand.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Membre;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
class CreateUserCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'app:create-user';
|
||||
|
||||
public function __construct(private EntityManagerInterface $em, private UserPasswordHasherInterface $hasher)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
}
|
||||
68
src/Controller/Api/AssistantIaApiController.php
Normal file
68
src/Controller/Api/AssistantIaApiController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\AssistantIa;
|
||||
use App\Repository\AssistantIaRepository;
|
||||
use App\Service\LockService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/assistant-ia')]
|
||||
class AssistantIaApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private AssistantIaRepository $assistantIaRepository,
|
||||
private LockService $lockService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/update-field', name: 'api_assistant_ia_update_field', methods: ['POST'])]
|
||||
public function updateField(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$assistant = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Controller/Api/ContribIaApiController.php
Normal file
68
src/Controller/Api/ContribIaApiController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\ContribIa;
|
||||
use App\Repository\ContribIaRepository;
|
||||
use App\Service\LockService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/contrib-ia')]
|
||||
class ContribIaApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private ContribIaRepository $contribIaRepository,
|
||||
private LockService $lockService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/update-field', name: 'api_contrib_ia_update_field', methods: ['POST'])]
|
||||
public function updateField(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$contrib = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Controller/Api/ContributionApiController.php
Normal file
68
src/Controller/Api/ContributionApiController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\Contribution;
|
||||
use App\Repository\ContributionRepository;
|
||||
use App\Service\LockService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/contribution')]
|
||||
class ContributionApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private ContributionRepository $contributionRepository,
|
||||
private LockService $lockService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/update-field', name: 'api_contribution_update_field', methods: ['POST'])]
|
||||
public function updateField(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$contrib = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/Controller/Api/MembreApiController.php
Normal file
165
src/Controller/Api/MembreApiController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\Membre;
|
||||
use App\Repository\MembreRepository;
|
||||
use App\Service\LockService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/membre')]
|
||||
class MembreApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private MembreRepository $membreRepository,
|
||||
private LockService $lockService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/lock', name: 'api_membre_lock', methods: ['POST'])]
|
||||
public function lock(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$membre = $this->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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
src/Controller/Api/ProjetApiController.php
Normal file
68
src/Controller/Api/ProjetApiController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\Projet;
|
||||
use App\Repository\ProjetRepository;
|
||||
use App\Service\LockService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/projet')]
|
||||
class ProjetApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private ProjetRepository $projetRepository,
|
||||
private LockService $lockService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/update-field', name: 'api_projet_update_field', methods: ['POST'])]
|
||||
public function updateField(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$projet = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/Controller/AssistantIaController.php
Normal file
85
src/Controller/AssistantIaController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\AssistantIa;
|
||||
use App\Form\AssistantIaType;
|
||||
use App\Repository\AssistantIaRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/assistant-ia')]
|
||||
class AssistantIaController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_assistant_ia_index', methods: ['GET'])]
|
||||
public function index(AssistantIaRepository $assistantIaRepository): Response
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
85
src/Controller/ContribIaController.php
Normal file
85
src/Controller/ContribIaController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\ContribIa;
|
||||
use App\Form\ContribIaType;
|
||||
use App\Repository\ContribIaRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/contrib-ia')]
|
||||
class ContribIaController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_contrib_ia_index', methods: ['GET'])]
|
||||
public function index(ContribIaRepository $contribIaRepository): Response
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
85
src/Controller/ContributionController.php
Normal file
85
src/Controller/ContributionController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Contribution;
|
||||
use App\Form\ContributionType;
|
||||
use App\Repository\ContributionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/contribution')]
|
||||
class ContributionController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_contribution_index', methods: ['GET'])]
|
||||
public function index(ContributionRepository $contributionRepository): Response
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
16
src/Controller/HomeController.php
Normal file
16
src/Controller/HomeController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('home/index.html.twig');
|
||||
}
|
||||
}
|
||||
55
src/Controller/LockController.php
Normal file
55
src/Controller/LockController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\LockService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/lock')]
|
||||
class LockController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private LockService $lockService
|
||||
) {}
|
||||
|
||||
#[Route('/cleanup', name: 'app_lock_cleanup', methods: ['POST'])]
|
||||
public function cleanup(): JsonResponse
|
||||
{
|
||||
$removedCount = $this->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');
|
||||
}
|
||||
}
|
||||
122
src/Controller/MembreController.php
Normal file
122
src/Controller/MembreController.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Membre;
|
||||
use App\Form\MembreType;
|
||||
use App\Repository\MembreRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/membre')]
|
||||
final class MembreController extends AbstractController
|
||||
{
|
||||
#[Route(name: 'app_membre_index', methods: ['GET'])]
|
||||
public function index(MembreRepository $membreRepository): Response
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/Controller/ProjetController.php
Normal file
85
src/Controller/ProjetController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Projet;
|
||||
use App\Form\ProjetType;
|
||||
use App\Repository\ProjetRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/projet')]
|
||||
class ProjetController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_projet_index', methods: ['GET'])]
|
||||
public function index(ProjetRepository $projetRepository): Response
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
30
src/Controller/SecurityController.php
Normal file
30
src/Controller/SecurityController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
use TargetPathTrait;
|
||||
#[Route(path: '/login', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->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
|
||||
}
|
||||
}
|
||||
34
src/Controller/TestController.php
Normal file
34
src/Controller/TestController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class TestController extends AbstractController
|
||||
{
|
||||
#[Route('/test', name: 'app_test', methods: ['GET'])]
|
||||
public function test(): JsonResponse
|
||||
{
|
||||
return new JsonResponse(['message' => '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');
|
||||
}
|
||||
}
|
||||
7
src/Controller/app_membre_update_field.php
Normal file
7
src/Controller/app_membre_update_field.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
// This file previously contained an auxiliary controller for AJAX updates.
|
||||
// Its functionality is now provided by `App\Controller\MembreController::updateField()`.
|
||||
// Keeping this file as a placeholder prevents accidental route/class duplication.
|
||||
|
||||
// Intentionally empty.
|
||||
199
src/Entity/Lock.php
Normal file
199
src/Entity/Lock.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\LockRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: LockRepository::class)]
|
||||
#[ORM\Table(name: 'lock_entity')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class Lock
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100)]
|
||||
#[Assert\NotBlank(message: 'L\'entité est obligatoire.')]
|
||||
private ?string $entityType = null;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Assert\Positive(message: 'L\'ID de l\'entité doit être positif.')]
|
||||
private ?int $entityId = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100)]
|
||||
#[Assert\NotBlank(message: 'L\'utilisateur est obligatoire.')]
|
||||
private ?string $userId = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100)]
|
||||
#[Assert\NotBlank(message: 'La session est obligatoire.')]
|
||||
private ?string $sessionId = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTimeInterface $lockedAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||
private ?\DateTimeInterface $expiresAt = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 50, nullable: true)]
|
||||
private ?string $userAgent = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 45, nullable: true)]
|
||||
private ?string $ipAddress = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int, string>
|
||||
*/
|
||||
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<int, Contribution>
|
||||
*/
|
||||
|
||||
32
src/Form/AssistantIaType.php
Normal file
32
src/Form/AssistantIaType.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\AssistantIa;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AssistantIaType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
78
src/Form/ContribIaType.php
Normal file
78
src/Form/ContribIaType.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\AssistantIa;
|
||||
use App\Entity\Contribution;
|
||||
use App\Entity\ContribIa;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ContribIaType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
src/Form/ContributionType.php
Normal file
66
src/Form/ContributionType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Contribution;
|
||||
use App\Entity\Membre;
|
||||
use App\Entity\Projet;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ContributionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
src/Form/MembreType.php
Normal file
27
src/Form/MembreType.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Membre;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class MembreType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('nom')
|
||||
->add('prenom')
|
||||
->add('email')
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Membre::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
61
src/Form/ProjetType.php
Normal file
61
src/Form/ProjetType.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Projet;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjetType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
110
src/Repository/AssistantIaRepository.php
Normal file
110
src/Repository/AssistantIaRepository.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AssistantIa;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AssistantIa>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
138
src/Repository/ContribIaRepository.php
Normal file
138
src/Repository/ContribIaRepository.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ContribIa;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ContribIa>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
164
src/Repository/ContributionRepository.php
Normal file
164
src/Repository/ContributionRepository.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Contribution;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Contribution>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
218
src/Repository/LockRepository.php
Normal file
218
src/Repository/LockRepository.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Lock;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Lock>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
185
src/Repository/MembreRepository.php
Normal file
185
src/Repository/MembreRepository.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Membre;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Membre>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
195
src/Repository/ProjetRepository.php
Normal file
195
src/Repository/ProjetRepository.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Projet;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Projet>
|
||||
*
|
||||
* @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()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
128
src/Service/LockService.php
Normal file
128
src/Service/LockService.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Lock;
|
||||
use App\Repository\LockRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class LockService
|
||||
{
|
||||
public function __construct(
|
||||
private LockRepository $lockRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Crée un verrou pour une entité
|
||||
*/
|
||||
public function createLock(string $entityType, int $entityId, Request $request): ?Lock
|
||||
{
|
||||
$userId = $this->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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
templates/assistant_ia/edit.html.twig
Normal file
40
templates/assistant_ia/edit.html.twig
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Modifier Assistant IA - {{ assistant_ia.nom }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Modifier Assistant IA</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_assistant_ia_show', {'id': assistant_ia.id}) }}" class="btn btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.nom) }}
|
||||
{{ form_widget(form.nom) }}
|
||||
{{ form_errors(form.nom) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
62
templates/assistant_ia/index.html.twig
Normal file
62
templates/assistant_ia/index.html.twig
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Assistants IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Assistants IA</h1>
|
||||
<a href="{{ path('app_assistant_ia_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Nouvel Assistant IA
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Nom</th>
|
||||
<th>Nombre de contributions</th>
|
||||
<th>Moyenne pertinence</th>
|
||||
<th>Moyenne temps</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for assistant_ia in assistant_ias %}
|
||||
<tr>
|
||||
<td>{{ assistant_ia.id }}</td>
|
||||
<td>{{ assistant_ia.nom }}</td>
|
||||
<td>{{ assistant_ia.nombreContributions }}</td>
|
||||
<td>
|
||||
{% if assistant_ia.moyennePertinence %}
|
||||
{{ assistant_ia.moyennePertinence }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if assistant_ia.moyenneTemps %}
|
||||
{{ assistant_ia.moyenneTemps }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_assistant_ia_show', {'id': assistant_ia.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_assistant_ia_edit', {'id': assistant_ia.id}) }}" class="btn btn-sm btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Aucun assistant IA trouvé</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
35
templates/assistant_ia/new.html.twig
Normal file
35
templates/assistant_ia/new.html.twig
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Nouvel Assistant IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Nouvel Assistant IA</h1>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.nom) }}
|
||||
{{ form_widget(form.nom) }}
|
||||
{{ form_errors(form.nom) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Créer
|
||||
</button>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
116
templates/assistant_ia/show.html.twig
Normal file
116
templates/assistant_ia/show.html.twig
Normal file
@@ -0,0 +1,116 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Assistant IA - {{ assistant_ia.nom }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>{{ assistant_ia.nom }}</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_assistant_ia_edit', {'id': assistant_ia.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations générales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>ID :</strong> {{ assistant_ia.id }}</p>
|
||||
<p><strong>Nom :</strong> {{ assistant_ia.nom }}</p>
|
||||
<p><strong>Nombre de contributions :</strong> {{ assistant_ia.nombreContributions }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Statistiques</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Moyenne pertinence :</strong>
|
||||
{% if assistant_ia.moyennePertinence %}
|
||||
{{ assistant_ia.moyennePertinence }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>Moyenne temps :</strong>
|
||||
{% if assistant_ia.moyenneTemps %}
|
||||
{{ assistant_ia.moyenneTemps }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if assistant_ia.contribIas|length > 0 %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Contributions IA</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Contribution</th>
|
||||
<th>Pertinence</th>
|
||||
<th>Temps</th>
|
||||
<th>Moyenne</th>
|
||||
<th>Commentaire</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contrib_ia in assistant_ia.contribIas %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ path('app_contribution_show', {'id': contrib_ia.contribution.id}) }}">
|
||||
{{ contrib_ia.contribution }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationPertinence %}
|
||||
{{ contrib_ia.libellePertinence }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationTemps %}
|
||||
{{ contrib_ia.libelleTemps }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.moyenneEvaluation %}
|
||||
{{ contrib_ia.moyenneEvaluation }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ contrib_ia.commentaire|default('') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -2,8 +2,65 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<title>{% block title %}Gestion de Projet IA{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.editable-cell {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.editable-cell:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.editable-cell.editing {
|
||||
background-color: #e3f2fd;
|
||||
border: 2px solid #2196f3;
|
||||
}
|
||||
|
||||
.inline-edit-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.inline-edit-input:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.lock-message {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.editable-cell.locked {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.editable-cell.locked::after {
|
||||
content: "🔒";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,6 +69,82 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ path('app_home') }}">Gestion Projet IA</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_membre_index') }}">Membres</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_projet_index') }}">Projets</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_contribution_index') }}">Contributions</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_assistant_ia_index') }}">Assistants IA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_contrib_ia_index') }}">Contributions IA</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_lock_stats') }}">Verrous</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
{% if app.user %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Bonjour, {{ app.user.prenom }} </a>
|
||||
</li>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_membre_index') }}">Gestion utilisateurs</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_logout') }}">Déconnexion</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('app_login') }}">Connexion</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
{% for type, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ type == 'error' ? 'danger' : type }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/inline-editing.js"></script>
|
||||
<script>
|
||||
// Test de débogage
|
||||
console.log('Script de base chargé');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM chargé, vérification de l\'édition inline...');
|
||||
if (window.inlineEditing) {
|
||||
console.log('✅ InlineEditing initialisé avec succès');
|
||||
} else {
|
||||
console.error('❌ InlineEditing non initialisé');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
69
templates/contrib_ia/edit.html.twig
Normal file
69
templates/contrib_ia/edit.html.twig
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Modifier Contribution IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Modifier Contribution IA</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_contrib_ia_show', {'id': contrib_ia.id}) }}" class="btn btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.assistantIa) }}
|
||||
{{ form_widget(form.assistantIa) }}
|
||||
{{ form_errors(form.assistantIa) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.contribution) }}
|
||||
{{ form_widget(form.contribution) }}
|
||||
{{ form_errors(form.contribution) }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.evaluationPertinence) }}
|
||||
{{ form_widget(form.evaluationPertinence) }}
|
||||
{{ form_errors(form.evaluationPertinence) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.evaluationTemps) }}
|
||||
{{ form_widget(form.evaluationTemps) }}
|
||||
{{ form_errors(form.evaluationTemps) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
70
templates/contrib_ia/index.html.twig
Normal file
70
templates/contrib_ia/index.html.twig
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Contributions IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Contributions IA</h1>
|
||||
<a href="{{ path('app_contrib_ia_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Nouvelle Contribution IA
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Assistant IA</th>
|
||||
<th>Contribution</th>
|
||||
<th>Pertinence</th>
|
||||
<th>Temps</th>
|
||||
<th>Moyenne</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contrib_ia in contrib_ias %}
|
||||
<tr>
|
||||
<td>{{ contrib_ia.id }}</td>
|
||||
<td>{{ contrib_ia.assistantIa.nom }}</td>
|
||||
<td>{{ contrib_ia.contribution }}</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationPertinence %}
|
||||
{{ contrib_ia.libellePertinence }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationTemps %}
|
||||
{{ contrib_ia.libelleTemps }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.moyenneEvaluation %}
|
||||
{{ contrib_ia.moyenneEvaluation }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_contrib_ia_show', {'id': contrib_ia.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_contrib_ia_edit', {'id': contrib_ia.id}) }}" class="btn btn-sm btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">Aucune contribution IA trouvée</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
templates/contrib_ia/new.html.twig
Normal file
64
templates/contrib_ia/new.html.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Nouvelle Contribution IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Nouvelle Contribution IA</h1>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.assistantIa) }}
|
||||
{{ form_widget(form.assistantIa) }}
|
||||
{{ form_errors(form.assistantIa) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.contribution) }}
|
||||
{{ form_widget(form.contribution) }}
|
||||
{{ form_errors(form.contribution) }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.evaluationPertinence) }}
|
||||
{{ form_widget(form.evaluationPertinence) }}
|
||||
{{ form_errors(form.evaluationPertinence) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.evaluationTemps) }}
|
||||
{{ form_widget(form.evaluationTemps) }}
|
||||
{{ form_errors(form.evaluationTemps) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Créer
|
||||
</button>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
86
templates/contrib_ia/show.html.twig
Normal file
86
templates/contrib_ia/show.html.twig
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Contribution IA - {{ contrib_ia.assistantIa.nom }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Contribution IA</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_contrib_ia_edit', {'id': contrib_ia.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations générales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>ID :</strong> {{ contrib_ia.id }}</p>
|
||||
<p><strong>Assistant IA :</strong>
|
||||
<a href="{{ path('app_assistant_ia_show', {'id': contrib_ia.assistantIa.id}) }}">
|
||||
{{ contrib_ia.assistantIa.nom }}
|
||||
</a>
|
||||
</p>
|
||||
<p><strong>Contribution :</strong>
|
||||
<a href="{{ path('app_contribution_show', {'id': contrib_ia.contribution.id}) }}">
|
||||
{{ contrib_ia.contribution }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Évaluations</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Pertinence :</strong>
|
||||
{% if contrib_ia.evaluationPertinence %}
|
||||
{{ contrib_ia.libellePertinence }} ({{ contrib_ia.evaluationPertinence }}/5)
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>Temps :</strong>
|
||||
{% if contrib_ia.evaluationTemps %}
|
||||
{{ contrib_ia.libelleTemps }} ({{ contrib_ia.evaluationTemps }}/5)
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>Moyenne :</strong>
|
||||
{% if contrib_ia.moyenneEvaluation %}
|
||||
{{ contrib_ia.moyenneEvaluation }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if contrib_ia.commentaire %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Commentaire</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ contrib_ia.commentaire }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
64
templates/contribution/edit.html.twig
Normal file
64
templates/contribution/edit.html.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Modifier Contribution{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Modifier Contribution</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_contribution_show', {'id': contribution.id}) }}" class="btn btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.membre) }}
|
||||
{{ form_widget(form.membre) }}
|
||||
{{ form_errors(form.membre) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.projet) }}
|
||||
{{ form_widget(form.projet) }}
|
||||
{{ form_errors(form.projet) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateContribution) }}
|
||||
{{ form_widget(form.dateContribution) }}
|
||||
{{ form_errors(form.dateContribution) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.duree) }}
|
||||
{{ form_widget(form.duree) }}
|
||||
{{ form_errors(form.duree) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
templates/contribution/index.html.twig
Normal file
52
templates/contribution/index.html.twig
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Contributions{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Contributions</h1>
|
||||
<a href="{{ path('app_contribution_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Nouvelle Contribution
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Membre</th>
|
||||
<th>Projet</th>
|
||||
<th>Date</th>
|
||||
<th>Durée</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contribution in contributions %}
|
||||
<tr>
|
||||
<td>{{ contribution.id }}</td>
|
||||
<td>{{ contribution.membre }}</td>
|
||||
<td>{{ contribution.projet.nom }}</td>
|
||||
<td>{{ contribution.dateContribution|date('d/m/Y') }}</td>
|
||||
<td>{{ contribution.dureeFormatee }}</td>
|
||||
<td>{{ contribution.commentaire|default('')|slice(0, 50) }}{% if contribution.commentaire|length > 50 %}...{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_contribution_show', {'id': contribution.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_contribution_edit', {'id': contribution.id}) }}" class="btn btn-sm btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">Aucune contribution trouvée</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/contribution/new.html.twig
Normal file
59
templates/contribution/new.html.twig
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Nouvelle Contribution{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Nouvelle Contribution</h1>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.membre) }}
|
||||
{{ form_widget(form.membre) }}
|
||||
{{ form_errors(form.membre) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.projet) }}
|
||||
{{ form_widget(form.projet) }}
|
||||
{{ form_errors(form.projet) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateContribution) }}
|
||||
{{ form_widget(form.dateContribution) }}
|
||||
{{ form_errors(form.dateContribution) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.duree) }}
|
||||
{{ form_widget(form.duree) }}
|
||||
{{ form_errors(form.duree) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Créer
|
||||
</button>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
117
templates/contribution/show.html.twig
Normal file
117
templates/contribution/show.html.twig
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Contribution - {{ contribution.membre }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Contribution</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_contribution_edit', {'id': contribution.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations générales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>ID :</strong> {{ contribution.id }}</p>
|
||||
<p><strong>Membre :</strong>
|
||||
<a href="{{ path('app_membre_show', {'id': contribution.membre.id}) }}">
|
||||
{{ contribution.membre }}
|
||||
</a>
|
||||
</p>
|
||||
<p><strong>Projet :</strong>
|
||||
<a href="{{ path('app_projet_show', {'id': contribution.projet.id}) }}">
|
||||
{{ contribution.projet.nom }}
|
||||
</a>
|
||||
</p>
|
||||
<p><strong>Date :</strong> {{ contribution.dateContribution|date('d/m/Y') }}</p>
|
||||
<p><strong>Durée :</strong> {{ contribution.dureeFormatee }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Commentaire</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contribution.commentaire %}
|
||||
<p>{{ contribution.commentaire }}</p>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucun commentaire</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if contribution.contribIas|length > 0 %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Évaluations IA</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Assistant IA</th>
|
||||
<th>Pertinence</th>
|
||||
<th>Temps</th>
|
||||
<th>Moyenne</th>
|
||||
<th>Commentaire</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contrib_ia in contribution.contribIas %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ path('app_assistant_ia_show', {'id': contrib_ia.assistantIa.id}) }}">
|
||||
{{ contrib_ia.assistantIa.nom }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationPertinence %}
|
||||
{{ contrib_ia.libellePertinence }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.evaluationTemps %}
|
||||
{{ contrib_ia.libelleTemps }}
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if contrib_ia.moyenneEvaluation %}
|
||||
{{ contrib_ia.moyenneEvaluation }}/5
|
||||
{% else %}
|
||||
<span class="text-muted">Non évalué</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ contrib_ia.commentaire|default('') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
74
templates/home/index.html.twig
Normal file
74
templates/home/index.html.twig
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Accueil - Gestion de Projet IA{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">Gestion de Projet IA</h1>
|
||||
<p class="lead">Bienvenue dans votre système de gestion de projet avec intelligence artificielle.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-users"></i> Membres
|
||||
</h5>
|
||||
<p class="card-text">Gérez les membres de votre équipe et leurs informations.</p>
|
||||
<a href="{{ path('app_membre_index') }}" class="btn btn-primary">Voir les membres</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-project-diagram"></i> Projets
|
||||
</h5>
|
||||
<p class="card-text">Créez et gérez vos projets avec leurs statuts et dates.</p>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-primary">Voir les projets</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-tasks"></i> Contributions
|
||||
</h5>
|
||||
<p class="card-text">Suivez les contributions des membres aux projets.</p>
|
||||
<a href="{{ path('app_contribution_index') }}" class="btn btn-primary">Voir les contributions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-robot"></i> Assistants IA
|
||||
</h5>
|
||||
<p class="card-text">Configurez et gérez vos assistants d'intelligence artificielle.</p>
|
||||
<a href="{{ path('app_assistant_ia_index') }}" class="btn btn-primary">Voir les assistants IA</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-brain"></i> Contributions IA
|
||||
</h5>
|
||||
<p class="card-text">Évaluez les contributions des assistants IA.</p>
|
||||
<a href="{{ path('app_contrib_ia_index') }}" class="btn btn-primary">Voir les contributions IA</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
templates/lock/_lock_info.html.twig
Normal file
14
templates/lock/_lock_info.html.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="lock-info" id="lock-info-{{ entityType }}-{{ entityId }}">
|
||||
<div class="alert alert-info d-flex align-items-center">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
<div>
|
||||
<strong>Élément verrouillé</strong>
|
||||
<small class="d-block text-muted">
|
||||
Modifié par {{ lockInfo.userId }} depuis {{ lockInfo.lockedAt }}
|
||||
{% if lockInfo.expiresAt %}
|
||||
- Expire à {{ lockInfo.expiresAt }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
124
templates/lock/stats.html.twig
Normal file
124
templates/lock/stats.html.twig
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Statistiques des Verrous{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Statistiques des Verrous</h1>
|
||||
<div>
|
||||
<button class="btn btn-warning" onclick="cleanupLocks()">
|
||||
<i class="fas fa-broom"></i> Nettoyer les verrous expirés
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="releaseAllLocks()">
|
||||
<i class="fas fa-unlock"></i> Libérer tous mes verrous
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Actions rapides</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary" onclick="refreshStats()">
|
||||
<i class="fas fa-sync"></i> Actualiser les statistiques
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="showUserLocks()">
|
||||
<i class="fas fa-list"></i> Mes verrous actifs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations système</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Verrous actifs :</strong> <span id="active-locks-count">-</span></p>
|
||||
<p><strong>Dernière vérification :</strong> <span id="last-check">-</span></p>
|
||||
<p><strong>Verrous expirés :</strong> <span id="expired-locks-count">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Verrous actifs</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="locks-list">
|
||||
<p class="text-muted">Chargement...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function refreshStats() {
|
||||
// Implémentation pour actualiser les statistiques
|
||||
console.log('Actualisation des statistiques...');
|
||||
}
|
||||
|
||||
function cleanupLocks() {
|
||||
fetch('/lock/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.removedLocks > 0) {
|
||||
alert(`${data.removedLocks} verrous expirés ont été supprimés.`);
|
||||
} else {
|
||||
alert('Aucun verrou expiré trouvé.');
|
||||
}
|
||||
refreshStats();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur:', error);
|
||||
alert('Erreur lors du nettoyage des verrous.');
|
||||
});
|
||||
}
|
||||
|
||||
function releaseAllLocks() {
|
||||
if (confirm('Êtes-vous sûr de vouloir libérer tous vos verrous ?')) {
|
||||
fetch('/lock/release-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert(`${data.removedLocks} verrous ont été libérés.`);
|
||||
refreshStats();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur:', error);
|
||||
alert('Erreur lors de la libération des verrous.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showUserLocks() {
|
||||
// Implémentation pour afficher les verrous de l'utilisateur
|
||||
console.log('Affichage des verrous de l\'utilisateur...');
|
||||
}
|
||||
|
||||
// Actualiser les statistiques au chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
refreshStats();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
6
templates/membre/_delete_form.html.twig
Normal file
6
templates/membre/_delete_form.html.twig
Normal file
@@ -0,0 +1,6 @@
|
||||
<form method="post" action="{{ path('app_membre_delete', {'id': membre.id}) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce membre ?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ membre.id) }}">
|
||||
<button class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Supprimer
|
||||
</button>
|
||||
</form>
|
||||
26
templates/membre/_form.html.twig
Normal file
26
templates/membre/_form.html.twig
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.nom) }}
|
||||
{{ form_widget(form.nom) }}
|
||||
{{ form_errors(form.nom) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.prenom) }}
|
||||
{{ form_widget(form.prenom) }}
|
||||
{{ form_errors(form.prenom) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.email) }}
|
||||
{{ form_widget(form.email) }}
|
||||
{{ form_errors(form.email) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> {{ button_label|default('Enregistrer') }}
|
||||
</button>
|
||||
<a href="{{ path('app_membre_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
27
templates/membre/edit.html.twig
Normal file
27
templates/membre/edit.html.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Modifier Membre - {{ membre }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Modifier Membre</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_membre_show', {'id': membre.id}) }}" class="btn btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_membre_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ include('membre/_form.html.twig', {'button_label': 'Enregistrer'}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
169
templates/membre/index.html.twig
Normal file
169
templates/membre/index.html.twig
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Membres{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Membres</h1>
|
||||
<a href="{{ path('app_membre_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Nouveau Membre
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Édition inline :</strong> Cliquez sur les cellules nom, prénom ou email pour les modifier directement.
|
||||
<br><small>Les modifications sont enregistrées automatiquement. Vérifiez la console (F12) pour les logs.</small>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Contributions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for membre in membres %}
|
||||
<tr data-membre-id="{{ membre.id }}">
|
||||
<td>{{ membre.id }}</td>
|
||||
<td class="editable-cell"
|
||||
data-entity-type="Membre"
|
||||
data-entity-id="{{ membre.id }}"
|
||||
data-field="nom"
|
||||
title="Cliquez pour modifier">
|
||||
{{ membre.nom|default('') }}
|
||||
</td>
|
||||
<td class="editable-cell"
|
||||
data-entity-type="Membre"
|
||||
data-entity-id="{{ membre.id }}"
|
||||
data-field="prenom"
|
||||
title="Cliquez pour modifier">
|
||||
{{ membre.prenom|default('') }}
|
||||
</td>
|
||||
<td class="editable-cell"
|
||||
data-entity-type="Membre"
|
||||
data-entity-id="{{ membre.id }}"
|
||||
data-field="email"
|
||||
title="Cliquez pour modifier">
|
||||
{{ membre.email|default('') }}
|
||||
</td>
|
||||
<td>{{ membre.contributions|length }}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_membre_show', {'id': membre.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_membre_edit', {'id': membre.id}) }}" class="btn btn-sm btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Aucun membre trouvé</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const editableCells = document.querySelectorAll('.editable-cell');
|
||||
|
||||
editableCells.forEach(cell => {
|
||||
cell.addEventListener('click', function() {
|
||||
// Ne rien faire si la cellule est déjà en mode édition
|
||||
if (cell.querySelector('input')) return;
|
||||
|
||||
const currentValue = cell.textContent.trim();
|
||||
const field = cell.dataset.field;
|
||||
const entityId = cell.dataset.entityId;
|
||||
const entityType = cell.dataset.entityType;
|
||||
|
||||
// Créer un input pour l'édition
|
||||
const input = document.createElement('input');
|
||||
input.type = field === 'email' ? 'email' : 'text';
|
||||
input.value = currentValue;
|
||||
input.classList.add('form-control', 'form-control-sm');
|
||||
|
||||
// Style pour l'input
|
||||
input.style.width = '100%';
|
||||
input.style.boxSizing = 'border-box';
|
||||
|
||||
// Remplacer le contenu de la cellule par l'input
|
||||
cell.innerHTML = '';
|
||||
cell.appendChild(input);
|
||||
input.focus();
|
||||
|
||||
// Gestion de la sauvegarde
|
||||
const saveChanges = async () => {
|
||||
const newValue = input.value.trim();
|
||||
|
||||
// Ne rien faire si la valeur n'a pas changé
|
||||
if (newValue === currentValue) {
|
||||
cell.textContent = currentValue;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Envoyer la requête AJAX
|
||||
const response = await fetch('{{ path('app_membre_update_field') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: entityId,
|
||||
field: field,
|
||||
value: newValue
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
cell.textContent = newValue;
|
||||
cell.classList.add('bg-success', 'text-white');
|
||||
setTimeout(() => {
|
||||
cell.classList.remove('bg-success', 'text-white');
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(data.message || 'Erreur lors de la mise à jour');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
cell.textContent = currentValue;
|
||||
alert('Erreur lors de la sauvegarde: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Sauvegarder lors de la perte de focus
|
||||
input.addEventListener('blur', saveChanges);
|
||||
|
||||
// Sauvegarder avec la touche Entrée
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Annuler avec la touche Échap
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
cell.textContent = currentValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
22
templates/membre/new.html.twig
Normal file
22
templates/membre/new.html.twig
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Nouveau Membre{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Nouveau Membre</h1>
|
||||
<a href="{{ path('app_membre_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ include('membre/_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
92
templates/membre/show.html.twig
Normal file
92
templates/membre/show.html.twig
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Membre - {{ membre }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>{{ membre }}</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_membre_edit', {'id': membre.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<a href="{{ path('app_membre_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations générales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>ID :</strong> {{ membre.id }}</p>
|
||||
<p><strong>Nom :</strong> {{ membre.nom }}</p>
|
||||
<p><strong>Prénom :</strong> {{ membre.prenom }}</p>
|
||||
<p><strong>Email :</strong> {{ membre.email }}</p>
|
||||
<p><strong>Nombre de contributions :</strong> {{ membre.contributions|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if membre.contributions|length > 0 %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Contributions ({{ membre.contributions|length }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Projet</th>
|
||||
<th>Date</th>
|
||||
<th>Durée</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contribution in membre.contributions %}
|
||||
<tr>
|
||||
<td>{{ contribution.projet.nom }}</td>
|
||||
<td>{{ contribution.dateContribution|date('d/m/Y') }}</td>
|
||||
<td>{{ contribution.dureeFormatee }}</td>
|
||||
<td>{{ contribution.commentaire|default('')|slice(0, 30) }}{% if contribution.commentaire|length > 30 %}...{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_contribution_show', {'id': contribution.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Actions</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ path('app_membre_edit', {'id': membre.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
{{ include('membre/_delete_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
69
templates/projet/edit.html.twig
Normal file
69
templates/projet/edit.html.twig
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Modifier Projet - {{ projet.nom }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Modifier Projet</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_projet_show', {'id': projet.id}) }}" class="btn btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.nom) }}
|
||||
{{ form_widget(form.nom) }}
|
||||
{{ form_errors(form.nom) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateLancement) }}
|
||||
{{ form_widget(form.dateLancement) }}
|
||||
{{ form_errors(form.dateLancement) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateCloture) }}
|
||||
{{ form_widget(form.dateCloture) }}
|
||||
{{ form_errors(form.dateCloture) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.statut) }}
|
||||
{{ form_widget(form.statut) }}
|
||||
{{ form_errors(form.statut) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Enregistrer
|
||||
</button>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
templates/projet/index.html.twig
Normal file
64
templates/projet/index.html.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Projets{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Projets</h1>
|
||||
<a href="{{ path('app_projet_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Nouveau Projet
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Nom</th>
|
||||
<th>Statut</th>
|
||||
<th>Date lancement</th>
|
||||
<th>Date clôture</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for projet in projets %}
|
||||
<tr>
|
||||
<td>{{ projet.id }}</td>
|
||||
<td>{{ projet.nom }}</td>
|
||||
<td>
|
||||
{% set statutClass = {
|
||||
'en_attente': 'warning',
|
||||
'en_cours': 'info',
|
||||
'termine': 'success',
|
||||
'annule': 'danger'
|
||||
} %}
|
||||
<span class="badge bg-{{ statutClass[projet.statut]|default('secondary') }}">
|
||||
{% for label, value in projet.getStatutChoices() %}
|
||||
{% if value == projet.statut %}{{ label }}{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : '-' }}</td>
|
||||
<td>{{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : '-' }}</td>
|
||||
<td>{{ projet.commentaire|default('')|slice(0, 50) }}{% if projet.commentaire|length > 50 %}...{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_projet_show', {'id': projet.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
<a href="{{ path('app_projet_edit', {'id': projet.id}) }}" class="btn btn-sm btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">Aucun projet trouvé</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
templates/projet/new.html.twig
Normal file
64
templates/projet/new.html.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Nouveau Projet{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Nouveau Projet</h1>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.nom) }}
|
||||
{{ form_widget(form.nom) }}
|
||||
{{ form_errors(form.nom) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.commentaire) }}
|
||||
{{ form_widget(form.commentaire) }}
|
||||
{{ form_errors(form.commentaire) }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateLancement) }}
|
||||
{{ form_widget(form.dateLancement) }}
|
||||
{{ form_errors(form.dateLancement) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.dateCloture) }}
|
||||
{{ form_widget(form.dateCloture) }}
|
||||
{{ form_errors(form.dateCloture) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form_label(form.statut) }}
|
||||
{{ form_widget(form.statut) }}
|
||||
{{ form_errors(form.statut) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Créer
|
||||
</button>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
templates/projet/show.html.twig
Normal file
103
templates/projet/show.html.twig
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Projet - {{ projet.nom }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>{{ projet.nom }}</h1>
|
||||
<div>
|
||||
<a href="{{ path('app_projet_edit', {'id': projet.id}) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</a>
|
||||
<a href="{{ path('app_projet_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour à la liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Informations générales</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>ID :</strong> {{ projet.id }}</p>
|
||||
<p><strong>Nom :</strong> {{ projet.nom }}</p>
|
||||
<p><strong>Statut :</strong>
|
||||
{% set statutClass = {
|
||||
'en_attente': 'warning',
|
||||
'en_cours': 'info',
|
||||
'termine': 'success',
|
||||
'annule': 'danger'
|
||||
} %}
|
||||
<span class="badge bg-{{ statutClass[projet.statut]|default('secondary') }}">
|
||||
{% for label, value in projet.getStatutChoices() %}
|
||||
{% if value == projet.statut %}{{ label }}{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Date de lancement :</strong> {{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : 'Non définie' }}</p>
|
||||
<p><strong>Date de clôture :</strong> {{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : 'Non définie' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Commentaire</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if projet.commentaire %}
|
||||
<p>{{ projet.commentaire }}</p>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucun commentaire</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if projet.contributions|length > 0 %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Contributions ({{ projet.contributions|length }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Membre</th>
|
||||
<th>Date</th>
|
||||
<th>Durée</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contribution in projet.contributions %}
|
||||
<tr>
|
||||
<td>{{ contribution.membre }}</td>
|
||||
<td>{{ contribution.dateContribution|date('d/m/Y') }}</td>
|
||||
<td>{{ contribution.dureeFormatee }}</td>
|
||||
<td>{{ contribution.commentaire|default('')|slice(0, 30) }}{% if contribution.commentaire|length > 30 %}...{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_contribution_show', {'id': contribution.id}) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i> Voir
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
30
templates/security/login.html.twig
Normal file
30
templates/security/login.html.twig
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Connexion{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Connexion</h3>
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ path('app_login') }}">
|
||||
<div class="mb-3">
|
||||
<label for="inputEmail" class="form-label">Email</label>
|
||||
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inputPassword" class="form-label">Mot de passe</label>
|
||||
<input type="password" name="password" id="inputPassword" class="form-control" required>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Se connecter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
90
templates/test.html.twig
Normal file
90
templates/test.html.twig
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Édition Inline</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.editable-cell {
|
||||
cursor: pointer;
|
||||
background-color: #f8f9fa;
|
||||
padding: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.editable-cell:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.editable-cell.editing {
|
||||
background-color: #e3f2fd;
|
||||
border: 2px solid #2196f3;
|
||||
}
|
||||
.inline-edit-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<h1>Test d'Édition Inline</h1>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Instructions :</strong> Cliquez sur les cellules pour les modifier.
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="editable-cell" data-entity-type="Membre" data-entity-id="1" data-field="nom">
|
||||
Jean
|
||||
</td>
|
||||
<td class="editable-cell" data-entity-type="Membre" data-entity-id="1" data-field="prenom">
|
||||
Dupont
|
||||
</td>
|
||||
<td class="editable-cell" data-entity-type="Membre" data-entity-id="1" data-field="email">
|
||||
jean.dupont@example.com
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="debug-info" class="mt-4">
|
||||
<h3>Informations de Débogage</h3>
|
||||
<div id="debug-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/test-inline.js"></script>
|
||||
<script>
|
||||
// Test de débogage
|
||||
console.log('Page de test chargée');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM chargé');
|
||||
|
||||
if (window.inlineEditing) {
|
||||
console.log('✅ InlineEditing initialisé');
|
||||
document.getElementById('debug-content').innerHTML = '<div class="alert alert-success">✅ JavaScript chargé et initialisé</div>';
|
||||
} else {
|
||||
console.error('❌ InlineEditing non initialisé');
|
||||
document.getElementById('debug-content').innerHTML = '<div class="alert alert-danger">❌ JavaScript non chargé</div>';
|
||||
}
|
||||
});
|
||||
|
||||
// Test des clics
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('editable-cell')) {
|
||||
console.log('Clic sur cellule éditables:', e.target);
|
||||
document.getElementById('debug-content').innerHTML += '<div class="alert alert-info">Clic détecté sur: ' + e.target.textContent + '</div>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user