Compare commits

...

4 Commits

Author SHA1 Message Date
aaa66849be 📝 Mise à jour du README avec les fonctionnalités finales et instructions d'installation 2025-05-08 15:20:40 +02:00
461e661f62 Finalisation application : sécurité, calendrier, vérifs d’unicité, filtres chauffagiste 2025-05-08 15:07:12 +02:00
a992c2ea6a Réparation du DashboardController et AuthenticationController 2025-05-08 13:58:02 +02:00
8580911c1a feat: gestion des remarques chauffagistes + refacto sécurité
- Ajout du champ 'Remarque' dans l'entité Intervention
- Création d'un formulaire RemarqueType dédié
- Ajout d'une route /intervention/{id}/remarque accessible uniquement au chauffagiste assigné
- Mise en place d'un contrôleur sécurisé pour ajouter une remarque
- Création de la vue intervention/remarque.html.twig
- Affichage conditionnel du bouton 'Ajouter une remarque' dans show.html.twig
- Séparation stricte des rôles : seuls les chauffagistes peuvent ajouter leur remarque
- Mise à jour de tous les contrôleurs avec denyUnlessAdminOrSecretaire() pour clarifier les accès
- Redirection des dashboards et calendriers selon rôle (admin, secrétaire, chauffagiste)

 Prochaine étape : liaison compétences ↔ pannes ou gestion des stocks associés
2025-05-08 12:31:40 +02:00
23 changed files with 506 additions and 382 deletions

4
.idea/dataSources.xml generated
View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hegreetconfort@localhost" uuid="21423ae4-3232-4641-affb-06399f70655a"> <data-source source="LOCAL" name="@localhost" uuid="21423ae4-3232-4641-affb-06399f70655a">
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hegreetconfort</jdbc-url> <jdbc-url>jdbc:postgresql://localhost:5433/</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
</component> </component>

110
README.md
View File

@ -1,54 +1,83 @@
# 🚀 Application de Gestion Chauffagiste # 🚀 Application de Gestion Chauffagiste
## Description ## Description
Cette application permet de gérer les interventions, les utilisateurs (chauffagistes, secrétaires, admin), les pièces détachées, les véhicules, et les plannings dans une entreprise de chauffagistes. Cette application permet de gérer les interventions, les utilisateurs (chauffagistes, secrétaires, admins), les pièces détachées, les véhicules, et les plannings dans une entreprise de chauffagistes.
---
## Fonctionnalités principales ## Fonctionnalités principales
- **Gestion des utilisateurs** : création de chauffagistes, secrétaires, et admins avec des rôles spécifiques. - **Gestion des utilisateurs** : création de chauffagistes, secrétaires, et admins avec des rôles spécifiques.
- **Gestion des interventions** : assignation des chauffagistes, suivi des véhicules et des pièces détachées. - **Gestion des interventions** : assignation des chauffagistes, véhicules, pièces détachées, ajout de remarques.
- **Gestion des stocks et des véhicules** : gestion des pièces détachées et des véhicules. - **Gestion des stocks et des véhicules** : CRUD complet pour les pièces détachées et les véhicules.
- **Planning des interventions** : chaque rôle a accès à un planning adapté (chauffagiste, secrétaire, admin). - **Planning des interventions** : chaque rôle accède à un planning personnalisé (chauffagiste, secrétaire, admin).
- **Sécurisation par rôles** : accès aux pages limité par rôle (Admin, Secrétaire, Chauffagiste). - **Sécurisation par rôles** : accès aux pages limité par rôle (Admin, Secrétaire, Chauffagiste).
### Précisions pour les rôles : ---
- **Admin** : Accès complet à toutes les fonctionnalités (gestion des utilisateurs, véhicules, stocks, plannings, etc.)
- **Secrétaire** : Peut gérer les chauffagistes, les interventions et le planning des chauffagistes, mais **ne peut pas gérer d'autres secrétaires ni les admins**. ## Fonctionnalités avancées
- **Chauffagiste** : Accède uniquement à ses propres interventions et à son planning. - ✅ **Contrôle dunicité** : interdiction de double-assignation dun même chauffagiste ou véhicule sur une même date/heure.
- ✅ **Calendrier FullCalendar dynamique** : affichage différent selon le rôle + clic pour afficher lintervention.
- ✅ **Sélecteurs intelligents** : seuls les chauffagistes apparaissent pour lassignation dans les interventions.
- ✅ **Remarque chauffagiste** : chaque chauffagiste peut ajouter une remarque à ses interventions uniquement.
- ✅ **Sécurité renforcée** : vérifications daccès sur toutes les routes sensibles.
---
## Rôles & restrictions
| Rôle | Droits |
|---------------|------------------------------------------------------------------------|
| **Admin** | Accès complet à tous les modules (utilisateurs, stocks, véhicules...) |
| **Secrétaire** | Accès complet à tous les modules **mais pas aux utilisateurs** |
| **Chauffagiste** | Accède uniquement à ses interventions et peut y ajouter des remarques |
---
## Installation ## Installation
### Prérequis : ### Prérequis :
- PHP 8.x - PHP 8.x
- Composer - Composer
- Symfony 7.x - Symfony 7.x
- Base de données PostgreSQL ou MySQL
### Étapes d'installation : ### Étapes :
1. Clonez le projet : ### 1. Clonez le projet
```bash ```bash
git clone https://gitea.btssio-poitiers.fr/sermandm/HegreEtConfort.git git clone https://gitea.btssio-poitiers.fr/sermandm/HegreEtConfort.git
``` cd HegreEtConfort
2. Installez les dépendances avec Composer : ```
```bash ### 2. Installez les dépendances
cd chauffagiste-app ```bash
composer install composer install
``` ```
3. Créez la base de données : ### 3. Créez la base de données
```bash ```bash
php bin/console doctrine:database:create php bin/console doctrine:database:create
``` ```
4. Exécutez les migrations pour créer les tables : ### 4. Appliquez les migrations
```bash ```bash
php bin/console doctrine:migrations:migrate php bin/console doctrine:migrations:migrate
``` ```
5. Lancez le serveur Symfony :
```bash
symfony server:start
```
Accédez ensuite à l'application sur `http://localhost:8000`. ### 5. Créez un utilisateur admin (exemple PostgreSQL) :
```sql
INSERT INTO "HegreEtConfort".public.utilisateur (
id, email, first_name, last_name, birth_date, phone, roles, password
)
VALUES (
1000, 'admin@admin.admin', 'admin', 'admin', '2025-04-10', '0000000000',
'["ROLE_ADMIN"]',
'$2y$13$4jqoZVgncgDJ6oPFDswZeeiVmt9TF2AC.xoBwyyrrbNl5Xz8r.50e'
);
```
## Sécurisation des accès ### 6. Démarrez le serveur Symfony :
Les secrétaires ont accès à toutes les pages **sauf celles concernant d'autres secrétaires et les admins**. Cela est géré par les contrôleurs via la méthode `denyAccessUnlessGranted()` pour vérifier le rôle de l'utilisateur. Par exemple, un secrétaire ne pourra pas modifier un autre secrétaire. ```bash
symfony server:start
```
➡️ Accédez ensuite à lapplication : [http://localhost:8000](http://localhost:8000)
--- ---
@ -56,4 +85,15 @@ Les secrétaires ont accès à toutes les pages **sauf celles concernant d'autre
- Symfony 7.x - Symfony 7.x
- Doctrine ORM - Doctrine ORM
- Twig - Twig
- PHP 8.x - PHP 8.x
- FullCalendar.js
- PostgreSQL (ou MySQL selon config)
---
## Auteur
Développé dans le cadre du BTS SIO - D'Hegre Et Confort
- Maxim SERMAND
- Alyssa ALLARD
- Giovanny BRUNET
- Lucas RAGUENEAU

View File

@ -0,0 +1,44 @@
<?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 Version20250508121539 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(<<<'SQL'
ALTER TABLE intervention ADD remarque TEXT DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE intervention ADD start_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE SCHEMA public
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE intervention DROP remarque
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE intervention DROP start_date
SQL);
}
}

