2025-07-07 17:30:12 +02:00
|
|
|
|
{% 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-07 22:40:18 +02:00
|
|
|
|
.statusCard h2 {
|
2025-07-07 17:30:12 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
2025-07-07 22:40:18 +02:00
|
|
|
|
|
|
|
|
|
.issueContainer table {
|
|
|
|
|
width:100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
font-family: sans-serif;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
th, td {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-07-07 17:30:12 +02:00
|
|
|
|
</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) {
|
2025-07-07 21:48:40 +02:00
|
|
|
|
console.log("UPDATE");
|
|
|
|
|
if (!event.originalEvent) return;
|
2025-07-07 17:30:12 +02:00
|
|
|
|
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: {
|
|
|
|
|
target: targetId,
|
|
|
|
|
targetIssues: targetIssues
|
|
|
|
|
},
|
|
|
|
|
success: function (response) {
|
|
|
|
|
console.log('Déplacement réussi', response);
|
|
|
|
|
},
|
|
|
|
|
error: function (xhr) {
|
2025-07-07 21:48:40 +02:00
|
|
|
|
console.log(xhr);
|
2025-07-07 17:30:12 +02:00
|
|
|
|
// Annuler le déplacement
|
|
|
|
|
if ($movedItem && $sourceContainer) {
|
|
|
|
|
const items = $sourceContainer.children();
|
|
|
|
|
if (originalIndex >= items.length) {
|
|
|
|
|
$sourceContainer.append($movedItem);
|
|
|
|
|
} else {
|
|
|
|
|
$movedItem.insertBefore(items.eq(originalIndex));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-07 21:48:40 +02:00
|
|
|
|
|
|
|
|
|
const message = xhr.responseJSON?.message || 'Une erreur est survenue lors de la requête.';
|
|
|
|
|
$('#ajaxErrorMessage').text(message);
|
|
|
|
|
|
|
|
|
|
const toast = new bootstrap.Toast(document.getElementById('ajaxErrorToast'), {
|
|
|
|
|
delay: 5000
|
|
|
|
|
});
|
|
|
|
|
toast.show();
|
2025-07-07 17:30:12 +02:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|