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

View File

@ -3,6 +3,8 @@
namespace App\Entity;
use App\Repository\IssueRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: IssueRepository::class)]
@ -32,6 +34,25 @@ class Issue
#[ORM\JoinColumn(nullable: false)]
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
{
return $this->id;
@ -115,4 +136,24 @@ class Issue
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
@ -251,7 +260,7 @@ class RedmineService
'project_id' => $projectId,
'limit' => $limit,
'offset' => $offset,
'status_id' => '*', // Inclure toutes les issues
'status_id' => '*',
];
if (null !== $updatedSince) {
@ -274,6 +283,7 @@ class RedmineService
$issues = $data['issues'] ?? [];
foreach ($issues as $key => $issue) {
$issues[$key] = $this->getIssue($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-header">
<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>
<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="d-flex">
<div class="mb-3" style="flex-grow:0.5">
<strong>Statut :</strong> {{ issue.redmine.status.name }}<br>
<strong>Priorité :</strong> {{ issue.redmine.priority.name }}<br><br>
<strong>Tracker =</strong> {{issue.redmine.tracker.name}}<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 %}
{% for sprint in issue.project.redmine.sprints %}
@ -22,21 +24,23 @@
{% endif %}
{% endfor %}
{% if sprintName %}
<strong>Sprint :</strong> {{sprintName}} (Position {{ issue.redmine.sprint.position }})<br>
<strong>Sprint =</strong> {{sprintName}} (Position {{ issue.redmine.sprint.position }})<br>
{% else %}
<strong>Sprint :</strong> Aucun (Position {{ issue.redmine.sprint.position }})<br>
<strong>Sprint =</strong> Aucun (Position {{ issue.redmine.sprint.position }})<br>
{% endif %}
<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>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>
</div>
<div>
<strong>Auteur :</strong> {{ issue.redmine.author.name }}<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>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 fin :</strong> {{ issue.redmine.due_date|date('d/m/Y') }}<br>
<strong>Auteur =</strong> {{ issue.redmine.author.name }}<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>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 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>
@ -71,7 +75,7 @@
</div>
{% endif %}
{{dump(issue.redmine)}}
{{dump(issue)}}
<br><br>
</div>
<br><br>
</div>

View File

@ -109,9 +109,42 @@
cursor: pointer;
}
.issueSubject{
.issueTitle{
zoom: 70%;
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 {
@ -140,11 +173,14 @@
}
.pulse-highlight {
animation: pulse 1.5s ease-in-out 1;
z-index: 1000;
position: relative;
animation: pulse 1.5s ease-in-out 1;
z-index: 1000;
position: relative;
}
.through {
text-decoration: line-through;
}
</style>
{% endblock %}
@ -205,6 +241,15 @@
<option value="0">Non</option>
<option value="1">Oui</option>
</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 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='issueHeader'>
<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'>
<i class='fas fa-eye' onClick='viewIssue({{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><strong>Tracker =</strong> {{issue.redmine.tracker.name}}</div>
<div><strong>Catégorie =</strong> {{issue.redmine.category is defined?issue.redmine.category.name:''}}</div>
<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>
{% 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>
{% endfor %}
</div>
@ -291,6 +357,8 @@
<script>
const projectId = '{{ project.id }}';
const viewedIssueKey = `project_${projectId}_viewedIssue`;
const bodyIssueKey = `project_${projectId}_bodyIssue`;
const filterIds = [
'statusFilter',
'sprintFilter',
@ -325,7 +393,7 @@
setTimeout(() => {
$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
function viewIssue(issueId) {
localStorage.setItem(viewedIssueKey, issueId);
@ -510,6 +624,16 @@
if (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