View File

@ -4,7 +4,7 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class AuthenticationController extends AbstractController class AuthenticationController extends AbstractController
@ -12,10 +12,11 @@ class AuthenticationController extends AbstractController
#[Route(path: '/', name: 'app_login')] #[Route(path: '/', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {
// Get the login error if there is one if ($this->getUser()) {
$error = $authenticationUtils->getLastAuthenticationError(); return $this->redirectToRoute('dashboard');
}
// Last username entered by the user $error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/index.html.twig', [ return $this->render('login/index.html.twig', [
@ -27,6 +28,6 @@ class AuthenticationController extends AbstractController
#[Route(path: '/logout', name: 'app_logout')] #[Route(path: '/logout', name: 'app_logout')]
public function logout(): void public function logout(): void
{ {
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new \LogicException('Cette méthode est interceptée par Symfony pour déconnecter.');
} }
} }

View File

@ -13,24 +13,21 @@ class CalendrierController extends AbstractController
#[Route('/chauffagiste', name: 'app_calendrier_indexChauffagiste')] #[Route('/chauffagiste', name: 'app_calendrier_indexChauffagiste')]
public function indexChauffagiste(InterventionRepository $interventionRepository): Response public function indexChauffagiste(InterventionRepository $interventionRepository): Response
{ {
// Vérification : seul le chauffagiste connecté peut voir son propre calendrier
$this->denyAccessUnlessGranted('ROLE_CHAUFFAGISTE'); $this->denyAccessUnlessGranted('ROLE_CHAUFFAGISTE');
// Récupérer les interventions du chauffagiste connecté
$interventions = $interventionRepository->findByUser($this->getUser()); $interventions = $interventionRepository->findByUser($this->getUser());
// Préparer les événements pour FullCalendar
$events = []; $events = [];
foreach ($interventions as $intervention) { foreach ($interventions as $intervention) {
$events[] = [ $events[] = [
'title' => $intervention->getTitle(), 'title' => ' - ' . $intervention->getWording() . ' (' . $intervention->getStatus() . ')',
'start' => $intervention->getStartDate()->format('Y-m-d H:i:s'), 'start' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'end' => $intervention->getEndDate()->format('Y-m-d H:i:s'), 'end' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'description' => $intervention->getDescription(), 'description' => $intervention->getDescription(),
'url' => $this->generateUrl('app_intervention_show', ['id' => $intervention->getId()])
]; ];
} }
// Passer les événements à la vue
return $this->render('calendrier/indexChauffagiste.html.twig', [ return $this->render('calendrier/indexChauffagiste.html.twig', [
'events' => json_encode($events), 'events' => json_encode($events),
]); ]);
@ -39,24 +36,21 @@ class CalendrierController extends AbstractController
#[Route('/secretaire', name: 'app_calendrier_indexSecretaire')] #[Route('/secretaire', name: 'app_calendrier_indexSecretaire')]
public function indexSecretaire(InterventionRepository $interventionRepository): Response public function indexSecretaire(InterventionRepository $interventionRepository): Response
{ {
// Vérification : seul le secrétaire peut accéder à ce calendrier
$this->denyAccessUnlessGranted('ROLE_SECRETAIRE'); $this->denyAccessUnlessGranted('ROLE_SECRETAIRE');
// Récupérer toutes les interventions de tous les chauffagistes
$interventions = $interventionRepository->findAll(); $interventions = $interventionRepository->findAll();
// Préparer les événements pour FullCalendar
$events = []; $events = [];
foreach ($interventions as $intervention) { foreach ($interventions as $intervention) {
$events[] = [ $events[] = [
'title' => $intervention->getTitle(), 'title' => ' - ' . $intervention->getWording() . ' (' . $intervention->getStatus() . ')',
'start' => $intervention->getStartDate()->format('Y-m-d H:i:s'), 'start' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'end' => $intervention->getEndDate()->format('Y-m-d H:i:s'), 'end' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'description' => $intervention->getDescription(), 'description' => $intervention->getDescription(),
'url' => $this->generateUrl('app_intervention_show', ['id' => $intervention->getId()])
]; ];
} }
// Passer les événements à la vue
return $this->render('calendrier/indexSecretaire.html.twig', [ return $this->render('calendrier/indexSecretaire.html.twig', [
'events' => json_encode($events), 'events' => json_encode($events),
]); ]);
@ -65,24 +59,21 @@ class CalendrierController extends AbstractController
#[Route('/admin', name: 'app_calendrier_index')] #[Route('/admin', name: 'app_calendrier_index')]
public function indexAdmin(InterventionRepository $interventionRepository): Response public function indexAdmin(InterventionRepository $interventionRepository): Response
{ {
// Vérification : seul un admin peut accéder à ce calendrier
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
// Récupérer toutes les interventions de tous les chauffagistes
$interventions = $interventionRepository->findAll(); $interventions = $interventionRepository->findAll();
// Préparer les événements pour FullCalendar
$events = []; $events = [];
foreach ($interventions as $intervention) { foreach ($interventions as $intervention) {
$events[] = [ $events[] = [
'title' => $intervention->getTitle(), 'title' => ' - ' . $intervention->getWording() . ' (' . $intervention->getStatus() . ')',
'start' => $intervention->getStartDate()->format('Y-m-d H:i:s'), 'start' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'end' => $intervention->getEndDate()->format('Y-m-d H:i:s'), 'end' => $intervention->getTimestamp()?->format('Y-m-d\TH:i:s') ?? '',
'description' => $intervention->getDescription(), 'description' => $intervention->getDescription(),
'url' => $this->generateUrl('app_intervention_show', ['id' => $intervention->getId()])
]; ];
} }
// Passer les événements à la vue
return $this->render('calendrier/index.html.twig', [ return $this->render('calendrier/index.html.twig', [
'events' => json_encode($events), 'events' => json_encode($events),
]); ]);

View File

@ -4,28 +4,29 @@ namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/dashboard')]
class DashboardController extends AbstractController class DashboardController extends AbstractController
{ {
#[Route('/admin/dashboard', name: 'admin_dashboard')] #[Route('/', name: 'dashboard')]
public function admin(): Response #[Route('/admin', name: 'admin_dashboard')]
#[Route('/secretaire', name: 'secretaire_dashboard')]
#[Route('/chauffagiste', name: 'chauffagiste_dashboard')]
public function index(): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); if ($this->isGranted('ROLE_ADMIN')) {
return $this->render('dashboard/admin.html.twig'); return $this->render('dashboard/admin.html.twig');
} }
#[Route('/secretaire/dashboard', name: 'secretaire_dashboard')] if ($this->isGranted('ROLE_SECRETAIRE')) {
public function secretaire(): Response return $this->render('dashboard/secretaire.html.twig');
{ }
$this->denyAccessUnlessGranted('ROLE_SECRETAIRE');
return $this->render('dashboard/secretaire.html.twig');
}
#[Route('/chauffagiste/dashboard', name: 'chauffagiste_dashboard')] if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
public function chauffagiste(): Response return $this->render('dashboard/chauffagiste.html.twig');
{ }
$this->denyAccessUnlessGranted('ROLE_CHAUFFAGISTE');
return $this->render('dashboard/chauffagiste.html.twig'); throw $this->createAccessDeniedException('Vous ne pouvez pas accéder à ce tableau de bord.');
} }
} }

View File

@ -9,45 +9,36 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/fault')] #[Route('/fault')]
final class FaultController extends AbstractController class FaultController extends AbstractController
{ {
#[Route(name: 'app_fault_index', methods: ['GET'])] #[Route('/', name: 'app_fault_index', methods: ['GET'])]
public function index(FaultRepository $faultRepository): Response public function index(FaultRepository $faultRepository): Response
{ {
// Filtrage des pannes : un chauffagiste ne peut voir que ses pannes $this->denyUnlessAdminOrSecretaire();
$faults = $this->isGranted('ROLE_CHAUFFAGISTE')
? $faultRepository->findByUser($this->getUser()) // Filtre les pannes par utilisateur
: $faultRepository->findAll(); // Admins voient toutes les pannes
return $this->render('fault/index.html.twig', [ return $this->render('fault/index.html.twig', [
'faults' => $faults, 'faults' => $faultRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_fault_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_fault_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$fault = new Fault(); $fault = new Fault();
$form = $this->createForm(FaultType::class, $fault); $form = $this->createForm(FaultType::class, $fault);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Associe la panne à un chauffagiste si c'est un chauffagiste
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$fault->setUser($this->getUser());
}
$entityManager->persist($fault); $entityManager->persist($fault);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_fault_index');
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('fault/new.html.twig', [ return $this->render('fault/new.html.twig', [
'fault' => $fault,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -55,11 +46,7 @@ final class FaultController extends AbstractController
#[Route('/{id}', name: 'app_fault_show', methods: ['GET'])] #[Route('/{id}', name: 'app_fault_show', methods: ['GET'])]
public function show(Fault $fault): Response public function show(Fault $fault): Response
{ {
// Un chauffagiste ne peut voir que ses pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir cette panne.');
}
return $this->render('fault/show.html.twig', [ return $this->render('fault/show.html.twig', [
'fault' => $fault, 'fault' => $fault,
]); ]);
@ -68,39 +55,39 @@ final class FaultController extends AbstractController
#[Route('/{id}/edit', name: 'app_fault_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_fault_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response public function edit(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response
{ {
// Un chauffagiste ne peut modifier que ses propres pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier cette panne.');
}
$form = $this->createForm(FaultType::class, $fault); $form = $this->createForm(FaultType::class, $fault);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_fault_index');
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('fault/edit.html.twig', [ return $this->render('fault/edit.html.twig', [
'fault' => $fault,
'form' => $form, 'form' => $form,
'fault' => $fault,
]); ]);
} }
#[Route('/{id}', name: 'app_fault_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_fault_delete', methods: ['POST'])]
public function delete(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response public function delete(Request $request, Fault $fault, EntityManagerInterface $entityManager): Response
{ {
// Un chauffagiste ne peut supprimer que ses propres pannes $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $fault->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer cette panne.');
}
if ($this->isCsrfTokenValid('delete'.$fault->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete'.$fault->getId(), $request->request->get('_token'))) {
$entityManager->remove($fault); $entityManager->remove($fault);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_fault_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_fault_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -4,54 +4,73 @@ namespace App\Controller;
use App\Entity\Intervention; use App\Entity\Intervention;
use App\Form\InterventionType; use App\Form\InterventionType;
use App\Form\RemarqueType;
use App\Repository\InterventionRepository; use App\Repository\InterventionRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/intervention')] #[Route('/intervention')]
final class InterventionController extends AbstractController class InterventionController extends AbstractController
{ {
#[Route(name: 'app_intervention_index', methods: ['GET'])] #[Route('/', name: 'app_intervention_index', methods: ['GET'])]
public function index(InterventionRepository $interventionRepository): Response public function index(InterventionRepository $interventionRepository): Response
{ {
// Vérifier si l'utilisateur est un chauffagiste, pour filtrer ses interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$interventions = $interventionRepository->findByUser($this->getUser()); // On filtre par utilisateur connecté
} else {
// Les autres rôles (admin) peuvent voir toutes les interventions
$interventions = $interventionRepository->findAll();
}
return $this->render('intervention/index.html.twig', [ return $this->render('intervention/index.html.twig', [
'interventions' => $interventions, 'interventions' => $interventionRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_intervention_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_intervention_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager, InterventionRepository $interventionRepository): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$intervention = new Intervention(); $intervention = new Intervention();
$form = $this->createForm(InterventionType::class, $intervention); $form = $this->createForm(InterventionType::class, $intervention);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Si l'utilisateur est un chauffagiste, on associe l'intervention à lui $timestamp = $intervention->getTimestamp();
if ($this->isGranted('ROLE_CHAUFFAGISTE')) { $chauffagiste = $intervention->getUser();
$intervention->setUser($this->getUser()); $vehicule = $intervention->getVehicle();
$conflictsUser = $interventionRepository->findBy([
'Timestamp' => $timestamp,
'user' => $chauffagiste,
]);
if ($conflictsUser) {
$this->addFlash('error', 'Ce chauffagiste a déjà une intervention à cette date.');
return $this->render('intervention/new.html.twig', [
'form' => $form,
]);
}
if ($vehicule) {
$conflictsVehicule = $interventionRepository->findBy([
'Timestamp' => $timestamp,
'vehicle' => $vehicule,
]);
if ($conflictsVehicule) {
$this->addFlash('error', 'Ce véhicule est déjà utilisé à cette date.');
return $this->render('intervention/new.html.twig', [
'form' => $form,
]);
}
} }
$entityManager->persist($intervention); $entityManager->persist($intervention);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_intervention_index');
} }
return $this->render('intervention/new.html.twig', [ return $this->render('intervention/new.html.twig', [
'intervention' => $intervention,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -59,73 +78,113 @@ final class InterventionController extends AbstractController
#[Route('/{id}', name: 'app_intervention_show', methods: ['GET'])] #[Route('/{id}', name: 'app_intervention_show', methods: ['GET'])]
public function show(Intervention $intervention): Response public function show(Intervention $intervention): Response
{ {
// Vérifier si l'utilisateur peut voir cette intervention (chauffagiste ne voit que ses interventions) $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir cette intervention.');
}
return $this->render('intervention/show.html.twig', [ return $this->render('intervention/show.html.twig', [
'intervention' => $intervention, 'intervention' => $intervention,
]); ]);
} }
#[Route('/{id}/edit', name: 'app_intervention_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_intervention_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response public function edit(Request $request, Intervention $intervention, EntityManagerInterface $entityManager, InterventionRepository $interventionRepository): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut modifier que ses propres interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier cette intervention.');
}
$form = $this->createForm(InterventionType::class, $intervention); $form = $this->createForm(InterventionType::class, $intervention);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $timestamp = $intervention->getTimestamp();
$chauffagiste = $intervention->getUser();
$vehicule = $intervention->getVehicle();
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); $conflictUser = $interventionRepository->createQueryBuilder('i')
->where('i.Timestamp = :time')
->andWhere('i.user = :user')
->andWhere('i != :current')
->setParameter('time', $timestamp)
->setParameter('user', $chauffagiste)
->setParameter('current', $intervention)
->getQuery()
->getResult();
if ($conflictUser) {
$this->addFlash('error', 'Ce chauffagiste a déjà une autre intervention à cette date.');
return $this->render('intervention/edit.html.twig', [
'form' => $form,
'intervention' => $intervention,
]);
}
if ($vehicule) {
$conflictVehicule = $interventionRepository->createQueryBuilder('i')
->where('i.Timestamp = :time')
->andWhere('i.vehicle = :vehicule')
->andWhere('i != :current')
->setParameter('time', $timestamp)
->setParameter('user', $chauffagiste)
->setParameter('current', $intervention)
->getQuery()
->getResult();
if ($conflictVehicule) {
$this->addFlash('error', 'Ce véhicule est déjà utilisé à cette date.');
return $this->render('intervention/edit.html.twig', [
'form' => $form,
'intervention' => $intervention,
]);
}
}
$entityManager->flush();
return $this->redirectToRoute('app_intervention_index');
} }
return $this->render('intervention/edit.html.twig', [ return $this->render('intervention/edit.html.twig', [
'intervention' => $intervention,
'form' => $form, 'form' => $form,
'intervention' => $intervention,
]); ]);
} }
#[Route('/{id}', name: 'app_intervention_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_intervention_delete', methods: ['POST'])]
public function delete(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response public function delete(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut supprimer que ses propres interventions $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $intervention->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer cette intervention.');
}
if ($this->isCsrfTokenValid('delete'.$intervention->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete' . $intervention->getId(), $request->request->get('_token'))) {
$entityManager->remove($intervention); $entityManager->remove($intervention);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_intervention_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_intervention_index');
} }
#[Route('/{id}/remarque', name: 'app_intervention_remarque', methods: ['GET', 'POST'])]
public function ajouterRemarque(Request $request, Intervention $intervention, EntityManagerInterface $entityManager): Response
#[Route('/api/interventions', name: 'api_interventions')]
public function apiInterventions(InterventionRepository $repo): JsonResponse
{ {
$interventions = $repo->findAll(); $user = $this->getUser();
if (!$this->isGranted('ROLE_CHAUFFAGISTE') || $intervention->getUser() !== $user) {
$events = []; throw $this->createAccessDeniedException("Vous ne pouvez modifier que vos propres interventions.");
foreach ($interventions as $intervention) {
$events[] = [
'id' => $intervention->getId(),
'title' => $intervention->getWording(), // ou getTitre() selon ton entité
'start' => $intervention->getDate()->format('Y-m-d\TH:i:s'),
// ajoute 'end' si tu veux une durée
];
} }
return $this->json($events); $form = $this->createForm(RemarqueType::class, $intervention);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
$this->addFlash('success', 'Remarque ajoutée avec succès.');
return $this->redirectToRoute('app_intervention_show', ['id' => $intervention->getId()]);
}
return $this->render('intervention/remarque.html.twig', [
'form' => $form,
'intervention' => $intervention,
]);
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -9,16 +9,16 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/skill')] #[Route('/skill')]
final class SkillController extends AbstractController class SkillController extends AbstractController
{ {
#[Route(name: 'app_skill_index', methods: ['GET'])] #[Route('/', name: 'app_skill_index', methods: ['GET'])]
public function index(SkillRepository $skillRepository): Response public function index(SkillRepository $skillRepository): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
return $this->render('skill/admin.html.twig', [ return $this->render('skill/index.html.twig', [
'skills' => $skillRepository->findAll(), 'skills' => $skillRepository->findAll(),
]); ]);
} }
@ -26,7 +26,8 @@ final class SkillController extends AbstractController
#[Route('/new', name: 'app_skill_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_skill_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
$skill = new Skill(); $skill = new Skill();
$form = $this->createForm(SkillType::class, $skill); $form = $this->createForm(SkillType::class, $skill);
$form->handleRequest($request); $form->handleRequest($request);
@ -35,11 +36,10 @@ final class SkillController extends AbstractController
$entityManager->persist($skill); $entityManager->persist($skill);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
} }
return $this->render('skill/new.html.twig', [ return $this->render('skill/new.html.twig', [
'skill' => $skill,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -47,6 +47,7 @@ final class SkillController extends AbstractController
#[Route('/{id}', name: 'app_skill_show', methods: ['GET'])] #[Route('/{id}', name: 'app_skill_show', methods: ['GET'])]
public function show(Skill $skill): Response public function show(Skill $skill): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('skill/show.html.twig', [ return $this->render('skill/show.html.twig', [
'skill' => $skill, 'skill' => $skill,
]); ]);
@ -55,31 +56,40 @@ final class SkillController extends AbstractController
#[Route('/{id}/edit', name: 'app_skill_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_skill_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response public function edit(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
$form = $this->createForm(SkillType::class, $skill); $form = $this->createForm(SkillType::class, $skill);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
} }
return $this->render('skill/edit.html.twig', [ return $this->render('skill/edit.html.twig', [
'skill' => $skill,
'form' => $form, 'form' => $form,
'skill' => $skill,
]); ]);
} }
#[Route('/{id}', name: 'app_skill_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_skill_delete', methods: ['POST'])]
public function delete(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response public function delete(Request $request, Skill $skill, EntityManagerInterface $entityManager): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyUnlessAdminOrSecretaire();
if ($this->isCsrfTokenValid('delete'.$skill->getId(), $request->get('csrf_token'))) {
if ($this->isCsrfTokenValid('delete' . $skill->getId(), $request->request->get('_token'))) {
$entityManager->remove($skill); $entityManager->remove($skill);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_skill_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_skill_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -9,14 +9,15 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/stock')] #[Route('/stock')]
final class StockController extends AbstractController class StockController extends AbstractController
{ {
#[Route(name: 'app_stock_index', methods: ['GET'])] #[Route('/', name: 'app_stock_index', methods: ['GET'])]
public function index(StockRepository $stockRepository): Response public function index(StockRepository $stockRepository): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('stock/index.html.twig', [ return $this->render('stock/index.html.twig', [
'stocks' => $stockRepository->findAll(), 'stocks' => $stockRepository->findAll(),
]); ]);
@ -25,6 +26,8 @@ final class StockController extends AbstractController
#[Route('/new', name: 'app_stock_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_stock_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$stock = new Stock(); $stock = new Stock();
$form = $this->createForm(StockType::class, $stock); $form = $this->createForm(StockType::class, $stock);
$form->handleRequest($request); $form->handleRequest($request);
@ -33,11 +36,10 @@ final class StockController extends AbstractController
$entityManager->persist($stock); $entityManager->persist($stock);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
} }
return $this->render('stock/new.html.twig', [ return $this->render('stock/new.html.twig', [
'stock' => $stock,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -45,6 +47,7 @@ final class StockController extends AbstractController
#[Route('/{id}', name: 'app_stock_show', methods: ['GET'])] #[Route('/{id}', name: 'app_stock_show', methods: ['GET'])]
public function show(Stock $stock): Response public function show(Stock $stock): Response
{ {
$this->denyUnlessAdminOrSecretaire();
return $this->render('stock/show.html.twig', [ return $this->render('stock/show.html.twig', [
'stock' => $stock, 'stock' => $stock,
]); ]);
@ -53,10 +56,7 @@ final class StockController extends AbstractController
#[Route('/{id}/edit', name: 'app_stock_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_stock_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response public function edit(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response
{ {
// Si un chauffagiste essaie de modifier un stock d'admin $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier ce stock.');
}
$form = $this->createForm(StockType::class, $stock); $form = $this->createForm(StockType::class, $stock);
$form->handleRequest($request); $form->handleRequest($request);
@ -64,23 +64,32 @@ final class StockController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
} }
return $this->render('stock/edit.html.twig', [ return $this->render('stock/edit.html.twig', [
'stock' => $stock,
'form' => $form, 'form' => $form,
'stock' => $stock,
]); ]);
} }
#[Route('/{id}', name: 'app_stock_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_stock_delete', methods: ['POST'])]
public function delete(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response public function delete(Request $request, Stock $stock, EntityManagerInterface $entityManager): Response
{ {
if ($this->isCsrfTokenValid('delete'.$stock->getId(), $request->get('csrf_token'))) { $this->denyUnlessAdminOrSecretaire();
if ($this->isCsrfTokenValid('delete' . $stock->getId(), $request->request->get('_token'))) {
$entityManager->remove($stock); $entityManager->remove($stock);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_stock_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_stock_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -11,10 +11,11 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')] // accès global restreint
final class UserController extends AbstractController final class UserController extends AbstractController
{ {
// Route pour afficher tous les utilisateurs
#[Route('/user', name: 'app_user_index', methods: ['GET'])] #[Route('/user', name: 'app_user_index', methods: ['GET'])]
public function index(UserRepository $userRepository): Response public function index(UserRepository $userRepository): Response
{ {
@ -23,7 +24,6 @@ final class UserController extends AbstractController
]); ]);
} }
// Route pour créer un nouvel utilisateur
#[Route('/user/new', name: 'app_user_new', methods: ['GET', 'POST'])] #[Route('/user/new', name: 'app_user_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher): Response public function new(Request $request, EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher): Response
{ {
@ -32,7 +32,6 @@ final class UserController extends AbstractController
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Hash du mot de passe
$plainPassword = $form->get('plainPassword')->getData(); $plainPassword = $form->get('plainPassword')->getData();
$hashedPassword = $passwordHasher->hashPassword($user, $plainPassword); $hashedPassword = $passwordHasher->hashPassword($user, $plainPassword);
$user->setPassword($hashedPassword); $user->setPassword($hashedPassword);
@ -49,7 +48,6 @@ final class UserController extends AbstractController
]); ]);
} }
// Route pour afficher un utilisateur spécifique
#[Route('/user/{id}', name: 'app_user_show', methods: ['GET'])] #[Route('/user/{id}', name: 'app_user_show', methods: ['GET'])]
public function show(Utilisateur $user): Response public function show(Utilisateur $user): Response
{ {
@ -58,21 +56,9 @@ final class UserController extends AbstractController
]); ]);
} }
// Route pour modifier un utilisateur spécifique
#[Route('/user/{id}/edit', name: 'app_user_edit', methods: ['GET', 'POST'])] #[Route('/user/{id}/edit', name: 'app_user_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Utilisateur $user, EntityManagerInterface $entityManager): Response public function edit(Request $request, Utilisateur $user, EntityManagerInterface $entityManager): Response
{ {
if ($this->isGranted('ROLE_SECRETAIRE') && $user->hasRole('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier un autre secrétaire.');
}
if ($this->isGranted('ROLE_CHAUFFAGISTE') && ($user->hasRole('ROLE_CHAUFFAGISTE') || $user->hasRole('ROLE_SECRETAIRE'))) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier un admin.');
}
// On s'assure que seul un admin peut éditer un autre admin
$this->denyAccessUnlessGranted('ROLE_ADMIN');
$form = $this->createForm(UserType::class, $user); $form = $this->createForm(UserType::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
@ -88,28 +74,17 @@ final class UserController extends AbstractController
]); ]);
} }
// Route pour supprimer un utilisateur spécifique
#[Route('/user/{id}', name: 'app_user_delete', methods: ['POST'])] #[Route('/user/{id}', name: 'app_user_delete', methods: ['POST'])]
public function delete(Request $request, Utilisateur $user, EntityManagerInterface $entityManager): Response public function delete(Request $request, Utilisateur $utilisateur, EntityManagerInterface $entityManager): Response
{ {
// Si l'utilisateur est un secrétaire et qu'il essaie de supprimer un autre secrétaire if ($this->isCsrfTokenValid('delete' . $utilisateur->getId(), $request->request->get('_token'))) {
if ($this->isGranted('ROLE_SECRETAIRE') && $user->hasRole('ROLE_SECRETAIRE')) { $entityManager->remove($utilisateur);
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer un autre secrétaire ou un administrateur.');
}
// Si l'utilisateur est un chauffagiste et qu'il essaie de supprimer un admin
if ($this->isGranted('ROLE_CHAUFFAGISTE') && ($user->hasRole('ROLE_SECRETAIRE') || $user->hasRole('ROLE_CHAUFFAGISTE'))) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer un utilisateur.');
}
// On s'assure que seul un admin peut supprimer un autre admin
$this->denyAccessUnlessGranted("ROLE_ADMIN");
if ($this->isCsrfTokenValid('delete' . $user->getId(), $request->get('csrf_token'))) {
$entityManager->remove($user);
$entityManager->flush(); $entityManager->flush();
$this->addFlash('success', 'Utilisateur supprimé avec succès.');
} else {
$this->addFlash('error', 'Token CSRF invalide.');
} }
return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_user_index');
} }
} }

View File

@ -9,45 +9,36 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Annotation\Route;
#[Route('/vehicle')] #[Route('/vehicle')]
final class VehicleController extends AbstractController class VehicleController extends AbstractController
{ {
#[Route(name: 'app_vehicle_index', methods: ['GET'])] #[Route('/', name: 'app_vehicle_index', methods: ['GET'])]
public function index(VehicleRepository $vehicleRepository): Response public function index(VehicleRepository $vehicleRepository): Response
{ {
// Admin peut voir tous les véhicules, chauffagiste ne peut voir que ses véhicules $this->denyUnlessAdminOrSecretaire();
$vehicles = $this->isGranted('ROLE_CHAUFFAGISTE')
? $vehicleRepository->findByUser($this->getUser()) // Filtre les véhicules par utilisateur
: $vehicleRepository->findAll(); // Les admins voient tout
return $this->render('vehicle/index.html.twig', [ return $this->render('vehicle/index.html.twig', [
'vehicles' => $vehicles, 'vehicles' => $vehicleRepository->findAll(),
]); ]);
} }
#[Route('/new', name: 'app_vehicle_new', methods: ['GET', 'POST'])] #[Route('/new', name: 'app_vehicle_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response public function new(Request $request, EntityManagerInterface $entityManager): Response
{ {
$this->denyUnlessAdminOrSecretaire();
$vehicle = new Vehicle(); $vehicle = new Vehicle();
$form = $this->createForm(VehicleType::class, $vehicle); $form = $this->createForm(VehicleType::class, $vehicle);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
// Si l'utilisateur est un chauffagiste, on associe le véhicule à lui
if ($this->isGranted('ROLE_CHAUFFAGISTE')) {
$vehicle->setUser($this->getUser());
}
$entityManager->persist($vehicle); $entityManager->persist($vehicle);
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_vehicle_index');
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('vehicle/new.html.twig', [ return $this->render('vehicle/new.html.twig', [
'vehicle' => $vehicle,
'form' => $form, 'form' => $form,
]); ]);
} }
@ -55,11 +46,7 @@ final class VehicleController extends AbstractController
#[Route('/{id}', name: 'app_vehicle_show', methods: ['GET'])] #[Route('/{id}', name: 'app_vehicle_show', methods: ['GET'])]
public function show(Vehicle $vehicle): Response public function show(Vehicle $vehicle): Response
{ {
// Si l'utilisateur est un chauffagiste et essaie de voir un véhicule d'un autre chauffagiste, on bloque $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas voir ce véhicule.');
}
return $this->render('vehicle/show.html.twig', [ return $this->render('vehicle/show.html.twig', [
'vehicle' => $vehicle, 'vehicle' => $vehicle,
]); ]);
@ -68,39 +55,39 @@ final class VehicleController extends AbstractController
#[Route('/{id}/edit', name: 'app_vehicle_edit', methods: ['GET', 'POST'])] #[Route('/{id}/edit', name: 'app_vehicle_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response public function edit(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response
{ {
// Vérifier si un chauffagiste essaie de modifier un véhicule d'un autre chauffagiste $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas modifier ce véhicule.');
}
$form = $this->createForm(VehicleType::class, $vehicle); $form = $this->createForm(VehicleType::class, $vehicle);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush(); $entityManager->flush();
return $this->redirectToRoute('app_vehicle_index');
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER);
} }
return $this->render('vehicle/edit.html.twig', [ return $this->render('vehicle/edit.html.twig', [
'vehicle' => $vehicle,
'form' => $form, 'form' => $form,
'vehicle' => $vehicle,
]); ]);
} }
#[Route('/{id}', name: 'app_vehicle_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_vehicle_delete', methods: ['POST'])]
public function delete(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response public function delete(Request $request, Vehicle $vehicle, EntityManagerInterface $entityManager): Response
{ {
// Vérification de sécurité : un chauffagiste ne peut supprimer un véhicule d'un autre chauffagiste $this->denyUnlessAdminOrSecretaire();
if ($this->isGranted('ROLE_CHAUFFAGISTE') && $vehicle->getUser() !== $this->getUser()) {
throw $this->createAccessDeniedException('Vous ne pouvez pas supprimer ce véhicule.');
}
if ($this->isCsrfTokenValid('delete'.$vehicle->getId(), $request->get('csrf_token'))) { if ($this->isCsrfTokenValid('delete' . $vehicle->getId(), $request->request->get('_token'))) {
$entityManager->remove($vehicle); $entityManager->remove($vehicle);
$entityManager->flush(); $entityManager->flush();
} }
return $this->redirectToRoute('app_vehicle_index', [], Response::HTTP_SEE_OTHER); return $this->redirectToRoute('app_vehicle_index');
}
private function denyUnlessAdminOrSecretaire(): void
{
if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_SECRETAIRE')) {
throw $this->createAccessDeniedException();
}
} }
} }

View File

@ -202,6 +202,9 @@ class Intervention
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $Status = null; private ?string $Status = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $Remarque = null;
#[ORM\ManyToOne(inversedBy: 'interventions')] #[ORM\ManyToOne(inversedBy: 'interventions')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private ?Utilisateur $user = null; private ?Utilisateur $user = null;
@ -355,5 +358,15 @@ class Intervention
{ {
return $this->Wording; return $this->Wording;
} }
public function getRemarque(): ?string
{
return $this->Remarque;
}
public function setRemarque(?string $Remarque): void
{
$this->Remarque = $Remarque;
}
} }

View File

@ -7,6 +7,7 @@ use App\Entity\Intervention;
use App\Entity\Stock; use App\Entity\Stock;
use App\Entity\Utilisateur; use App\Entity\Utilisateur;
use App\Entity\Vehicle; use App\Entity\Vehicle;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
@ -28,7 +29,15 @@ class InterventionType extends AbstractType
->add('Status', TextType::class) ->add('Status', TextType::class)
->add('user', EntityType::class, [ ->add('user', EntityType::class, [
'class' => Utilisateur::class, 'class' => Utilisateur::class,
'choice_label' => 'FirstName', // ou autre (LastName, email) 'choice_label' => function (Utilisateur $user) {
return $user->getFirstName() . ' ' . $user->getLastName();
},
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('JSON_CONTAINS(u.roles, :role) = 1')
->setParameter('role', '"ROLE_CHAUFFAGISTE"');
},
'label' => 'Chauffagiste assigné',
]) ])
->add('fault', EntityType::class, [ ->add('fault', EntityType::class, [
'class' => Fault::class, 'class' => Fault::class,
@ -44,16 +53,7 @@ class InterventionType extends AbstractType
'choice_label' => 'Wording', 'choice_label' => 'Wording',
'multiple' => true, 'multiple' => true,
'expanded' => true, 'expanded' => true,
]) ]);
;
// // 👉 Sélecteur de véhicule
// ->add('vehicule', EntityType::class, [
// 'class' => Vehicule::class,
// 'choice_label' => 'immatriculation', // ou n'importe quel champ que tu veux afficher
// 'placeholder' => 'Aucun véhicule sélectionné',
// 'required' => false,
// ]);
} }
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void

31
src/Form/RemarqueType.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Intervention;
class RemarqueType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('Remarque', TextareaType::class, [
'label' => 'Ajouter une remarque',
'attr' => [
'rows' => 5,
'placeholder' => 'Renseignez les observations de lintervention…'
]
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Intervention::class,
]);
}
}

