svg
This commit is contained in:
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Repository\IssueRepository;
|
|
||||||
use App\Service\RedmineService;
|
|
||||||
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;
|
||||||
@ -11,49 +9,15 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
|
|
||||||
class HomeController extends AbstractController
|
class HomeController extends AbstractController
|
||||||
{
|
{
|
||||||
private RedmineService $redmineService;
|
|
||||||
|
|
||||||
public function __construct(RedmineService $redmineService)
|
|
||||||
{
|
|
||||||
$this->redmineService = $redmineService;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/', name: 'app_home')]
|
#[Route('/', name: 'app_home')]
|
||||||
public function home(Request $request): Response
|
public function home(Request $request): Response
|
||||||
{
|
{
|
||||||
$project = $request->getSession()->get('project');
|
$projects = $this->getUser()->getProjects();
|
||||||
if (!$project) {
|
|
||||||
return $this->noproject();
|
|
||||||
}
|
|
||||||
|
|
||||||
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
|
||||||
$project->setRedmine($redmine);
|
|
||||||
$this->redmineService->majProjectIssues($project, $this->getParameter('redmineApikey'), false);
|
|
||||||
|
|
||||||
$predmine = $project->getRedmine();
|
|
||||||
|
|
||||||
foreach ($project->getIssues() as $issue) {
|
|
||||||
foreach ($predmine['sprints'] as $key => $sprint) {
|
|
||||||
if ($sprint['id'] === $issue->getRedmine()['sprint']['agile_sprint_id']) {
|
|
||||||
if (!array_key_exists('story_points', $predmine['sprints'][$key])) {
|
|
||||||
$predmine['sprints'][$key]['story_points']['total'] = 0;
|
|
||||||
}
|
|
||||||
$predmine['sprints'][$key]['story_points']['total'] += $issue->getRedmine()['sprint']['story_points'];
|
|
||||||
|
|
||||||
if (!array_key_exists($issue->getRedmine()['status']['id'], $predmine['sprints'][$key]['story_points'])) {
|
|
||||||
$predmine['sprints'][$key]['story_points'][$issue->getRedmine()['status']['id']] = 0;
|
|
||||||
}
|
|
||||||
$predmine['sprints'][$key]['story_points'][$issue->getRedmine()['status']['id']] += $issue->getRedmine()['sprint']['story_points'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$project->setRedmine($predmine);
|
|
||||||
|
|
||||||
return $this->render('home/home.html.twig', [
|
return $this->render('home/home.html.twig', [
|
||||||
'usemenu' => true,
|
'usemenu' => true,
|
||||||
'usesidebar' => false,
|
'usesidebar' => false,
|
||||||
'project' => $project,
|
'projects' => $projects,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,23 +29,4 @@ class HomeController extends AbstractController
|
|||||||
'usesidebar' => true,
|
'usesidebar' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/noproject', name: 'app_noproject')]
|
|
||||||
public function noproject(): Response
|
|
||||||
{
|
|
||||||
return $this->render('home/noproject.html.twig', [
|
|
||||||
'usemenu' => true,
|
|
||||||
'usesidebar' => false,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/user/issue/{id}', name: 'issue_view')]
|
|
||||||
public function view(int $id, IssueRepository $issueRepository): Response
|
|
||||||
{
|
|
||||||
$issue = $issueRepository->find($id);
|
|
||||||
|
|
||||||
return $this->render('issue/view.html.twig', [
|
|
||||||
'issue' => $issue,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
85
src/Controller/IssueController.php
Normal file
85
src/Controller/IssueController.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\IssueRepository;
|
||||||
|
use App\Service\RedmineService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class IssueController extends AbstractController
|
||||||
|
{
|
||||||
|
private RedmineService $redmineService;
|
||||||
|
|
||||||
|
public function __construct(RedmineService $redmineService)
|
||||||
|
{
|
||||||
|
$this->redmineService = $redmineService;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/user/issue/{id}', name: 'app_issue_view')]
|
||||||
|
public function viewIssue(int $id, IssueRepository $issueRepository): Response
|
||||||
|
{
|
||||||
|
$issue = $issueRepository->find($id);
|
||||||
|
if (!$issue) {
|
||||||
|
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||||
|
}
|
||||||
|
if (!$issue->getProject()->getUsers()->contains($this->getUser())) {
|
||||||
|
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('issue/view.html.twig', [
|
||||||
|
'issue' => $issue,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/user/issue/order/{id}', name: 'app_issue_order', methods: ['POST'])]
|
||||||
|
public function orderIssue(int $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->request;
|
||||||
|
|
||||||
|
$sourceIssues = $data->all('sourceIssues');
|
||||||
|
$source = explode('|', $data->get('source'));
|
||||||
|
$sourceStatus = $source[0];
|
||||||
|
$sourceSprint = $source[1];
|
||||||
|
$sourceVersion = $source[2];
|
||||||
|
|
||||||
|
$target = explode('|', $data->get('target'));
|
||||||
|
$targetStatus = $target[0];
|
||||||
|
$targetSprint = $target[1];
|
||||||
|
$targetVersion = $target[2];
|
||||||
|
|
||||||
|
$targetIssues = $data->all('targetIssues');
|
||||||
|
|
||||||
|
if (!$sourceIssues || !$source || !$targetIssues || !$target) {
|
||||||
|
return new JsonResponse(['error' => 'Données incomplètes.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'fixed_version_id' => $target,
|
||||||
|
];
|
||||||
|
|
||||||
|
// $redmineService->updateIssue($id, $payload);
|
||||||
|
|
||||||
|
// ✅ Tu peux stocker l’ordre si besoin dans Redmine via custom field (optionnel)
|
||||||
|
// ou en interne selon ta logique métier
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'message' => 'Ordre mis à jour',
|
||||||
|
'moved' => $id,
|
||||||
|
'sourceIssues' => $sourceIssues,
|
||||||
|
'sourceStatus' => $targetStatus,
|
||||||
|
'sourceSprint' => $sourceSprint,
|
||||||
|
'sourceVersion' => $sourceVersion,
|
||||||
|
|
||||||
|
'targetIssues' => $targetIssues,
|
||||||
|
'targetStatus' => $targetStatus,
|
||||||
|
'targetSprint' => $targetSprint,
|
||||||
|
'targetVersion' => $targetVersion,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,10 @@ use App\Repository\ProjectRepository;
|
|||||||
use App\Service\RedmineService;
|
use App\Service\RedmineService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class ProjectController extends AbstractController
|
class ProjectController extends AbstractController
|
||||||
@ -22,6 +24,48 @@ class ProjectController extends AbstractController
|
|||||||
$this->redmineService = $redmineService;
|
$this->redmineService = $redmineService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/user/project/{id}', name: 'app_project_view')]
|
||||||
|
public function viewProject(int $id, ProjectRepository $projectRepository): Response
|
||||||
|
{
|
||||||
|
$project = $projectRepository->find($id);
|
||||||
|
if (!$project) {
|
||||||
|
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||||
|
}
|
||||||
|
if (!$project->getUsers()->contains($this->getUser())) {
|
||||||
|
throw new AccessDeniedException('Vous n\'avez pas accès à cette ressource.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
||||||
|
$project->setRedmine($redmine);
|
||||||
|
$this->redmineService->majProjectIssues($project, $this->getParameter('redmineApikey'), false);
|
||||||
|
|
||||||
|
$predmine = $project->getRedmine();
|
||||||
|
|
||||||
|
foreach ($project->getIssues() as $issue) {
|
||||||
|
foreach ($predmine['sprints'] as $key => $sprint) {
|
||||||
|
if ($sprint['id'] === $issue->getRedmine()['sprint']['agile_sprint_id']) {
|
||||||
|
if (!array_key_exists('story_points', $predmine['sprints'][$key])) {
|
||||||
|
$predmine['sprints'][$key]['story_points']['total'] = 0;
|
||||||
|
}
|
||||||
|
$predmine['sprints'][$key]['story_points']['total'] += $issue->getRedmine()['sprint']['story_points'];
|
||||||
|
|
||||||
|
if (!array_key_exists($issue->getRedmine()['status']['id'], $predmine['sprints'][$key]['story_points'])) {
|
||||||
|
$predmine['sprints'][$key]['story_points'][$issue->getRedmine()['status']['id']] = 0;
|
||||||
|
}
|
||||||
|
$predmine['sprints'][$key]['story_points'][$issue->getRedmine()['status']['id']] += $issue->getRedmine()['sprint']['story_points'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$project->setRedmine($predmine);
|
||||||
|
|
||||||
|
return $this->render('project/view.html.twig', [
|
||||||
|
'usemenu' => true,
|
||||||
|
'usesidebar' => false,
|
||||||
|
'project' => $project,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/admin/project', name: 'app_admin_project')]
|
#[Route('/admin/project', name: 'app_admin_project')]
|
||||||
public function list(ProjectRepository $projectRepository): Response
|
public function list(ProjectRepository $projectRepository): Response
|
||||||
{
|
{
|
||||||
@ -66,11 +110,11 @@ class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/admin/project/update/{id}', name: 'app_admin_project_update')]
|
#[Route('/admin/project/update/{id}', name: 'app_admin_project_update')]
|
||||||
public function update(int $id, Request $request, EntityManagerInterface $em): Response
|
public function update(int $id, Request $request, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
$project = $em->getRepository(Project::class)->find($id);
|
$project = $projectRepository->find($id);
|
||||||
if (!$project) {
|
if (!$project) {
|
||||||
return $this->redirectToRoute('app_admin_project');
|
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
||||||
@ -102,11 +146,11 @@ class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/admin/project/reclone/{id}', name: 'app_admin_project_reclone')]
|
#[Route('/admin/project/reclone/{id}', name: 'app_admin_project_reclone')]
|
||||||
public function reclone(int $id, EntityManagerInterface $em): Response
|
public function reclone(int $id, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
$project = $em->getRepository(Project::class)->find($id);
|
$project = $projectRepository->find($id);
|
||||||
if (!$project) {
|
if (!$project) {
|
||||||
return $this->redirectToRoute('app_admin_project');
|
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
$redmine = $this->redmineService->getProject($project->getId(), $this->getParameter('redmineApikey'));
|
||||||
@ -118,12 +162,11 @@ class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/admin/project/delete/{id}', name: 'app_admin_project_delete')]
|
#[Route('/admin/project/delete/{id}', name: 'app_admin_project_delete')]
|
||||||
public function delete(int $id, EntityManagerInterface $em): Response
|
public function delete(int $id, ProjectRepository $projectRepository, EntityManagerInterface $em): Response
|
||||||
{
|
{
|
||||||
// Récupération de l'enregistrement courant
|
$project = $projectRepository->find($id);
|
||||||
$project = $em->getRepository(Project::class)->find($id);
|
|
||||||
if (!$project) {
|
if (!$project) {
|
||||||
return $this->redirectToRoute('app_admin_project');
|
throw new NotFoundHttpException('La ressource demandée est introuvable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $em->getRepository(User::class)->findBy(['project' => $project]);
|
$users = $em->getRepository(User::class)->findBy(['project' => $project]);
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\EventListener;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
|
||||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
|
||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
|
||||||
|
|
||||||
final class SessionListener
|
|
||||||
{
|
|
||||||
private Security $security;
|
|
||||||
private RouterInterface $router;
|
|
||||||
|
|
||||||
public function __construct(Security $security, RouterInterface $router)
|
|
||||||
{
|
|
||||||
$this->security = $security;
|
|
||||||
$this->router = $router;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[AsEventListener(event: KernelEvents::REQUEST)]
|
|
||||||
public function onKernelRequest(RequestEvent $event): void
|
|
||||||
{
|
|
||||||
$request = $event->getRequest();
|
|
||||||
$session = $request->getSession();
|
|
||||||
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
if ($user instanceof User) {
|
|
||||||
// Intialisation de la compagnie en cours
|
|
||||||
if (!$user->getProject()) {
|
|
||||||
if ($user->getProjects()) {
|
|
||||||
$user->setProject($user->getProjects()[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$session->set('project', $user->getProject());
|
|
||||||
$session->set('projects', $user->getProjects());
|
|
||||||
|
|
||||||
$currentPath = $request->getPathInfo();
|
|
||||||
$noProjectPath = $this->router->generate('app_noproject');
|
|
||||||
if (!$user->getProject() && 0 === stripos($currentPath, 'noproject')) {
|
|
||||||
$event->setResponse(new RedirectResponse($noProjectPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -43,11 +43,7 @@
|
|||||||
<nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
|
<nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{{ path('app_home') }}">
|
<a class="navbar-brand" href="{{ path('app_home') }}">
|
||||||
{% if app.session.get('project') %}
|
|
||||||
<img src="{{asset(app.session.get('project').logo)}}"> Projet = {{app.session.get('project').title}}
|
|
||||||
{% else %}
|
|
||||||
<img src="{{asset("medias/logo/logo.png")}}"> {{appName}}
|
<img src="{{asset("medias/logo/logo.png")}}"> {{appName}}
|
||||||
{% endif %}
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarColor02">
|
<div class="collapse navbar-collapse" id="navbarColor02">
|
||||||
@ -60,15 +56,6 @@
|
|||||||
|
|
||||||
<div class="navbar-nav ms-auto ps-3 d-flex flex-row position-absolute" style="right: 15px; top:15px">
|
<div class="navbar-nav ms-auto ps-3 d-flex flex-row position-absolute" style="right: 15px; top:15px">
|
||||||
{% if app.user %}
|
{% if app.user %}
|
||||||
<div class="nav-link">
|
|
||||||
<select class="select2" name="selectproject" id="selectproject" style="width:200px" data-change="{{path('app_user_selectproject')}}">
|
|
||||||
<option value="" disabled selected>Selectionnez un projet</option>
|
|
||||||
{%for project in app.session.get('projects')%}
|
|
||||||
<option value="{{project.id}}" {{app.session.get('project') and project.id==app.session.get('project').id?"selected":""}}>{{project.title}}</option>
|
|
||||||
{%endfor%}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if is_granted('ROLE_ADMIN') %}
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
<a class="nav-link px-2" href="{{path('app_admin')}}"><i class="fa-solid fa-cog fa-2x"></i></a>
|
<a class="nav-link px-2" href="{{path('app_admin')}}"><i class="fa-solid fa-cog fa-2x"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,433 +1,17 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block localstyle %}
|
|
||||||
<style>
|
|
||||||
small {
|
|
||||||
font-size:70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
verysmall {
|
|
||||||
font-size:60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
width: 100%;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding-top:80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
content {
|
|
||||||
padding:0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrumContainer {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueContainer {
|
|
||||||
position:fixed;
|
|
||||||
width:750px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filtreContainer{
|
|
||||||
display:flex;
|
|
||||||
width:300px;
|
|
||||||
flex-direction:column;
|
|
||||||
background-color: var(--bs-dark);
|
|
||||||
padding:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.containerStatus{
|
|
||||||
display:flex;
|
|
||||||
//width:10000px;
|
|
||||||
overflow-x:scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusCard {
|
|
||||||
width:300px;
|
|
||||||
padding: 15px 10px 0px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size:18px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin-bottom:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sprintCardHeader {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versionCard {
|
|
||||||
background-color: var(--bs-gray-100);
|
|
||||||
padding: 5px 5px 0px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versionBody {
|
|
||||||
min-height:60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versionCard h5 {
|
|
||||||
color: var(--bs-primary-text-emphasis);
|
|
||||||
font-size:16px;
|
|
||||||
padding-left:10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueCard {
|
|
||||||
padding:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueId{
|
|
||||||
padding: 0px 5px 0px 2px;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueAction{
|
|
||||||
align-self: baseline;
|
|
||||||
padding: 0px 2px 0px 5px;
|
|
||||||
text-align:center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-eye {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issueSubject{
|
|
||||||
zoom: 70%;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{%block body%}
|
{%block body%}
|
||||||
|
<h2>Projets</h2>
|
||||||
|
|
||||||
<div class='issueContainer'>
|
<div class='d-flex' style='justify-content: center'>
|
||||||
|
{% for project in projects %}
|
||||||
|
<div class='card'>
|
||||||
|
<a href='{{ path('app_project_view',{id:project.id})}}' class='d-flex' style='flex-direction: column; text-align: center'>
|
||||||
|
<img src='{{asset(project.logo)}}' style='width:250px'>
|
||||||
|
<h5>{{project.title}}</h5>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='scrumContainer'>
|
|
||||||
<div class='filtreContainer'>
|
|
||||||
<label>Statut</label>
|
|
||||||
<select id="statusFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
|
||||||
{% for statut in project.redmine.issue_statuses %}
|
|
||||||
{% if statut.id not in project.hiddenstatuses %}
|
|
||||||
<option value="{{statut.id}}">{{statut.name}}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label>Sprints</label>
|
|
||||||
<select id="sprintFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
|
||||||
<option value="None">Aucun</option>
|
|
||||||
{% for sprint in project.redmine.sprints|reverse %}
|
|
||||||
{% if sprint.id not in project.hiddensprints %}
|
|
||||||
<option value="{{sprint.id}}">{{sprint.name}}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label>Versions</label>
|
|
||||||
<select id="versionFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
|
||||||
<option value="None">Aucune</option>
|
|
||||||
{% for version in project.redmine.versions|reverse %}
|
|
||||||
{% if version.id not in project.hiddenversions %}
|
|
||||||
<option value="{{version.id}}">{{version.name}}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button id="toggleIssueBody" class="btn btn-secondary btn-sm mt-3">Afficher Détail</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='containerStatus'>
|
|
||||||
{% for status in project.redmine.issue_statuses %}
|
|
||||||
{% if status.id not in project.hiddenstatuses %}
|
|
||||||
<div class='statusCard statusCard{{status.id}}'>
|
|
||||||
<h2>{{ status.name }}</h2>
|
|
||||||
|
|
||||||
{% for sprint in project.redmine.sprints|reverse %}
|
|
||||||
{% if sprint.id not in project.hiddensprints %}
|
|
||||||
<div class='sprintCard sprintCard{{sprint.id}} card' style='margin-bottom:20px'>
|
|
||||||
<div class='sprintCardHeader card-header'>
|
|
||||||
<div style='flex-grow:1'>Sprint = {{ sprint.name }}</div>
|
|
||||||
{% if sprint.story_points[status.id] is defined %}
|
|
||||||
<div>{{ sprint.story_points[status.id] }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for version in project.redmine.versions|reverse %}
|
|
||||||
{% if version.id not in project.hiddenversions %}
|
|
||||||
<div class='versionCard versionCard{{version.id}} card-body'>
|
|
||||||
<h5>Version = {{ version.name }}</h5>
|
|
||||||
<div class='versionBody' data-id='{{status.id}}-{{sprint.id}}-{{version.name}}'></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class='versionCard versionCardNone card-body'>
|
|
||||||
<h5>Version = Aucune</h5>
|
|
||||||
<div class='versionBody' data-id='{{status.id}}-{{sprint.id}}-'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class='sprintCard sprintCardNone card'>
|
|
||||||
<div class='card-header'>
|
|
||||||
Sprint Aucun
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for version in project.redmine.versions|reverse %}
|
|
||||||
{% if version.id not in project.hiddenversions %}
|
|
||||||
<div class='versionCard versionCard{{version.id}} card-body'>
|
|
||||||
<h5>Version = {{ version.name }}</h5>
|
|
||||||
<div class='versionBody' data-id='{{status.id}}--{{version.name}}'></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class='versionCard versionCardNone card-body'>
|
|
||||||
<h5>Version = Aucune</h5>
|
|
||||||
<div class='versionBody' data-id='{{status.id}}--'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for issue in project.issues %}
|
|
||||||
<div class="issueCard card" data-status='{{issue.redmine.status.id}}' data-sprint='{{issue.rowsprint}}' data-version='{{issue.rowversion}}'>
|
|
||||||
<div class='issueHeader'>
|
|
||||||
<div class='issueId'>#{{issue.id}}</div>
|
|
||||||
<div class='issueSubject'>{{issue.redmine.subject}}</div>
|
|
||||||
<div class='issueAction'>
|
|
||||||
<i class='fas fa-eye' onClick='fetchAndRenderIssue({{issue.id}})'></i>
|
|
||||||
<verysmall>{{issue.redmine.sprint.story_points}}</verysmall>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class='issueBody'>
|
|
||||||
sprint = {{issue.rowsprint}}<br>
|
|
||||||
version = {{issue.rowversion}}<br>
|
|
||||||
status = {{issue.redmine.status.name}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%endblock%}
|
{%endblock%}
|
||||||
|
|
||||||
{% block javascripts %}
|
|
||||||
<script>
|
|
||||||
let showIssuebody = false;
|
|
||||||
|
|
||||||
function adjustHeight() {
|
|
||||||
console.log("here");
|
|
||||||
let ele = $(".issueDescription");
|
|
||||||
if (ele.length) {
|
|
||||||
ele.css("height", window.innerHeight - ele.offset().top + "px");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showhide() {
|
|
||||||
// Statut
|
|
||||||
selected = $('#statusFilter').val();
|
|
||||||
if (!selected || selected.length === 0) {
|
|
||||||
$('.statusCard').show();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('.statusCard').hide();
|
|
||||||
selected.forEach(function (id) {
|
|
||||||
$('.statusCard' + id).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprint
|
|
||||||
selected = $('#sprintFilter').val();
|
|
||||||
if (!selected || selected.length === 0) {
|
|
||||||
$('.sprintCard').show();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('.sprintCard').hide();
|
|
||||||
selected.forEach(function (id) {
|
|
||||||
$('.sprintCard' + id).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version
|
|
||||||
selected = $('#versionFilter').val();
|
|
||||||
if (!selected || selected.length === 0) {
|
|
||||||
$('.versionCard').show();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('.versionCard').hide();
|
|
||||||
selected.forEach(function (id) {
|
|
||||||
$('.versionCard' + id).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toogleIssueBody() {
|
|
||||||
if (showIssuebody) {
|
|
||||||
$('.issueBody').show();
|
|
||||||
$(this).text('Masquer Détail');
|
|
||||||
} else {
|
|
||||||
$('.issueBody').hide();
|
|
||||||
$(this).text('Afficher Détail');
|
|
||||||
}
|
|
||||||
showIssuebody=!showIssuebody;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
// Ranger les issues
|
|
||||||
$('.issueCard').each(function () {
|
|
||||||
const id = $(this).data('status')+'-'+$(this).data('sprint')+'-'+$(this).data('version');
|
|
||||||
const $column = $(`[data-id='${id}']`);
|
|
||||||
if ($column.length) {
|
|
||||||
$column.append($(this));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log ('no ='+id);
|
|
||||||
$(this).hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Afficher le scrum après rangement
|
|
||||||
$('.scrumContainer').css('display', 'flex');
|
|
||||||
|
|
||||||
// Filtre
|
|
||||||
$('#statusFilter').on('change',showhide);
|
|
||||||
$('#sprintFilter').on('change',showhide);
|
|
||||||
$('#versionFilter').on('change',showhide);
|
|
||||||
showhide();
|
|
||||||
|
|
||||||
// issueBody
|
|
||||||
$('#toggleIssueBody').on('click',toogleIssueBody);
|
|
||||||
toogleIssueBody();
|
|
||||||
|
|
||||||
// Ajuste height
|
|
||||||
adjustHeight();
|
|
||||||
window.addEventListener("resize", adjustHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
function fetchAndRenderIssue(issueId) {
|
|
||||||
url='{{path('issue_view',{id:'xxx'})}}';
|
|
||||||
url=url.replace('xxx',issueId);
|
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
method: 'GET',
|
|
||||||
dataType: 'html',
|
|
||||||
success: function (html) {
|
|
||||||
$('.issueContainer').html(html);
|
|
||||||
$('.issueContainer').show();
|
|
||||||
$('.scrumContainer').css('padding-left','750px');
|
|
||||||
$('.issueDescription').animate({scrollTop: 0}, 0);
|
|
||||||
adjustHeight();
|
|
||||||
},
|
|
||||||
error: function (xhr, status, error) {
|
|
||||||
console.error('Erreur lors du chargement de l’issue :', error);
|
|
||||||
$('.issueContainer').html('<div class="alert alert-danger">Erreur lors du chargement de l’issue.</div>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
let $sourceContainer = null;
|
|
||||||
let $movedItem = null;
|
|
||||||
let originalIndex = null;
|
|
||||||
|
|
||||||
$('.versionBody').sortable({
|
|
||||||
connectWith: '.versionBody',
|
|
||||||
items: '.issueCard',
|
|
||||||
handle: '.issueId',
|
|
||||||
placeholder: 'issue-placeholder',
|
|
||||||
forcePlaceholderSize: true,
|
|
||||||
|
|
||||||
start: function (event, ui) {
|
|
||||||
$sourceContainer = ui.item.parent();
|
|
||||||
$movedItem = ui.item;
|
|
||||||
originalIndex = ui.item.index();
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function (event, ui) {
|
|
||||||
// ❗ Se déclenche même pour tri dans la même colonne
|
|
||||||
if (!event.originalEvent) return; // ← ignorer les appels indirects
|
|
||||||
const $targetContainer = $(this);
|
|
||||||
|
|
||||||
const sourceId = $sourceContainer.data('id');
|
|
||||||
const targetId = $targetContainer.data('id');
|
|
||||||
|
|
||||||
// Ne pas dupliquer l'appel si c’est un vrai "receive"
|
|
||||||
if (ui.sender) return;
|
|
||||||
|
|
||||||
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
|
|
||||||
},
|
|
||||||
|
|
||||||
receive: function (event, ui) {
|
|
||||||
const $targetContainer = $(this);
|
|
||||||
const sourceId = $sourceContainer.data('id');
|
|
||||||
const targetId = $targetContainer.data('id');
|
|
||||||
|
|
||||||
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex) {
|
|
||||||
const sourceIssues = $sourceContainer.find('.issueCard').map(function () {
|
|
||||||
return $(this).data('id');
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
const targetIssues = $targetContainer.find('.issueCard').map(function () {
|
|
||||||
return $(this).data('id');
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/your/route/here', // 🔁 à adapter
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
sourceVersion: sourceId,
|
|
||||||
targetVersion: targetId,
|
|
||||||
sourceIssues: sourceIssues,
|
|
||||||
targetIssues: targetIssues
|
|
||||||
},
|
|
||||||
success: function (response) {
|
|
||||||
console.log('Déplacement réussi', response);
|
|
||||||
},
|
|
||||||
error: function (xhr) {
|
|
||||||
console.error('Erreur AJAX', xhr);
|
|
||||||
|
|
||||||
// Annuler le déplacement
|
|
||||||
if ($movedItem && $sourceContainer) {
|
|
||||||
const items = $sourceContainer.children();
|
|
||||||
if (originalIndex >= items.length) {
|
|
||||||
$sourceContainer.append($movedItem);
|
|
||||||
} else {
|
|
||||||
$movedItem.insertBefore(items.eq(originalIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,4 +0,0 @@
|
|||||||
{% extends 'base.html.twig' %}
|
|
||||||
{%block body%}
|
|
||||||
<h2>Veuillez selectionner un projet</h2>
|
|
||||||
{%endblock%}
|
|
@ -43,7 +43,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<hr>
|
<hr>
|
||||||
<strong>Description :</strong>
|
<strong>Description :</strong>
|
||||||
<p>{{ issue.redmine.description|markdown_to_html|textile_to_html }}</p>
|
<p>{{ issue.redmine.description|textile_to_html|markdown_to_html }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
486
templates/project/view.html.twig
Normal file
486
templates/project/view.html.twig
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block localstyle %}
|
||||||
|
<style>
|
||||||
|
small {
|
||||||
|
font-size:70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
verysmall {
|
||||||
|
font-size:60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-top:80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
content {
|
||||||
|
padding:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrumContainer {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueContainer {
|
||||||
|
position:fixed;
|
||||||
|
width:750px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtreContainer{
|
||||||
|
display:flex;
|
||||||
|
width:300px;
|
||||||
|
flex-direction:column;
|
||||||
|
background-color: var(--bs-dark);
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerStatus{
|
||||||
|
display:flex;
|
||||||
|
//width:10000px;
|
||||||
|
overflow-x:scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCard {
|
||||||
|
width:300px;
|
||||||
|
padding: 15px 10px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size:18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sprintCardHeader {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionCard {
|
||||||
|
background-color: var(--bs-gray-100);
|
||||||
|
padding: 5px 5px 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionBody {
|
||||||
|
min-height:60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionCard h5 {
|
||||||
|
color: var(--bs-primary-text-emphasis);
|
||||||
|
font-size:16px;
|
||||||
|
padding-left:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueCard {
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueId{
|
||||||
|
padding: 0px 5px 0px 2px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueAction{
|
||||||
|
align-self: baseline;
|
||||||
|
padding: 0px 2px 0px 5px;
|
||||||
|
text-align:center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-eye {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issueSubject{
|
||||||
|
zoom: 70%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class='issueContainer'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='scrumContainer'>
|
||||||
|
<div class='filtreContainer'>
|
||||||
|
<label>Statut</label>
|
||||||
|
<select id="statusFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
||||||
|
{% for statut in project.redmine.issue_statuses %}
|
||||||
|
{% if statut.id not in project.hiddenstatuses %}
|
||||||
|
<option value="{{statut.id}}">{{statut.name}}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Sprints</label>
|
||||||
|
<select id="sprintFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
||||||
|
<option value="None">Aucun</option>
|
||||||
|
{% for sprint in project.redmine.sprints|reverse %}
|
||||||
|
{% if sprint.id not in project.hiddensprints %}
|
||||||
|
<option value="{{sprint.id}}">{{sprint.name}}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Versions</label>
|
||||||
|
<select id="versionFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
||||||
|
<option value="None">Aucune</option>
|
||||||
|
{% for version in project.redmine.versions|reverse %}
|
||||||
|
{% if version.id not in project.hiddenversions %}
|
||||||
|
<option value="{{version.id}}">{{version.name}}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Trackers</label>
|
||||||
|
<select id="trackerFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
||||||
|
{% for tracker in project.redmine.trackers %}
|
||||||
|
<option value="tracker{{tracker.id}}">{{tracker.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Catégories</label>
|
||||||
|
<select id="categoryFilter" class="select2 form-select" multiple="true" tabindex="-1" aria-hidden="true">
|
||||||
|
{% for category in project.redmine.issue_categories %}
|
||||||
|
<option value="category{{category.id}}">{{category.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
<button id="toggleIssueBody" class="btn btn-secondary btn-sm mt-3">Afficher Détail</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='containerStatus'>
|
||||||
|
{% for status in project.redmine.issue_statuses %}
|
||||||
|
{% if status.id not in project.hiddenstatuses %}
|
||||||
|
<div class='statusCard statusCard{{status.id}}'>
|
||||||
|
<h2>{{ status.name }}</h2>
|
||||||
|
|
||||||
|
{% for sprint in project.redmine.sprints|reverse %}
|
||||||
|
{% if sprint.id not in project.hiddensprints %}
|
||||||
|
<div class='sprintCard sprintCard{{sprint.id}} card' style='margin-bottom:20px'>
|
||||||
|
<div class='sprintCardHeader card-header'>
|
||||||
|
<div style='flex-grow:1'>Sprint = {{ sprint.name }}</div>
|
||||||
|
{% if sprint.story_points[status.id] is defined %}
|
||||||
|
<div>{{ sprint.story_points[status.id] }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for version in project.redmine.versions|reverse %}
|
||||||
|
{% if version.id not in project.hiddenversions %}
|
||||||
|
<div class='versionCard versionCard{{version.id}} card-body'>
|
||||||
|
<h5>Version = {{ version.name }}</h5>
|
||||||
|
<div class='versionBody' data-id='{{status.id}}|{{sprint.id}}|{{version.id}}'></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class='versionCard versionCardNone card-body'>
|
||||||
|
<h5>Version = Aucune</h5>
|
||||||
|
<div class='versionBody' data-id='{{status.id}}|{{sprint.id}}|'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class='sprintCard sprintCardNone card'>
|
||||||
|
<div class='card-header'>
|
||||||
|
Sprint Aucun
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for version in project.redmine.versions|reverse %}
|
||||||
|
{% if version.id not in project.hiddenversions %}
|
||||||
|
<div class='versionCard versionCard{{version.id}} card-body'>
|
||||||
|
<h5>Version = {{ version.name }}</h5>
|
||||||
|
<div class='versionBody' data-id='{{status.id}}||{{version.id}}'></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class='versionCard versionCardNone card-body'>
|
||||||
|
<h5>Version = Aucune</h5>
|
||||||
|
<div class='versionBody' data-id='{{status.id}}||'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for issue in project.issues %}
|
||||||
|
<div class="issueCard card tracker{{issue.redmine.tracker.id}} category{{(issue.redmine.category is defined?issue.redmine.category.id:'0') }}" data-status='{{issue.redmine.status.id}}' data-sprint='{{issue.rowsprint}}' data-version='{{(issue.redmine.fixed_version is defined?issue.redmine.fixed_version.id:'')}}' data-id='{{issue.id}}'>
|
||||||
|
<div class='issueHeader'>
|
||||||
|
<div class='issueId'>#{{issue.id}}</div>
|
||||||
|
<div class='issueSubject'>{{issue.redmine.subject}}</div>
|
||||||
|
<div class='issueAction'>
|
||||||
|
<i class='fas fa-eye' onClick='fetchAndRenderIssue({{issue.id}})'></i>
|
||||||
|
<verysmall>{{issue.redmine.sprint.story_points}}</verysmall>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='issueBody'>
|
||||||
|
sprint = {{issue.rowsprint}}<br>
|
||||||
|
version = {{issue.rowversion}}<br>
|
||||||
|
status = {{issue.redmine.status.name}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
<script>
|
||||||
|
let showIssuebody = false;
|
||||||
|
|
||||||
|
function adjustHeight() {
|
||||||
|
let ele = $(".issueDescription");
|
||||||
|
if (ele.length) {
|
||||||
|
ele.css("height", window.innerHeight - ele.offset().top + "px");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showhide() {
|
||||||
|
// Statut
|
||||||
|
selected = $('#statusFilter').val();
|
||||||
|
if (!selected || selected.length === 0) {
|
||||||
|
$('.statusCard').show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.statusCard').hide();
|
||||||
|
selected.forEach(function (id) {
|
||||||
|
$('.statusCard' + id).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint
|
||||||
|
selected = $('#sprintFilter').val();
|
||||||
|
if (!selected || selected.length === 0) {
|
||||||
|
$('.sprintCard').show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.sprintCard').hide();
|
||||||
|
selected.forEach(function (id) {
|
||||||
|
$('.sprintCard' + id).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
selected = $('#versionFilter').val();
|
||||||
|
if (!selected || selected.length === 0) {
|
||||||
|
$('.versionCard').show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.versionCard').hide();
|
||||||
|
selected.forEach(function (id) {
|
||||||
|
$('.versionCard' + id).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre issue
|
||||||
|
$('.issueCard').hide();
|
||||||
|
|
||||||
|
const selectedTrackers = $('#trackerFilter').val() || [];
|
||||||
|
const selectedCategories = $('#categoryFilter').val() || [];
|
||||||
|
|
||||||
|
//const selectedCategories = []; // ou ['category1'] etc.
|
||||||
|
$('.issueCard').each(function () {
|
||||||
|
const classes = $(this).attr('class').split(/\s+/);
|
||||||
|
|
||||||
|
const hasValidTracker =
|
||||||
|
selectedTrackers.length === 0 ||
|
||||||
|
selectedTrackers.some(tracker => classes.includes(tracker));
|
||||||
|
|
||||||
|
const hasValidCategory =
|
||||||
|
selectedCategories.length === 0 ||
|
||||||
|
selectedCategories.some(category => classes.includes(category));
|
||||||
|
|
||||||
|
if(hasValidTracker&&hasValidCategory) $(this).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tracker
|
||||||
|
selected = $('#versionFilter').val();
|
||||||
|
if (!selected || selected.length === 0) {
|
||||||
|
$('.versionCard').show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.versionCard').hide();
|
||||||
|
selected.forEach(function (id) {
|
||||||
|
$('.versionCard' + id).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toogleIssueBody() {
|
||||||
|
if (showIssuebody) {
|
||||||
|
$('.issueBody').show();
|
||||||
|
$(this).text('Masquer Détail');
|
||||||
|
} else {
|
||||||
|
$('.issueBody').hide();
|
||||||
|
$(this).text('Afficher Détail');
|
||||||
|
}
|
||||||
|
showIssuebody=!showIssuebody;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Ranger les issues
|
||||||
|
$('.issueCard').each(function () {
|
||||||
|
const id = $(this).data('status')+'|'+$(this).data('sprint')+'|'+$(this).data('version');
|
||||||
|
const $column = $(`[data-id='${id}']`);
|
||||||
|
if ($column.length) {
|
||||||
|
$column.append($(this));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log ('no ='+id);
|
||||||
|
$(this).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Afficher le scrum après rangement
|
||||||
|
$('.scrumContainer').css('display', 'flex');
|
||||||
|
|
||||||
|
// Filtre
|
||||||
|
$('#statusFilter').on('change',showhide);
|
||||||
|
$('#sprintFilter').on('change',showhide);
|
||||||
|
$('#versionFilter').on('change',showhide);
|
||||||
|
$('#trackerFilter').on('change',showhide);
|
||||||
|
$('#categoryFilter').on('change',showhide);
|
||||||
|
showhide();
|
||||||
|
|
||||||
|
// issueBody
|
||||||
|
$('#toggleIssueBody').on('click',toogleIssueBody);
|
||||||
|
toogleIssueBody();
|
||||||
|
|
||||||
|
// Ajuste height
|
||||||
|
adjustHeight();
|
||||||
|
window.addEventListener("resize", adjustHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchAndRenderIssue(issueId) {
|
||||||
|
url='{{path('app_issue_view',{id:'xxx'})}}';
|
||||||
|
url=url.replace('xxx',issueId);
|
||||||
|
console.log(url);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'html',
|
||||||
|
success: function (html) {
|
||||||
|
$('.issueContainer').html(html);
|
||||||
|
$('.issueContainer').show();
|
||||||
|
$('.scrumContainer').css('padding-left','750px');
|
||||||
|
$('.issueContainer').css('z-index','1000');
|
||||||
|
$('.issueDescription').animate({scrollTop: 0}, 0);
|
||||||
|
adjustHeight();
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Erreur lors du chargement de l’issue :', error);
|
||||||
|
$('.issueContainer').html('<div class="alert alert-danger">Erreur lors du chargement de l’issue.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
let $sourceContainer = null;
|
||||||
|
let $movedItem = null;
|
||||||
|
let originalIndex = null;
|
||||||
|
|
||||||
|
$('.versionBody').sortable({
|
||||||
|
connectWith: '.versionBody',
|
||||||
|
items: '.issueCard',
|
||||||
|
handle: '.issueId',
|
||||||
|
placeholder: 'issue-placeholder',
|
||||||
|
forcePlaceholderSize: true,
|
||||||
|
|
||||||
|
start: function (event, ui) {
|
||||||
|
$sourceContainer = ui.item.parent();
|
||||||
|
$movedItem = ui.item;
|
||||||
|
originalIndex = ui.item.index();
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function (event, ui) {
|
||||||
|
// ❗ Se déclenche même pour tri dans la même colonne
|
||||||
|
if (!event.originalEvent) return; // ← ignorer les appels indirects
|
||||||
|
const $targetContainer = $(this);
|
||||||
|
|
||||||
|
const sourceId = $sourceContainer.data('id');
|
||||||
|
const targetId = $targetContainer.data('id');
|
||||||
|
|
||||||
|
// Ne pas dupliquer l'appel si c’est un vrai "receive"
|
||||||
|
if (ui.sender) return;
|
||||||
|
|
||||||
|
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
receive: function (event, ui) {
|
||||||
|
const $targetContainer = $(this);
|
||||||
|
const sourceId = $sourceContainer.data('id');
|
||||||
|
const targetId = $targetContainer.data('id');
|
||||||
|
|
||||||
|
sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendUpdate(sourceId, targetId, $sourceContainer, $targetContainer, $movedItem, originalIndex) {
|
||||||
|
const sourceIssues = $sourceContainer.find('.issueCard').map(function () {
|
||||||
|
return $(this).data('id');
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
const targetIssues = $targetContainer.find('.issueCard').map(function () {
|
||||||
|
return $(this).data('id');
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
url='{{path('app_issue_order',{id:'xxx'})}}';
|
||||||
|
url=url.replace('xxx',$movedItem.data('id'));
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
sourceIssues: sourceIssues,
|
||||||
|
targetIssues: targetIssues
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
console.log('Déplacement réussi', response);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error('Erreur AJAX', xhr);
|
||||||
|
|
||||||
|
// Annuler le déplacement
|
||||||
|
if ($movedItem && $sourceContainer) {
|
||||||
|
const items = $sourceContainer.children();
|
||||||
|
if (originalIndex >= items.length) {
|
||||||
|
$sourceContainer.append($movedItem);
|
||||||
|
} else {
|
||||||
|
$movedItem.insertBefore(items.eq(originalIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user