This commit is contained in:
2025-07-09 22:12:43 +02:00
parent 0a178899d7
commit 1cc4db4943
5 changed files with 207 additions and 28 deletions

View File

@ -38,7 +38,7 @@ class IssueController extends AbstractController
} }
#[Route('/user/issue/order/{id}', name: 'app_issue_order', methods: ['POST'])] #[Route('/user/issue/order/{id}', name: 'app_issue_order', methods: ['POST'])]
public function orderIssue(int $id, Request $request): JsonResponse public function orderIssue(int $id, Request $request, IssueRepository $issueRepository): JsonResponse
{ {
$data = $request->request; $data = $request->request;
@ -57,8 +57,8 @@ class IssueController extends AbstractController
$allowed = false; $allowed = false;
if (in_array($id, $targetIssues)) { if (in_array($id, $targetIssues)) {
// Verifier que l'on a la permission de changer de statut // Verifier que l'on a la permission de changer de statut
$rissue = $this->redmineService->getIssue($id, $this->getParameter('redmineApikey')); $issue = $issueRepository->find($id);
foreach ($rissue['allowed_statuses'] as $status) { foreach ($issue->getRedmine()['allowed_statuses'] as $status) {
if ($status['id'] == $targetStatus) { if ($status['id'] == $targetStatus) {
$allowed = true; $allowed = true;
break; break;

View File

@ -3,6 +3,8 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\IssueRepository; use App\Repository\IssueRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: IssueRepository::class)] #[ORM\Entity(repositoryClass: IssueRepository::class)]
@ -32,6 +34,25 @@ class Issue
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private Project $project; private Project $project;
#[ORM\ManyToOne(targetEntity: Issue::class, inversedBy: 'childs')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Issue $parent = null;
#[ORM\OneToMany(targetEntity: Issue::class, mappedBy: 'parent', cascade: ['persist'])]
#[ORM\OrderBy([
'rowstatus' => 'ASC',
'rowsprint' => 'DESC',
'rowversion' => 'DESC',
'rowissue' => 'ASC',
'id' => 'DESC',
])]
private Collection $childs;
public function __construct()
{
$this->childs = new ArrayCollection();
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -115,4 +136,24 @@ class Issue
return $this; return $this;
} }
public function getParent(): ?Issue
{
return $this->parent;
}
public function setParent(?Issue $issue): self
{
$this->parent = $issue;
return $this;
}
/**
* @return Collection<int, Issue>
*/
public function getChilds(): Collection
{
return $this->childs;
}
} }

View File