View File

@ -95,18 +95,23 @@
<ul> <ul>
{% if is_granted('ROLE_ADMIN') %} {% if is_granted('ROLE_ADMIN') %}
<li><a href="{{ path('admin_dashboard') }}">Dashboard Admin</a></li> <li><a href="{{ path('admin_dashboard') }}">Dashboard Admin</a></li>
<li><a href="{{ path('app_intervention_index') }}">Gérer les interventions</a></li>
<li><a href="{{ path('app_user_index') }}">Gérer les utilisateurs</a></li> <li><a href="{{ path('app_user_index') }}">Gérer les utilisateurs</a></li>
<li><a href="{{ path('app_vehicle_index') }}">Gérer les véhicules</a></li> <li><a href="{{ path('app_vehicle_index') }}">Gérer les véhicules</a></li>
<li><a href="{{ path('app_stock_index') }}">Gérer les stocks</a></li> <li><a href="{{ path('app_stock_index') }}">Gérer les stocks</a></li>
<li><a href="{{ path('app_fault_index') }}">Gérer les pannes</a></li> <li><a href="{{ path('app_fault_index') }}">Gérer les pannes</a></li>
<li><a href="{{ path('app_skill_index') }}">Gérer les compétences</a></li>
<li><a href="{{ path('app_calendrier_index') }}">Tous les plannings</a></li> <li><a href="{{ path('app_calendrier_index') }}">Tous les plannings</a></li>
{% endif %} {% endif %}
{% if is_granted('ROLE_SECRETAIRE') %} {% if is_granted('ROLE_SECRETAIRE') %}
<li><a href="{{ path('secretaire_dashboard') }}">Dashboard Secrétaire</a></li> <li><a href="{{ path('secretaire_dashboard') }}">Dashboard Secrétaire</a></li>
<li><a href="{{ path('app_intervention_index') }}">Gérer les interventions</a></li>
<li><a href="{{ path('app_user_index') }}">Créer un chauffagiste</a></li> <li><a href="{{ path('app_user_index') }}">Créer un chauffagiste</a></li>
<li><a href="{{ path('app_vehicle_index') }}">Gérer les véhicules</a></li> <li><a href="{{ path('app_vehicle_index') }}">Gérer les véhicules</a></li>
<li><a href="{{ path('app_stock_index') }}">Gérer les stocks</a></li> <li><a href="{{ path('app_stock_index') }}">Gérer les stocks</a></li>
<li><a href="{{ path('app_fault_index') }}">Gérer les pannes</a></li>
<li><a href="{{ path('app_skill_index') }}">Gérer les compétences</a></li>
<li><a href="{{ path('app_calendrier_indexSecretaire') }}">Plannings chauffagistes</a></li> <li><a href="{{ path('app_calendrier_indexSecretaire') }}">Plannings chauffagistes</a></li>
{% endif %} {% endif %}

