version 1
This commit is contained in:
30
_projet/priorités.txt
Normal file
30
_projet/priorités.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
- ajouter bootstrap en mettant des liens vers les cdn CSS et JS (dans templates/base.html.twig)
|
||||
- creer une vue qui détaille le contenue d'un projet :
|
||||
- liste des contribs, avec sur usage de l'IA ou non
|
||||
- possibilité de trier cette liste (sur le nom du dev, la date de la contrib...)
|
||||
- afficher "en plus" :
|
||||
- la durée totale des contribs (somme des durées)
|
||||
- la liste des développeurs ayant travaillé sur le projet
|
||||
- ajout, modif, supression d'un projet :
|
||||
- on reste sur la page qui affiche la liste des projets
|
||||
- les formulaires sont dans des modeles
|
||||
- attention : refuser de supprimer un projet s'il contient des contributions
|
||||
=> pour l'ensemble : bootstrap, propre et lisible
|
||||
|
||||
- Suppression de projet non vide : possible avec avertissement (perte de données des contributions)
|
||||
- Sur la page détail d'un projet :
|
||||
- pouvoir ajouter une contribution (qui, avec assistant ou non)
|
||||
- pouvoir modifier une contribution :
|
||||
- modifier la durée ou ajouter/supprimer un assistant IA :
|
||||
- modif la durée : directement dans le tableau vers un "verrou"
|
||||
- ajouter:supprimer assistant : directement dans le tableau
|
||||
- supprimer une contribution : avec confirmation
|
||||
- ajouter le temps passé par chaque dev sur le projet
|
||||
|
||||
- Accueil : dashboard => voir les infos proposées par l'IA
|
||||
|
||||
- Ajout d'une navbar des projets, page accueil-dashboard, page de gestion des utilisateurs,
|
||||
page de gestion des assistant IA
|
||||
|
||||
- Ajout d'un bouton d'aide (navbar ou autre) : affiche une modale qui explique brièvement le fonctionnement de la vue
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
"php": ">=8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"doctrine/dbal": "^3",
|
||||
"doctrine/dbal": "^3.10.3",
|
||||
"doctrine/doctrine-bundle": "^2.18",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.5",
|
||||
"doctrine/orm": "^3.5",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"doctrine/orm": "^3.5.2",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.3",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/asset-mapper": "7.3.*",
|
||||
@@ -19,14 +19,14 @@
|
||||
"symfony/doctrine-messenger": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/flex": "^2.8.2",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/intl": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/mime": "7.3.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/notifier": "7.3.*",
|
||||
"symfony/process": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
@@ -34,16 +34,16 @@
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/stimulus-bundle": "^2.30",
|
||||
"symfony/stimulus-bundle": "^2.31",
|
||||
"symfony/string": "7.3.*",
|
||||
"symfony/translation": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-turbo": "^2.30",
|
||||
"symfony/ux-turbo": "^2.31",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/web-link": "7.3.*",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
"twig/extra-bundle": "^2.12|^3.21",
|
||||
"twig/twig": "^2.12|^3.21.1"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
@@ -97,11 +97,11 @@
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
"phpunit/phpunit": "^12.4.1",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/debug-bundle": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/maker-bundle": "^1.64",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*"
|
||||
}
|
||||
|
||||
4447
composer.lock
generated
4447
composer.lock
generated
File diff suppressed because it is too large
Load Diff
88
src/Controller/AssistantIaController.php
Normal file
88
src/Controller/AssistantIaController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AssistantIaRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class AssistantIaController extends AbstractController
|
||||
{
|
||||
#[Route('/assistants-ia/{id}/detail', name: 'assistant_ia_detail')]
|
||||
public function detail(int $id, AssistantIaRepository $repo): Response
|
||||
{
|
||||
$assistant = $repo->find($id);
|
||||
if (!$assistant) {
|
||||
$this->addFlash('danger', 'Assistant IA introuvable.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
return $this->render('assistant_ia/detail.html.twig', [
|
||||
'assistant' => $assistant,
|
||||
]);
|
||||
}
|
||||
#[Route('/assistants-ia', name: 'assistant_ia_index')]
|
||||
public function index(AssistantIaRepository $assistantIaRepository): Response
|
||||
{
|
||||
$assistants = $assistantIaRepository->findAll();
|
||||
return $this->render('assistant_ia/index.html.twig', [
|
||||
'assistants' => $assistants,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/assistants-ia/add', name: 'assistant_ia_add', methods: ['POST'])]
|
||||
public function add(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$nom = $request->request->get('nom');
|
||||
if (!$nom) {
|
||||
$this->addFlash('danger', 'Le nom est obligatoire.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
$assistant = new \App\Entity\AssistantIa();
|
||||
$assistant->setNom($nom);
|
||||
$em->persist($assistant);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Assistant IA ajouté avec succès.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
|
||||
#[Route('/assistants-ia/edit/{id}', name: 'assistant_ia_edit', methods: ['POST'])]
|
||||
public function edit(int $id, Request $request, EntityManagerInterface $em, AssistantIaRepository $repo): Response
|
||||
{
|
||||
$assistant = $repo->find($id);
|
||||
if (!$assistant) {
|
||||
$this->addFlash('danger', 'Assistant IA introuvable.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
$nom = $request->request->get('nom');
|
||||
if (!$nom) {
|
||||
$this->addFlash('danger', 'Le nom est obligatoire.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
$assistant->setNom($nom);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Assistant IA modifié avec succès.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
|
||||
#[Route('/assistants-ia/delete/{id}', name: 'assistant_ia_delete', methods: ['POST'])]
|
||||
public function delete(int $id, Request $request, EntityManagerInterface $em, AssistantIaRepository $repo): Response
|
||||
{
|
||||
$assistant = $repo->find($id);
|
||||
if (!$assistant) {
|
||||
$this->addFlash('danger', 'Assistant IA introuvable.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
$token = $request->request->get('_token');
|
||||
if (!$this->isCsrfTokenValid('delete_assistant_' . $assistant->getId(), $token)) {
|
||||
$this->addFlash('danger', 'Token CSRF invalide.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
$em->remove($assistant);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Assistant IA supprimé avec succès.');
|
||||
return $this->redirectToRoute('assistant_ia_index');
|
||||
}
|
||||
}
|
||||
97
src/Controller/MembreController.php
Normal file
97
src/Controller/MembreController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\MembreRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class MembreController extends AbstractController
|
||||
{
|
||||
#[Route('/utilisateurs/{id}/detail', name: 'membre_detail')]
|
||||
public function detail(int $id, MembreRepository $repo): Response
|
||||
{
|
||||
$membre = $repo->find($id);
|
||||
if (!$membre) {
|
||||
$this->addFlash('danger', 'Utilisateur introuvable.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
return $this->render('membre/detail.html.twig', [
|
||||
'membre' => $membre,
|
||||
]);
|
||||
}
|
||||
#[Route('/utilisateurs', name: 'membre_index')]
|
||||
public function index(MembreRepository $membreRepository): Response
|
||||
{
|
||||
$membres = $membreRepository->findAll();
|
||||
return $this->render('membre/index.html.twig', [
|
||||
'membres' => $membres,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/utilisateurs/add', name: 'membre_add', methods: ['POST'])]
|
||||
public function add(Request $request, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
$nom = $request->request->get('nom');
|
||||
$prenom = $request->request->get('prenom');
|
||||
$email = $request->request->get('email');
|
||||
if (!$nom || !$prenom || !$email) {
|
||||
$this->addFlash('danger', 'Tous les champs sont obligatoires.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
$membre = new \App\Entity\Membre();
|
||||
$membre->setNom($nom);
|
||||
$membre->setPrenom($prenom);
|
||||
$membre->setEmail($email);
|
||||
$em->persist($membre);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Utilisateur ajouté avec succès.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
|
||||
#[Route('/utilisateurs/edit/{id}', name: 'membre_edit', methods: ['POST'])]
|
||||
public function edit(int $id, Request $request, EntityManagerInterface $em, MembreRepository $repo): RedirectResponse
|
||||
{
|
||||
$membre = $repo->find($id);
|
||||
if (!$membre) {
|
||||
$this->addFlash('danger', 'Utilisateur introuvable.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
$nom = $request->request->get('nom');
|
||||
$prenom = $request->request->get('prenom');
|
||||
$email = $request->request->get('email');
|
||||
if (!$nom || !$prenom || !$email) {
|
||||
$this->addFlash('danger', 'Tous les champs sont obligatoires.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
$membre->setNom($nom);
|
||||
$membre->setPrenom($prenom);
|
||||
$membre->setEmail($email);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Utilisateur modifié avec succès.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
|
||||
#[Route('/utilisateurs/delete/{id}', name: 'membre_delete', methods: ['POST'])]
|
||||
public function delete(int $id, Request $request, EntityManagerInterface $em, MembreRepository $repo): RedirectResponse
|
||||
{
|
||||
$membre = $repo->find($id);
|
||||
if (!$membre) {
|
||||
$this->addFlash('danger', 'Utilisateur introuvable.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
$token = $request->request->get('_token');
|
||||
if (!$this->isCsrfTokenValid('delete_membre_' . $membre->getId(), $token)) {
|
||||
$this->addFlash('danger', 'Token CSRF invalide.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
$em->remove($membre);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Utilisateur supprimé avec succès.');
|
||||
return $this->redirectToRoute('membre_index');
|
||||
}
|
||||
}
|
||||
303
src/Controller/ProjetController.php
Normal file
303
src/Controller/ProjetController.php
Normal file
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Projet;
|
||||
use App\Repository\ProjetRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use App\Entity\Contribution;
|
||||
use App\Entity\Membre;
|
||||
use App\Entity\AssistantIa;
|
||||
use App\Entity\ContribIa;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class ProjetController extends AbstractController
|
||||
{
|
||||
#[Route('/projets/add', name: 'projet_add', methods: ['POST'])]
|
||||
public function add(Request $request, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('projet_add', $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
|
||||
$projet = new Projet();
|
||||
$projet->setNom($request->request->get('nom'));
|
||||
$projet->setCommentaire($request->request->get('commentaire'));
|
||||
$projet->setStatut($request->request->get('statut', Projet::STATUT_EN_ATTENTE));
|
||||
|
||||
$dateLancement = $request->request->get('dateLancement');
|
||||
if ($dateLancement) {
|
||||
try {
|
||||
$projet->setDateLancement(new \DateTime($dateLancement));
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
$dateCloture = $request->request->get('dateCloture');
|
||||
if ($dateCloture) {
|
||||
try {
|
||||
$projet->setDateCloture(new \DateTime($dateCloture));
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
$em->persist($projet);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Projet ajouté avec succès.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
#[Route('/projets', name: 'projet_index')]
|
||||
public function index(ProjetRepository $projetRepository): Response
|
||||
{
|
||||
$projets = $projetRepository->findAll();
|
||||
return $this->render('projet/index.html.twig', [
|
||||
'projets' => $projets,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/projets/edit/{id}', name: 'projet_edit', methods: ['POST'])]
|
||||
public function edit(int $id, Request $request, EntityManagerInterface $em, ProjetRepository $repo): RedirectResponse
|
||||
{
|
||||
$projet = $repo->find($id);
|
||||
if (!$projet) {
|
||||
$this->addFlash('danger', 'Projet introuvable.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
if (!$this->isCsrfTokenValid('edit_projet_' . $projet->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
$projet->setNom($request->request->get('nom'));
|
||||
$projet->setCommentaire($request->request->get('commentaire'));
|
||||
$projet->setStatut($request->request->get('statut', $projet->getStatut()));
|
||||
$dateLancement = $request->request->get('dateLancement');
|
||||
if ($dateLancement) {
|
||||
try {
|
||||
$projet->setDateLancement(new \DateTime($dateLancement));
|
||||
} catch (\Exception $e) {}
|
||||
} else {
|
||||
$projet->setDateLancement(null);
|
||||
}
|
||||
$dateCloture = $request->request->get('dateCloture');
|
||||
if ($dateCloture) {
|
||||
try {
|
||||
$projet->setDateCloture(new \DateTime($dateCloture));
|
||||
} catch (\Exception $e) {}
|
||||
} else {
|
||||
$projet->setDateCloture(null);
|
||||
}
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Projet modifié avec succès.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
|
||||
#[Route('/projets/{id}', name: 'projet_show', requirements: ['id' => '\\d+'])]
|
||||
public function show(Projet $projet, Request $request, ManagerRegistry $doctrine): Response
|
||||
{
|
||||
// tri sur la liste des contributions
|
||||
$sort = $request->query->get('sort', 'date');
|
||||
$direction = strtolower($request->query->get('direction', 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$contribs = $projet->getContributions()->toArray();
|
||||
|
||||
usort($contribs, function ($a, $b) use ($sort, $direction) {
|
||||
switch ($sort) {
|
||||
case 'membre':
|
||||
$va = (string) $a->getMembre();
|
||||
$vb = (string) $b->getMembre();
|
||||
break;
|
||||
case 'duree':
|
||||
$va = $a->getDuree();
|
||||
$vb = $b->getDuree();
|
||||
break;
|
||||
case 'date':
|
||||
default:
|
||||
$va = $a->getDateContribution() ? $a->getDateContribution()->getTimestamp() : 0;
|
||||
$vb = $b->getDateContribution() ? $b->getDateContribution()->getTimestamp() : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($va === $vb) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($direction === 'asc') {
|
||||
return ($va < $vb) ? -1 : 1;
|
||||
}
|
||||
|
||||
return ($va > $vb) ? -1 : 1;
|
||||
});
|
||||
|
||||
// calculs complémentaires
|
||||
$totalDuree = array_reduce($contribs, function ($carry, $item) {
|
||||
return $carry + (int) $item->getDuree();
|
||||
}, 0);
|
||||
|
||||
$devs = [];
|
||||
foreach ($contribs as $c) {
|
||||
$m = $c->getMembre();
|
||||
if ($m) {
|
||||
$devs[$m->getId()] = (string) $m;
|
||||
}
|
||||
}
|
||||
// récupérer membres et assistants pour les formulaires
|
||||
$membres = $doctrine->getRepository(Membre::class)->findAll();
|
||||
$assistants = $doctrine->getRepository(AssistantIa::class)->findAll();
|
||||
|
||||
return $this->render('projet/show.html.twig', [
|
||||
'projet' => $projet,
|
||||
'contribs' => $contribs,
|
||||
'sort' => $sort,
|
||||
'direction' => $direction,
|
||||
'totalDuree' => $totalDuree,
|
||||
'developpeurs' => array_values($devs),
|
||||
'membres' => $membres,
|
||||
'assistants' => $assistants,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/projets/{id}/delete', name: 'projet_delete', methods: ['POST'])]
|
||||
public function delete(Request $request, Projet $projet, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('delete'.$projet->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
$force = (bool) $request->request->get('_force');
|
||||
|
||||
if ($projet->getContributions()->count() > 0 && !$force) {
|
||||
// Le front peut afficher une confirmation et renvoyer _force=1
|
||||
$this->addFlash('warning', 'Le projet contient des contributions. Confirmez la suppression pour supprimer également les contributions (perte de données).');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
|
||||
$em->remove($projet);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Projet supprimé avec succès.');
|
||||
return $this->redirectToRoute('projet_index');
|
||||
}
|
||||
|
||||
#[Route('/projets/{id}/contrib/add', name: 'contrib_add', methods: ['POST'])]
|
||||
public function addContribution(Request $request, Projet $projet, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('contrib_add'.$projet->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $projet->getId()]);
|
||||
}
|
||||
|
||||
$membreId = $request->request->get('membre');
|
||||
$assistantId = $request->request->get('assistant');
|
||||
$date = $request->request->get('dateContribution');
|
||||
$duree = (int) $request->request->get('duree');
|
||||
$commentaire = $request->request->get('commentaire');
|
||||
|
||||
$membre = $em->getRepository(Membre::class)->find($membreId);
|
||||
if (!$membre) {
|
||||
$this->addFlash('danger', 'Membre invalide.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $projet->getId()]);
|
||||
}
|
||||
|
||||
$contrib = new Contribution();
|
||||
$contrib->setMembre($membre);
|
||||
$contrib->setProjet($projet);
|
||||
$contrib->setDuree($duree);
|
||||
if ($date) {
|
||||
try {
|
||||
$contrib->setDateContribution(new \DateTime($date));
|
||||
} catch (\Exception $e) {
|
||||
// ignore invalid date, laisser la valeur par défaut
|
||||
}
|
||||
}
|
||||
$contrib->setCommentaire($commentaire);
|
||||
|
||||
$em->persist($contrib);
|
||||
|
||||
if ($assistantId) {
|
||||
$assistant = $em->getRepository(AssistantIa::class)->find($assistantId);
|
||||
if ($assistant) {
|
||||
$contribIa = new ContribIa();
|
||||
$contribIa->setAssistantIa($assistant);
|
||||
$contribIa->setContribution($contrib);
|
||||
$em->persist($contribIa);
|
||||
}
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Contribution ajoutée.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $projet->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/contrib/{id}/edit', name: 'contrib_edit', methods: ['POST'])]
|
||||
public function editContribution(Request $request, Contribution $contrib, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('contrib_edit'.$contrib->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $contrib->getProjet()->getId()]);
|
||||
}
|
||||
|
||||
$duree = (int) $request->request->get('duree');
|
||||
$assistantId = $request->request->get('assistant');
|
||||
$commentaire = $request->request->get('commentaire');
|
||||
|
||||
$contrib->setDuree($duree);
|
||||
$contrib->setCommentaire($commentaire);
|
||||
|
||||
// supprimer les ContribIa existantes
|
||||
foreach ($contrib->getContribIas() as $ci) {
|
||||
$em->remove($ci);
|
||||
}
|
||||
|
||||
if ($assistantId) {
|
||||
$assistant = $em->getRepository(AssistantIa::class)->find($assistantId);
|
||||
if ($assistant) {
|
||||
$contribIa = new ContribIa();
|
||||
$contribIa->setAssistantIa($assistant);
|
||||
$contribIa->setContribution($contrib);
|
||||
$em->persist($contribIa);
|
||||
}
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Contribution modifiée.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $contrib->getProjet()->getId()]);
|
||||
}
|
||||
|
||||
#[Route('/contrib/{id}/delete', name: 'contrib_delete', methods: ['POST'])]
|
||||
public function deleteContribution(Request $request, Contribution $contrib, EntityManagerInterface $em): RedirectResponse
|
||||
{
|
||||
if (!$this->isCsrfTokenValid('contrib_delete'.$contrib->getId(), $request->request->get('_token'))) {
|
||||
$this->addFlash('danger', 'Jeton CSRF invalide.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $contrib->getProjet()->getId()]);
|
||||
}
|
||||
|
||||
$projectId = $contrib->getProjet()->getId();
|
||||
$em->remove($contrib);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Contribution supprimée.');
|
||||
return $this->redirectToRoute('projet_show', ['id' => $projectId]);
|
||||
}
|
||||
|
||||
#[Route('/dashboard', name: 'dashboard')]
|
||||
public function dashboard(ProjetRepository $projetRepo, \App\Repository\MembreRepository $membreRepo, \App\Repository\AssistantIaRepository $assistantRepo, \App\Repository\ContributionRepository $contribRepo): Response
|
||||
{
|
||||
$nbProjets = $projetRepo->count([]);
|
||||
$nbMembres = $membreRepo->count([]);
|
||||
$nbAssistants = $assistantRepo->count([]);
|
||||
$nbContributions = $contribRepo->count([]);
|
||||
$recentContribs = $contribRepo->findBy([], ['id' => 'DESC'], 5);
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'nbProjets' => $nbProjets,
|
||||
'nbMembres' => $nbMembres,
|
||||
'nbAssistants' => $nbAssistants,
|
||||
'nbContributions' => $nbContributions,
|
||||
'recentContribs' => $recentContribs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,6 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ContribIaRepository::class)]
|
||||
#[ORM\Table(name: 'contrib_ia')]
|
||||
#[ORM\CheckConstraints([
|
||||
new ORM\CheckConstraint(
|
||||
name: 'check_evaluation_pertinence',
|
||||
expression: 'evaluation_pertinence >= 1 AND evaluation_pertinence <= 5'
|
||||
),
|
||||
new ORM\CheckConstraint(
|
||||
name: 'check_evaluation_temps',
|
||||
expression: 'evaluation_temps >= 1 AND evaluation_temps <= 5'
|
||||
),
|
||||
])]
|
||||
class ContribIa
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
||||
25
src/Repository/AssistantIaRepository.php
Normal file
25
src/Repository/AssistantIaRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
// Méthodes personnalisées si besoin
|
||||
}
|
||||
25
src/Repository/ContribIaRepository.php
Normal file
25
src/Repository/ContribIaRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
// Méthodes personnalisées si besoin
|
||||
}
|
||||
25
src/Repository/ContributionRepository.php
Normal file
25
src/Repository/ContributionRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
// Méthodes personnalisées si besoin
|
||||
}
|
||||
25
src/Repository/MembreRepository.php
Normal file
25
src/Repository/MembreRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
// Méthodes personnalisées si besoin
|
||||
}
|
||||
25
src/Repository/ProjetRepository.php
Normal file
25
src/Repository/ProjetRepository.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
// Ajoute ici des méthodes de recherche personnalisées si besoin
|
||||
}
|
||||
25
templates/assistant_ia/detail.html.twig
Normal file
25
templates/assistant_ia/detail.html.twig
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block title %}Détails Assistant IA{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container py-4">
|
||||
<h1 class="mb-4">Détails de l'assistant IA</h1>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ assistant.nom }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mt-4">Contributions IA</h3>
|
||||
<ul class="list-group mb-4">
|
||||
{% for contribIa in assistant.contribIas %}
|
||||
<li class="list-group-item">
|
||||
<strong>Projet :</strong> {{ contribIa.contribution.projet.nom }}<br>
|
||||
<strong>Utilisateur :</strong> {{ contribIa.contribution.membre.nom }} {{ contribIa.contribution.membre.prenom }}<br>
|
||||
<strong>Commentaire :</strong> {{ contribIa.commentaire }}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="list-group-item text-muted">Aucune contribution IA liée.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{{ path('assistant_ia_index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Retour</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
90
templates/assistant_ia/index.html.twig
Normal file
90
templates/assistant_ia/index.html.twig
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block title %}Assistants IA{% endblock %}
|
||||
{% block body %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="display-5 fw-bold mb-0">Assistants IA</h1>
|
||||
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addAssistantModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> Ajouter un assistant IA
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive shadow-sm rounded">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for assistant in assistants %}
|
||||
<tr>
|
||||
<td><span class="fw-semibold text-primary">{{ assistant.nom }}</span></td>
|
||||
<td>
|
||||
<a href="{{ path('assistant_ia_detail', {id: assistant.id}) }}" class="btn btn-sm btn-info me-1">
|
||||
<i class="bi bi-eye"></i> Détails
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editAssistantModal{{ assistant.id }}">
|
||||
<i class="bi bi-pencil"></i> Modifier
|
||||
</button>
|
||||
<form method="post" action="{{ path('assistant_ia_delete', {id: assistant.id}) }}" style="display:inline-block" onsubmit="return confirm('Supprimer cet assistant IA ?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete_assistant_ia_' ~ assistant.id) }}">
|
||||
<button class="btn btn-sm btn-danger"><i class="bi bi-trash"></i> Supprimer</button>
|
||||
</form>
|
||||
<div class="modal fade" id="editAssistantModal{{ assistant.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('assistant_ia_edit', {id: assistant.id}) }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifier l'assistant IA</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" value="{{ assistant.nom }}" required maxlength="50">
|
||||
</div>
|
||||
{# Le champ 'type' a été supprimé car il n'existe pas dans l'entité #}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Aucun assistant IA trouvé.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Modal ajout assistant IA #}
|
||||
<div class="modal fade" id="addAssistantModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('assistant_ia_add') }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter un assistant IA</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" required maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,13 +5,76 @@
|
||||
<title>{% block title %}Welcome!{% 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>">
|
||||
{% block stylesheets %}
|
||||
{# Bootstrap CSS via CDN (par défaut) - les templates enfants peuvent ajouter leurs propres styles ici #}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="" crossorigin="anonymous">
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{# Importmap (si utilisé) placé dans le head par défaut #}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
{# Navbar global #}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ path('projet_index') }}">ContribV2</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ path('projet_index') }}">Projets</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ path('dashboard') }}">Dashboard</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ path('membre_index') }}">Utilisateurs</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ path('assistant_ia_index') }}">Assistants IA</a></li>
|
||||
</ul>
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#helpModal">Aide</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container my-4">
|
||||
{# messages flash #}
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ label == 'danger' ? 'danger' : (label == 'warning' ? 'warning' : (label == 'success' ? 'success' : 'info')) }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{# Modale d'aide globale #}
|
||||
<div class="modal fade" id="helpModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Aide</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Cette application permet de gérer des projets et leurs contributions. Depuis la liste des projets vous pouvez accéder aux détails d'un projet, ajouter/modifier/supprimer des contributions, et gérer les assistants IA.</p>
|
||||
<ul>
|
||||
<li>Sur la page projet : ajouter des contributions, indiquer si une contribution a utilisé un assistant IA.</li>
|
||||
<li>Suppression de projet : si le projet contient des contributions, une confirmation supplémentaire est requise.</li>
|
||||
<li>Utilisez les boutons « Détails », « Modifier » et « Supprimer » pour gérer les éléments.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Scripts JS à la fin du body pour de meilleures performances. Les templates enfants peuvent étendre ce bloc. #}
|
||||
{% block bottom_javascripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="" crossorigin="anonymous"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
90
templates/dashboard/index.html.twig
Normal file
90
templates/dashboard/index.html.twig
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="display-5 fw-bold mb-0">Dashboard</h1>
|
||||
<span class="badge rounded-pill bg-primary fs-5 px-3 py-2 shadow">Vue synthétique</span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info shadow-sm mb-4">
|
||||
<h5 class="mb-2"><i class="bi bi-robot me-2"></i>Suggestions IA</h5>
|
||||
<ul class="mb-0 ps-3">
|
||||
<li>Vous avez <strong>{{ nbProjets }}</strong> projet{{ nbProjets > 1 ? 's' : '' }}, <strong>{{ nbMembres }}</strong> utilisateur{{ nbMembres > 1 ? 's' : '' }}, <strong>{{ nbAssistants }}</strong> assistant{{ nbAssistants > 1 ? 's IA' : ' IA' }}, et <strong>{{ nbContributions }}</strong> contribution{{ nbContributions > 1 ? 's' : '' }}.</li>
|
||||
<li>Surveillez les activités récentes pour suivre l’avancement et l’implication des membres.</li>
|
||||
<li>Relancez les membres ou clôturez les projets inactifs si besoin.</li>
|
||||
<li>Utilisez les tris et filtres pour analyser la répartition des contributions.</li>
|
||||
<li>Besoin d’aide ? <span class="text-primary">Cliquez sur « Aide » dans la barre de navigation.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow h-100 text-bg-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-kanban-fill fs-1 mb-2"></i>
|
||||
<h5 class="card-title">Projets</h5>
|
||||
<p class="card-text display-6 fw-bold">{{ nbProjets }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow h-100 text-bg-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-people-fill fs-1 mb-2"></i>
|
||||
<h5 class="card-title">Utilisateurs</h5>
|
||||
<p class="card-text display-6 fw-bold">{{ nbMembres }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow h-100 text-bg-info">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-cpu-fill fs-1 mb-2"></i>
|
||||
<h5 class="card-title">Assistants IA</h5>
|
||||
<p class="card-text display-6 fw-bold">{{ nbAssistants }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow h-100 text-bg-warning">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-lightbulb-fill fs-1 mb-2"></i>
|
||||
<h5 class="card-title">Contributions</h5>
|
||||
<p class="card-text display-6 fw-bold">{{ nbContributions }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="h4 mt-4 mb-3"><i class="bi bi-clock-history me-2"></i>Activités récentes</h2>
|
||||
<div class="table-responsive shadow-sm rounded">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="display:none;">ID</th>
|
||||
<th>Projet</th>
|
||||
<th>Membre</th>
|
||||
<th>Durée</th>
|
||||
<th>Date</th>
|
||||
<th>Commentaire</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contrib in recentContribs %}
|
||||
<tr>
|
||||
<td style="display:none;"><input type="hidden" value="{{ contrib.id }}" /></td>
|
||||
<td><span class="fw-semibold text-primary">{{ contrib.projet.nom }}</span></td>
|
||||
<td>{{ contrib.membre ? '<span class="badge bg-secondary">' ~ contrib.membre.nom ~ ' ' ~ contrib.membre.prenom ~ '</span>' : '' }}</td>
|
||||
<td><span class="badge bg-info text-dark">{{ contrib.duree }}</span></td>
|
||||
<td>{{ contrib.dateContribution ? contrib.dateContribution|date('d/m/Y') : '' }}</td>
|
||||
<td>{{ contrib.commentaire }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-center text-muted">Aucune activité récente.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/membre/detail.html.twig
Normal file
32
templates/membre/detail.html.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block title %}Détails utilisateur{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container py-4">
|
||||
<h1 class="mb-4">Détails de l'utilisateur</h1>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ membre.nom }} {{ membre.prenom }}</h5>
|
||||
<p class="card-text"><strong>Email :</strong> {{ membre.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mt-4">Contributions</h3>
|
||||
<ul class="list-group mb-4">
|
||||
{% for contribution in membre.contributions %}
|
||||
<li class="list-group-item">
|
||||
<strong>Projet :</strong> {{ contribution.projet.nom }}<br>
|
||||
{% set iaList = [] %}
|
||||
{% for contribIa in contribution.contribIas %}
|
||||
{% if contribIa.assistantIa is not null %}
|
||||
{% set iaList = iaList|merge([contribIa.assistantIa.nom]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<strong>Assistants IA :</strong> {{ iaList|join(', ') }}<br>
|
||||
<strong>Description :</strong> {{ contribution.commentaire }}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="list-group-item text-muted">Aucune contribution liée.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{{ path('membre_index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Retour</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
109
templates/membre/index.html.twig
Normal file
109
templates/membre/index.html.twig
Normal file
@@ -0,0 +1,109 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block title %}Utilisateurs{% endblock %}
|
||||
{% block body %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="display-5 fw-bold mb-0">Utilisateurs</h1>
|
||||
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addMembreModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> Ajouter un utilisateur
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive shadow-sm rounded">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for membre in membres %}
|
||||
<tr>
|
||||
<td><span class="fw-semibold text-primary">{{ membre.nom }}</span></td>
|
||||
<td>{{ membre.prenom }}</td>
|
||||
<td>{{ membre.email }}</td>
|
||||
<td>
|
||||
<a href="{{ path('membre_detail', {id: membre.id}) }}" class="btn btn-sm btn-info me-1">
|
||||
<i class="bi bi-eye"></i> Détails
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editMembreModal{{ membre.id }}">
|
||||
<i class="bi bi-pencil"></i> Modifier
|
||||
</button>
|
||||
<form method="post" action="{{ path('membre_delete', {id: membre.id}) }}" style="display:inline-block" onsubmit="return confirm('Supprimer cet utilisateur ?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete_membre_' ~ membre.id) }}">
|
||||
<button class="btn btn-sm btn-danger"><i class="bi bi-trash"></i> Supprimer</button>
|
||||
</form>
|
||||
<div class="modal fade" id="editMembreModal{{ membre.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('membre_edit', {id: membre.id}) }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifier l'utilisateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" value="{{ membre.nom }}" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Prénom</label>
|
||||
<input type="text" name="prenom" class="form-control" value="{{ membre.prenom }}" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ membre.email }}" required maxlength="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Aucun utilisateur trouvé.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Modal ajout utilisateur #}
|
||||
<div class="modal fade" id="addMembreModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('membre_add') }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter un utilisateur</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Prénom</label>
|
||||
<input type="text" name="prenom" class="form-control" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" required maxlength="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
171
templates/projet/index.html.twig
Normal file
171
templates/projet/index.html.twig
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Liste des projets{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="display-5 fw-bold mb-0">Projets</h1>
|
||||
<button class="btn btn-primary btn-lg shadow" data-bs-toggle="modal" data-bs-target="#addProjetModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> Nouveau projet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive shadow-sm rounded">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Commentaire</th>
|
||||
<th>Date de lancement</th>
|
||||
<th>Date de clôture</th>
|
||||
<th>Statut</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for projet in projets %}
|
||||
<tr>
|
||||
<td><span class="fw-semibold text-primary">{{ projet.nom }}</span></td>
|
||||
<td>{{ projet.commentaire }}</td>
|
||||
<td>{{ projet.dateLancement ? projet.dateLancement|date('d/m/Y') : '' }}</td>
|
||||
<td>{{ projet.dateCloture ? projet.dateCloture|date('d/m/Y') : '' }}</td>
|
||||
<td><span class="badge bg-info text-dark">{{ projet.statut }}</span></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary me-1" href="{{ path('projet_show', {id: projet.id}) }}">
|
||||
<i class="bi bi-eye"></i> Détails
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-toggle="modal" data-bs-target="#editProjetModal{{ projet.id }}">
|
||||
<i class="bi bi-pencil"></i> Modifier
|
||||
</button>
|
||||
<form method="post" action="{{ path('projet_delete', {id: projet.id}) }}" style="display:inline-block" data-contrib-count="{{ projet.contributions|length }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ projet.id) }}">
|
||||
<input type="hidden" name="_force" value="0">
|
||||
<button class="btn btn-sm btn-danger btn-delete-projet"><i class="bi bi-trash"></i> Supprimer</button>
|
||||
</form>
|
||||
<div class="modal fade" id="editProjetModal{{ projet.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('projet_edit', {id: projet.id}) }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('edit_projet_' ~ projet.id) }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifier le projet</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" value="{{ projet.nom }}" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Commentaire</label>
|
||||
<textarea name="commentaire" class="form-control">{{ projet.commentaire }}</textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Date de lancement</label>
|
||||
<input type="date" name="dateLancement" class="form-control" value="{{ projet.dateLancement ? projet.dateLancement|date('Y-m-d') : '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Date de clôture</label>
|
||||
<input type="date" name="dateCloture" class="form-control" value="{{ projet.dateCloture ? projet.dateCloture|date('Y-m-d') : '' }}">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Statut</label>
|
||||
<select name="statut" class="form-select" required>
|
||||
<option value="en_attente" {% if projet.statut == 'en_attente' %}selected{% endif %}>En attente</option>
|
||||
<option value="en_cours" {% if projet.statut == 'en_cours' %}selected{% endif %}>En cours</option>
|
||||
<option value="termine" {% if projet.statut == 'termine' %}selected{% endif %}>Terminé</option>
|
||||
<option value="annule" {% if projet.statut == 'annule' %}selected{% endif %}>Annulé</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Aucun projet trouvé.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block bottom_javascripts %}
|
||||
{{ parent() }}
|
||||
<!-- Modal ajout projet -->
|
||||
<div class="modal fade" id="addProjetModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ path('projet_add') }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter un projet</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('projet_add') }}">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Nom</label>
|
||||
<input type="text" name="nom" class="form-control" required maxlength="50">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Commentaire</label>
|
||||
<textarea name="commentaire" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Date de lancement</label>
|
||||
<input type="date" name="dateLancement" class="form-control">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Date de clôture</label>
|
||||
<input type="date" name="dateCloture" class="form-control">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Statut</label>
|
||||
<select name="statut" class="form-select" required>
|
||||
<option value="en_attente">En attente</option>
|
||||
<option value="en_cours">En cours</option>
|
||||
<option value="termine">Terminé</option>
|
||||
<option value="annule">Annulé</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||
<button type="submit" class="btn btn-primary">Ajouter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
document.querySelectorAll('form[data-contrib-count]').forEach(function(form){
|
||||
var btn = form.querySelector('.btn-delete-projet');
|
||||
btn.addEventListener('click', function(ev){
|
||||
ev.preventDefault();
|
||||
var count = parseInt(form.dataset.contribCount || '0', 10);
|
||||
var proceed = false;
|
||||
if (count > 0) {
|
||||
proceed = confirm('Ce projet contient ' + count + ' contribution(s). La suppression entrainera la perte des contributions. Confirmer la suppression ?');
|
||||
if (proceed) {
|
||||
form.querySelector('input[name="_force"]').value = '1';
|
||||
}
|
||||
} else {
|
||||
proceed = confirm('Confirmer la suppression du projet ?');
|
||||
}
|
||||
if (proceed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
258
templates/projet/show.html.twig
Normal file
258
templates/projet/show.html.twig
Normal file
@@ -0,0 +1,258 @@
|
||||
{# show.html.twig — partie contributions (remplace la portion correspondante) #}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Contributions</h5>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted">Trier par :</small>
|
||||
<a href="?sort=date&direction={{ sort == 'date' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Date</a>
|
||||
<a href="?sort=membre&direction={{ sort == 'membre' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Développeur</a>
|
||||
<a href="?sort=duree&direction={{ sort == 'duree' and direction == 'asc' ? 'desc' : 'asc' }}" class="btn btn-sm btn-link">Durée</a>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addContribModal">Ajouter une contribution</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if contribs is empty %}
|
||||
<p>Aucune contribution pour ce projet.</p>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Développeur</th>
|
||||
<th>Date</th>
|
||||
<th>Durée</th>
|
||||
<th>Utilise IA ?</th>
|
||||
<th>Commentaires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in contribs %}
|
||||
<tr id="contrib-row-{{ c.id }}" data-token="{{ csrf_token('contrib_edit' ~ c.id) }}">
|
||||
<td>{{ c.membre }}</td>
|
||||
<td>{{ c.dateContribution ? c.dateContribution|date('d/m/Y') : '' }}</td>
|
||||
|
||||
{# Durée (affichage + input caché) #}
|
||||
<td class="contrib-duree">
|
||||
<span class="read-mode">{{ c.duree }} min</span>
|
||||
<input class="edit-mode form-control form-control-sm" type="number" min="0" value="{{ c.duree }}" style="display:none; width:100px;">
|
||||
</td>
|
||||
|
||||
{# Utilise IA ? (badge remains) #}
|
||||
<td>
|
||||
{% if c.contribIas|length > 0 %}
|
||||
<span class="badge bg-success">Oui</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Non</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Commentaire (span + textarea) #}
|
||||
<td class="contrib-commentaire">
|
||||
<div class="read-mode text-truncate" style="max-width:320px;">{{ c.commentaire }}</div>
|
||||
<textarea class="edit-mode form-control form-control-sm" style="display:none; max-width:320px;" rows="2">{{ c.commentaire }}</textarea>
|
||||
</td>
|
||||
|
||||
{# Assistant (display name + select) #}
|
||||
<td style="min-width:220px;">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="read-mode">
|
||||
{% if c.contribIas|first %}
|
||||
{{ c.contribIas|first.assistantIa.nom }}
|
||||
{% else %}
|
||||
Aucun
|
||||
{% endif %}
|
||||
</div>
|
||||
<select class="edit-mode form-select form-select-sm" style="display:none;">
|
||||
<option value="">Aucun</option>
|
||||
{% for a in assistants %}
|
||||
<option value="{{ a.id }}" {% if c.contribIas|first and c.contribIas|first.assistantIa.id == a.id %}selected{% endif %}>{{ a.nom }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Actions: cadenas + save/cancel + suppression #}
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="actions">
|
||||
<button type="button" class="btn btn-outline-secondary btn-lock" title="Éditer">
|
||||
<i class="bi bi-lock-fill"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-save" style="display:none;">
|
||||
<i class="bi bi-check-lg"></i> Enregistrer
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-cancel" style="display:none;">
|
||||
Annuler
|
||||
</button>
|
||||
<form method="post" action="{{ path('contrib_delete', {id: c.id}) }}" style="display:inline-block" onsubmit="return confirm('Confirmer la suppression de cette contribution ?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('contrib_delete' ~ c.id) }}">
|
||||
<button class="btn btn-sm btn-danger" type="submit"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<p><strong>Durée totale :</strong> {{ totalDuree }} minutes ({{ (totalDuree // 60) }}h{{ '%02d'|format(totalDuree % 60) }})</p>
|
||||
<p><strong>Développeurs ayant travaillé sur ce projet :</strong> {{ developpeurs|join(', ') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('projet_index') }}" class="btn btn-outline-secondary">Retour à la liste</a>
|
||||
|
||||
{# keep addContribModal from your original file here (unchanged) #}
|
||||
{# ... (modal addContribModal) ... #}
|
||||
|
||||
{# JS pour édition inline et cadenas #}
|
||||
{% block bottom_javascripts %}
|
||||
{{ parent() }}
|
||||
|
||||
<style>
|
||||
/* petit style utilitaire */
|
||||
.btn-lock { width:38px; }
|
||||
tr.editing { background: rgba(13,110,253,0.04); }
|
||||
.text-truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Helper: find elements in a row
|
||||
function rowEls(row){
|
||||
return {
|
||||
row: row,
|
||||
lockBtn: row.querySelector('.btn-lock'),
|
||||
saveBtn: row.querySelector('.btn-save'),
|
||||
cancelBtn: row.querySelector('.btn-cancel'),
|
||||
dureeSpan: row.querySelector('.contrib-duree .read-mode'),
|
||||
dureeInput: row.querySelector('.contrib-duree .edit-mode'),
|
||||
commentaireSpan: row.querySelector('.contrib-commentaire .read-mode'),
|
||||
commentaireTextarea: row.querySelector('.contrib-commentaire .edit-mode'),
|
||||
assistantRead: row.querySelector('td:nth-child(6) .read-mode'),
|
||||
assistantSelect: row.querySelector('td:nth-child(6) .edit-mode'),
|
||||
token: row.dataset.token || ''
|
||||
};
|
||||
}
|
||||
|
||||
function setEditMode(row, enabled){
|
||||
var els = rowEls(row);
|
||||
if(enabled){
|
||||
row.classList.add('editing');
|
||||
// show inputs
|
||||
Array.from(row.querySelectorAll('.read-mode')).forEach(e => e.style.display = 'none');
|
||||
Array.from(row.querySelectorAll('.edit-mode')).forEach(e => e.style.display = '');
|
||||
els.lockBtn.innerHTML = '<i class="bi bi-unlock-fill"></i>';
|
||||
els.saveBtn.style.display = '';
|
||||
els.cancelBtn.style.display = '';
|
||||
} else {
|
||||
row.classList.remove('editing');
|
||||
Array.from(row.querySelectorAll('.read-mode')).forEach(e => e.style.display = '');
|
||||
Array.from(row.querySelectorAll('.edit-mode')).forEach(e => e.style.display = 'none');
|
||||
els.lockBtn.innerHTML = '<i class="bi bi-lock-fill"></i>';
|
||||
els.saveBtn.style.display = 'none';
|
||||
els.cancelBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(row, message){
|
||||
// simple alert for now; you can replace with nicer toast
|
||||
alert('Erreur: ' + message);
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
// Attache handlers pour chaque ligne
|
||||
document.querySelectorAll('tr[id^="contrib-row-"]').forEach(function(row){
|
||||
var els = rowEls(row);
|
||||
|
||||
// Lock/unlock
|
||||
els.lockBtn.addEventListener('click', function(){
|
||||
var editing = row.classList.contains('editing');
|
||||
if(editing){
|
||||
// if currently editing, act like "lock" (cancel changes)
|
||||
if(confirm('Verrouiller la ligne et annuler les modifications non sauvegardées ?')) {
|
||||
// reset inputs to original values from read-mode
|
||||
els.dureeInput.value = (els.dureeSpan.textContent || '').replace(' min','').trim();
|
||||
els.commentaireTextarea.value = els.commentaireSpan.textContent || '';
|
||||
// assistant - try to select option by text
|
||||
var currentAssistText = els.assistantRead ? els.assistantRead.textContent.trim() : '';
|
||||
var found = Array.from(els.assistantSelect.options).find(o => o.text === currentAssistText);
|
||||
els.assistantSelect.value = found ? found.value : '';
|
||||
setEditMode(row, false);
|
||||
}
|
||||
} else {
|
||||
setEditMode(row, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel button
|
||||
els.cancelBtn.addEventListener('click', function(){
|
||||
if(confirm('Annuler les modifications ?')){
|
||||
els.dureeInput.value = (els.dureeSpan.textContent || '').replace(' min','').trim();
|
||||
els.commentaireTextarea.value = els.commentaireSpan.textContent || '';
|
||||
var currentAssistText = els.assistantRead ? els.assistantRead.textContent.trim() : '';
|
||||
var found = Array.from(els.assistantSelect.options).find(o => o.text === currentAssistText);
|
||||
els.assistantSelect.value = found ? found.value : '';
|
||||
setEditMode(row, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Save button -> ajax POST JSON
|
||||
els.saveBtn.addEventListener('click', function(){
|
||||
var id = row.id.replace('contrib-row-','');
|
||||
var data = {
|
||||
duree: parseInt(els.dureeInput.value || '0', 10),
|
||||
assistant: els.assistantSelect.value || null,
|
||||
commentaire: els.commentaireTextarea.value || '',
|
||||
token: els.token
|
||||
};
|
||||
|
||||
// simple validation
|
||||
if(isNaN(data.duree) || data.duree < 0){
|
||||
return showError(row, 'Durée invalide.');
|
||||
}
|
||||
|
||||
els.saveBtn.disabled = true;
|
||||
els.saveBtn.textContent = 'Enregistrement...';
|
||||
|
||||
fetch('{{ path('contrib_edit_inline', {'id': 'REPLACE_ID'}) }}'.replace('REPLACE_ID', id), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(function(resp){
|
||||
return resp.json().then(function(json){ return { ok: resp.ok, status: resp.status, json: json }; });
|
||||
}).then(function(res){
|
||||
if(!res.ok){
|
||||
showError(row, res.json && res.json.message ? res.json.message : ('HTTP ' + res.status));
|
||||
return;
|
||||
}
|
||||
// update read mode values with returned data
|
||||
var j = res.json;
|
||||
els.dureeSpan.textContent = (j.duree || 0) + ' min';
|
||||
els.commentaireSpan.textContent = j.commentaire || '';
|
||||
if(els.assistantRead) els.assistantRead.textContent = j.assistantName || 'Aucun';
|
||||
// update total duration? (optional) — not handled here
|
||||
setEditMode(row, false);
|
||||
}).catch(function(err){
|
||||
showError(row, err.message || err);
|
||||
}).finally(function(){
|
||||
els.saveBtn.disabled = false;
|
||||
els.saveBtn.innerHTML = '<i class="bi bi-check-lg"></i> Enregistrer';
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user