@ -238,6 +238,15 @@ class RedmineService
} }
} }
} }
// Parent
foreach ($project->getIssues() as $issue) {
$parent = null;
if (array_key_exists('parent', $issue->getRedmine())) {
$parent = $this->issueRepository->find($issue->getRedmine()['parent']['id']);
}
$issue->setParent($parent);
}
} }
public function getProjectIssues(int $projectId, string $apiKey, ?\DateTimeInterface $updatedSince = null): array public function getProjectIssues(int $projectId, string $apiKey, ?\DateTimeInterface $updatedSince = null): array
@ -251,7 +260,7 @@ class RedmineService
'project_id' => $projectId, 'project_id' => $projectId,
'limit' => $limit, 'limit' => $limit,
'offset' => $offset, 'offset' => $offset,
'status_id' => '*', // Inclure toutes les issues 'status_id' => '*',
]; ];
if (null !== $updatedSince) { if (null !== $updatedSince) {
@ -274,6 +283,7 @@ class RedmineService
$issues = $data['issues'] ?? []; $issues = $data['issues'] ?? [];
foreach ($issues as $key => $issue) { foreach ($issues as $key => $issue) {
$issues[$key] = $this->getIssue($issue['id'], $apiKey);
$issues[$key]['sprint'] = $this->getIssueAgile($issue['id'], $apiKey); $issues[$key]['sprint'] = $this->getIssueAgile($issue['id'], $apiKey);
} }

View File

@ -1,7 +1,7 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<div class="mb-0 d-flex" style="align-items:baseline"> <div class="mb-0 d-flex" style="align-items:baseline">
<h5 style="flex-grow:1">#{{ issue.redmine.id }} {{ issue.redmine.subject }}</h5> <h5 style="flex-grow:1">#{{ issue.redmine.id }} = {{ issue.redmine.subject }}</h5>
<a href="{{redmineUrl}}/issues/{{issue.id}}" target="_blank" class="btn btn-primary"><i class="fas fa-eye"></i></a> <a href="{{redmineUrl}}/issues/{{issue.id}}" target="_blank" class="btn btn-primary"><i class="fas fa-eye"></i></a>
<div class="btn btn-secondary" onClick="hideIssue()"><i class="fas fa-window-close"></i></div> <div class="btn btn-secondary" onClick="hideIssue()"><i class="fas fa-window-close"></i></div>
@ -12,8 +12,10 @@
<div class="issueDescription card-body" style="height:500px;overflow-y:auto"> <div class="issueDescription card-body" style="height:500px;overflow-y:auto">
<div class="d-flex"> <div class="d-flex">
<div class="mb-3" style="flex-grow:0.5"> <div class="mb-3" style="flex-grow:0.5">
<strong>Statut :</strong> {{ issue.redmine.status.name }}<br> <strong>Tracker =</strong> {{issue.redmine.tracker.name}}<br>
<strong>Priorité :</strong> {{ issue.redmine.priority.name }}<br><br> <strong>Catégorie =</strong> {{issue.redmine.category is defined?issue.redmine.category.name:''}}<br>
<strong>Statut =</strong> {{ issue.redmine.status.name }}<br>
<strong>Priorité =</strong> {{ issue.redmine.priority.name }}<br><br>
{% set sprintName = null %} {% set sprintName = null %}
{% for sprint in issue.project.redmine.sprints %} {% for sprint in issue.project.redmine.sprints %}
@ -22,21 +24,23 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if sprintName %} {% if sprintName %}
<strong>Sprint :</strong> {{sprintName}} (Position {{ issue.redmine.sprint.position }})<br> <strong>Sprint =</strong> {{sprintName}} (Position {{ issue.redmine.sprint.position }})<br>
{% else %} {% else %}
<strong>Sprint :</strong> Aucun (Position {{ issue.redmine.sprint.position }})<br> <strong>Sprint =</strong> Aucun (Position {{ issue.redmine.sprint.position }})<br>
{% endif %} {% endif %}
<strong>Version Cible :</strong> {{(issue.redmine.fixed_version is defined?issue.redmine.fixed_version.name:'Aucune')}}<br> <strong>Version Cible =</strong> {{(issue.redmine.fixed_version is defined?issue.redmine.fixed_version.name:'Aucune')}}<br>
<strong>Story Point :</strong> {{issue.redmine.sprint.story_points}}<br> <strong>Story Point =</strong> {{issue.redmine.sprint.story_points}}<br>
</div> </div>
<div> <div>
<strong>Auteur :</strong> {{ issue.redmine.author.name }}<br> <strong>Auteur =</strong> {{ issue.redmine.author.name }}<br>
<strong>Progression :</strong> {{ issue.redmine.done_ratio }}%<br> <strong>Progression =</strong> {{ issue.redmine.done_ratio }}%<br>
<strong>Créé le :</strong>{{ issue.redmine.created_on|date('d/m/Y H:i') }}<br> <strong>Créé le =</strong>{{ issue.redmine.created_on|date('d/m/Y H:i') }}<br>
<strong>Mis à jour le :</strong>{{ issue.redmine.updated_on|date('d/m/Y H:i') }}<br> <strong>Mis à jour le =</strong>{{ issue.redmine.updated_on|date('d/m/Y H:i') }}<br>
<strong>Date de début :</strong> {{ issue.redmine.start_date|date('d/m/Y') }}<br> <strong>Date de début =</strong> {{ issue.redmine.start_date|date('d/m/Y') }}<br>
<strong>Date de fin :</strong> {{ issue.redmine.due_date|date('d/m/Y') }}<br> <strong>Date de fin =</strong> {{ issue.redmine.due_date|date('d/m/Y') }}<br><br>
<strong>Affecté à =</strong> {{(issue.redmine.assigned_to is defined?issue.redmine.assigned_to.name:'')}}<br>
</div> </div>
</div> </div>
@ -71,7 +75,7 @@
</div> </div>
{% endif %} {% endif %}
{{dump(issue.redmine)}} {{dump(issue)}}
<br><br>
</div> </div>
<br><br>
</div> </div>

View File

@ -109,9 +109,42 @@
cursor: pointer; cursor: pointer;
} }
.issueSubject{ .issueTitle{
zoom: 70%; zoom: 70%;
flex-grow: 1; flex-grow: 1;
cursor: pointer;
line-height:18px;
}
.issueParent{
font-size:12px;
line-height:12px;
color:var(--bs-green);
}
.issueBody {
font-size:10px;
display:flex;
flex-direction: column;
border-top: 1px solid var(--bs-gray-100);
margin-top:5px;
padding-top:5px;
}
.issueChilds {
font-size:10px;
display:flex;
flex-direction: column;
border-top: 1px solid var(--bs-gray-100);
margin-top:5px;
padding-top:5px;
line-height:11px;
}
.issueChild {
display:flex;
cursor: pointer;
margin-top:5px;
} }
.issueContainer table { .issueContainer table {
@ -140,11 +173,14 @@
} }
.pulse-highlight { .pulse-highlight {
animation: pulse 1.5s ease-in-out 1; animation: pulse 1.5s ease-in-out 1;
z-index: 1000; z-index: 1000;
position: relative; position: relative;
} }
.through {
text-decoration: line-through;
}
</style> </style>
{% endblock %} {% endblock %}
@ -205,6 +241,15 @@
<option value="0">Non</option> <option value="0">Non</option>
<option value="1">Oui</option> <option value="1">Oui</option>
</select> </select>
<table style="margin-top:30px;">
{% for sprint in project.redmine.sprints|reverse %}
{% if sprint.story_points is defined and sprint.id not in project.hiddensprints %}
<tr><td style="padding:2px">{{sprint.name}}</td><td style="padding:2px; text-align: center;">{{sprint.story_points.total}}</td></tr>
{% endif %}
{% endfor %}
</table>
</div> </div>
<div class='scrumContainer'> <div class='scrumContainer'>
@ -270,17 +315,38 @@
<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="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='issueHeader'>
<div class='issueId'>#{{issue.id}}</div> <div class='issueId'>#{{issue.id}}</div>
<div class='issueSubject'>{{issue.redmine.subject}}</div> <div class='issueTitle'>
{% if issue.parent %}
<div class='issueParent' data-id='{{issue.parent.id}}'>#{{issue.parent.id}} = {{issue.parent.redmine.subject}}</div>
{% endif %}
<div class='issueSubject'>
{{issue.redmine.subject}}
</div>
</div>
<div class='issueAction'> <div class='issueAction'>
<i class='fas fa-eye' onClick='viewIssue({{issue.id}})'></i> <i class='fas fa-eye' onClick='viewIssue({{issue.id}})'></i>
<verysmall>{{issue.redmine.sprint.story_points}}</verysmall> <verysmall>{{issue.redmine.sprint.story_points}}</verysmall>
</div> </div>
</div> </div>
<div class='issueBody'> <div class='issueBody'>
sprint = {{issue.rowsprint}}<br> <div><strong>Tracker =</strong> {{issue.redmine.tracker.name}}</div>
version = {{issue.rowversion}}<br> <div><strong>Catégorie =</strong> {{issue.redmine.category is defined?issue.redmine.category.name:''}}</div>
status = {{issue.redmine.status.name}} <div><strong>Affecté à =</strong> {{(issue.redmine.assigned_to is defined?issue.redmine.assigned_to.name:'')}}</div>
<div><strong>Créé le =</strong> {{ issue.redmine.created_on|date('d/m/Y H:i') }}</div>
<div><strong>Mis à jour le =</strong> {{ issue.redmine.updated_on|date('d/m/Y H:i') }}</div>
</div> </div>
{% if issue.childs is not empty %}
<div class='issueChilds'>
{% for child in issue.childs %}
<div class='issueChild {{issue.redmine.status.is_closed?'through':''}}' data-id='{{child.id}}'>
<div>#{{child.id}}</div>
<div style='padding-left:5px;'>{{child.redmine.subject}}</div>
</div>
{% endfor %}
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -291,6 +357,8 @@
<script> <script>
const projectId = '{{ project.id }}'; const projectId = '{{ project.id }}';
const viewedIssueKey = `project_${projectId}_viewedIssue`; const viewedIssueKey = `project_${projectId}_viewedIssue`;
const bodyIssueKey = `project_${projectId}_bodyIssue`;
const filterIds = [ const filterIds = [
'statusFilter', 'statusFilter',
'sprintFilter', 'sprintFilter',
@ -325,7 +393,7 @@
setTimeout(() => { setTimeout(() => {
$element.removeClass('pulse-highlight'); $element.removeClass('pulse-highlight');
}, 3000); // Ajuster selon la durée de l'animation CSS }, 1500); // Ajuster selon la durée de l'animation CSS
}); });
} }
@ -385,6 +453,52 @@
}); });
}); });
// Affichage Body Issue
function getVisibleIssueIds() {
const stored = localStorage.getItem(bodyIssueKey);
return stored ? JSON.parse(stored) : [];
}
function saveVisibleIssueIds(ids) {
localStorage.setItem(bodyIssueKey, JSON.stringify(ids));
}
// Parent / Child
$(document).on('click', '.issueParent', function () {
const value = $(this).data('id');
const $target = $(`.issueCard[data-id="${value}"]`);
if ($target.length > 0) {
highlightAndScroll($target);
}
});
$(document).on('click', '.issueChild', function () {
const value = $(this).data('id');
const $target = $(`.issueCard[data-id="${value}"]`);
if ($target.length > 0) {
highlightAndScroll($target);
}
});
// Toggle issueBody
$(document).on('click', '.issueSubject', function () {
const $card = $(this).closest('.issueCard');
const $body = $card.find('.issueBody');
const issueId = $card.data('id').toString();
let visibleIds = getVisibleIssueIds();
if ($body.is(':visible')) {
$body.slideUp(200);
visibleIds = visibleIds.filter(id => id !== issueId);
} else {
$body.slideDown(200);
if (!visibleIds.includes(issueId)) {
visibleIds.push(issueId);
}
}
saveVisibleIssueIds(visibleIds);
});
// Affichage Issue // Affichage Issue
function viewIssue(issueId) { function viewIssue(issueId) {
localStorage.setItem(viewedIssueKey, issueId); localStorage.setItem(viewedIssueKey, issueId);
@ -510,6 +624,16 @@
if (savedIssueId) { if (savedIssueId) {
viewIssue(savedIssueId); viewIssue(savedIssueId);
} }
// Affiche bodyIssue
const visibleIds = getVisibleIssueIds();
visibleIds.forEach(id => {
const $card = $(`.issueCard[data-id="${id}"]`);
const $body = $card.find('.issueBody');
if ($body.length && !$body.is(':visible')) {
$body.show(); // ou .slideDown(200) si tu veux de l'anim
}
});
} }
// Écouteurs sur tous les filtres // Écouteurs sur tous les filtres