View File

@ -5,38 +5,24 @@
{% block body %} {% block body %}
<h1>📅 Calendrier des interventions</h1> <h1>📅 Calendrier des interventions</h1>
<a href="{{ path('app_intervention_new') }}" class="btn btn-success mt-3"> Ajouter une intervention</a>
<div id="calendar"></div> <div id="calendar"></div>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
events: '/api/interventions' // ✅ Point daccès API locale: 'fr',
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
events: {{ events|raw }}
}); });
calendar.render(); calendar.render();
}); });
</script> </script>
{# <div id="calendar"></div>#}
{# <script>#}
{# document.addEventListener('DOMContentLoaded', function() {#}
{# var calendarEl = document.getElementById('calendar');#}
{# var calendar = new FullCalendar.Calendar(calendarEl, {#}
{# initialView: 'dayGridMonth',#}
{# events: {{ events | raw }},#}
{# eventClick: function(info) {#}
{# alert('Intervention : ' + info.event.title + '\n' + info.event.extendedProps.description);#}
{# }#}
{# });#}
{# calendar.render();#}
{# });#}
{# </script>#}
{% endblock %} {% endblock %}

View File

@ -7,15 +7,20 @@
<div id="calendar"></div> <div id="calendar"></div>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
events: {{ events | raw }}, locale: 'fr',
eventClick: function(info) { eventTimeFormat: {
alert('Intervention : ' + info.event.title + '\n' + info.event.extendedProps.description); hour: '2-digit',
} minute: '2-digit',
hour12: false
},
events: {{ events|raw }}
}); });
calendar.render(); calendar.render();
}); });

View File

@ -7,15 +7,20 @@
<div id="calendar"></div> <div id="calendar"></div>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
events: {{ events | raw }}, locale: 'fr',
eventClick: function(info) { eventTimeFormat: {
alert('Intervention : ' + info.event.title + '\n' + info.event.extendedProps.description); hour: '2-digit',
} minute: '2-digit',
hour12: false
},
events: {{ events|raw }}
}); });
calendar.render(); calendar.render();
}); });

View File

@ -1,9 +1,3 @@
{#{{ form_start(form) }}#}
{# {{ form_widget(form) }}#}
{# <button class="btn">{{ button_label|default('Save') }}</button>#}
{#{{ form_end(form) }}#}
{{ form_start(form) }} {{ form_start(form) }}
<div class="form-group"> <div class="form-group">
@ -36,12 +30,30 @@
{{ form_errors(form.Status) }} {{ form_errors(form.Status) }}
</div> </div>
{#<div class="form-group">#} <div class="form-group">
{# {{ form_label(form.vehicule, 'Véhicule associé') }}#} {{ form_label(form.fault, 'Panne') }}
{# {{ form_widget(form.vehicule, {'attr': {'class': 'form-control'}}) }}#} {{ form_widget(form.fault, {'attr': {'class': 'form-control'}}) }}
{# {{ form_errors(form.vehicule) }}#} {{ form_errors(form.fault) }}
{#</div>#} </div>
<button class="btn btn-primary mt-3">{{ button_label|default('Enregistrer') }}</button> <div class="form-group">
{{ form_label(form.user, 'Chauffagiste assigné') }}
{{ form_widget(form.user, {'attr': {'class': 'form-control'}}) }}
{{ form_errors(form.user) }}
</div>
<div class="form-group">
{{ form_label(form.vehicle, 'Véhicule') }}
{{ form_widget(form.vehicle, {'attr': {'class': 'form-control'}}) }}
{{ form_errors(form.vehicle) }}
</div>
<div class="form-group">
{{ form_label(form.stocks, 'Pièces utilisées') }}
{{ form_widget(form.stocks, {'attr': {'class': 'form-control'}}) }}
{{ form_errors(form.stocks) }}
</div>
<button class="btn btn-primary mt-4">{{ button_label|default('Enregistrer') }}</button>
{{ form_end(form) }} {{ form_end(form) }}

View File

@ -5,67 +5,7 @@
{% block body %} {% block body %}
<h1 class="mb-4"> Créer une nouvelle intervention</h1> <h1 class="mb-4"> Créer une nouvelle intervention</h1>
{#<<<<<<< HEAD#} <div class="background-intervention">
<div class="background-intervention"> {{ include('intervention/_form.html.twig') }}
{# <input required id="nom" placeholder="Nom de l'intervention">#}
{# <select required type="select" name="pannes" id="pannes">#}
{# <option value="pan1">pan1</option>#}
{# <option value="pan2">pan2</option>#}
{# <option value="pan3">pan3</option>#}
{# <option value="pan4">pan4</option>#}
{# <option value="pan5">pan5</option>#}
{# <option value="pan6">pan6</option>#}
{# </select>#}
{# <select required type="select" name="pannes" id="employesCompetences">#}
{# <option value="emp1">employé1 - sa compétence</option>#}
{# <option value="emp2">employé2 - sa compétence</option>#}
{# <option value="emp3">employé3 - sa compétence</option>#}
{# <option value="emp4">employé4 - sa compétence</option>#}
{# <option value="emp5">employé5 - sa compétence</option>#}
{# <option value="emp6">employé6 - sa compétence</option>#}
{# </select>#}
{# <!-- <input required id="employesCompetences" placeholder="Liste des employés et de leurs compétences"> -->#}
{# <select required type="select" name="piece" id="piece">#}
{# <option value="pièce1">Pièce affecter 1</option>#}
{# <option value="pièce2">Pièce affecter 2</option>#}
{# <option value="pièce3">Pièce affecter 3</option>#}
{# <option value="pièce4">Pièce affecter 4</option>#}
{# <option value="pièce5">Pièce affecter 5</option>#}
{# <option value="pièce6">Pièce affecter 6</option>#}
{# </select>#}
{# <p id="vehicule">Véhicule nécessaire</p>#}
{# <form>#}
{# <label id="oui">#}
{# <input type="radio" name="choix" value="option1">#}
{# Oui </label>#}
{# <label id="non">#}
{# <input type="radio" name="choix" value="option2">#}
{# Non </label>#}
{# </form>#}
{# <p id="date">Jour de l'intervention</p>#}
{# <input type="date" id="calendar">#}
{# <input type="text" id="description" placeholder="Description">#}
{# <input type="text" id="adresse" placeholder="Adresse">#}
{# <button type="submit" class="applique"> Appliquée </button>#}
{# <button type="reset" class="supprimer"> Supprimer </button>#}
{{ include('intervention/_form.html.twig') }}
</div>
</body>
</html>
{#=======#}
<div class="mt-3">
<a href="{{ path('app_intervention_index') }}" class="btn btn-secondary">← Retour à la liste des interventions</a>
</div>
<div class="mt-3">
<a href="{{ path('app_calendrier_index') }}" class="btn btn-secondary">← Retour au calendrier</a>
</div> </div>
{% endblock %} {% endblock %}
{#>>>>>>> 4fc91211f0d814453d2ed97caf6a1d94d709058e#}

View File

@ -0,0 +1,18 @@
{% extends 'base.html.twig' %}
{% block title %}Ajouter une remarque{% endblock %}
{% block body %}
<h1 class="mb-4">📝 Ajouter une remarque à l'intervention #{{ intervention.id }}</h1>
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn btn-primary mt-3">Enregistrer la remarque</button>
{{ form_end(form) }}
<div class="mt-3">
<a href="{{ path('app_intervention_show', {'id': intervention.id}) }}" class="btn btn-secondary">
← Retour à l'intervention
</a>
</div>
{% endblock %}

View File

@ -63,6 +63,11 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if is_granted('ROLE_CHAUFFAGISTE') and intervention.user == app.user %}
<a href="{{ path('app_intervention_remarque', {'id': intervention.id}) }}" class="btn btn-outline-primary">
📝 Ajouter une remarque
</a>
{% endif %}
</tbody> </tbody>
</table> </table>