Files
ninemine/templates/project/view.html.twig

605 lines
19 KiB
Twig
Raw Normal View History

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;
}
.issueContainer {
position:fixed;
width:750px;
}
.filtreContainer{
2025-07-09 08:52:18 +02:00
position:fixed;
2025-07-07 17:30:12 +02:00
display:flex;
width:300px;
flex-direction:column;
background-color: var(--bs-dark);
padding:5px;
2025-07-09 08:52:18 +02:00
z-index: 900;
}
.scrumContainer {
display:none;
padding-left:300px;
2025-07-07 17:30:12 +02:00
}
.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-09 08:52:18 +02:00
@keyframes pulse {
0% { transform: scale(1); }
25% { transform: scale(1.1); }
50% { transform: scale(1); }
75% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.pulse-highlight {
animation: pulse 1.5s ease-in-out 1;
z-index: 1000;
position: relative;
}
2025-07-07 22:40:18 +02:00
2025-07-07 17:30:12 +02:00
</style>
{% endblock %}
{% block body %}
<div class='issueContainer'>
</div>
2025-07-09 08:52:18 +02:00
<div class='filtreContainer'>
<label>Issue</label>
<input type="number" id="issueSearchInput" class="form-control" placeholder="Rechercher une issue" />
<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>
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
<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>
<label>Détail Issue</label>
<select id="detailFilter" class="select2 form-select" tabindex="-1" aria-hidden="true">
<option value="0">Non</option>
<option value="1">Oui</option>
</select>
</div>
<div class='scrumContainer'>
2025-07-07 17:30:12 +02:00
<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'>
2025-07-09 08:52:18 +02:00
<i class='fas fa-eye' onClick='viewIssue({{issue.id}})'></i>
2025-07-07 17:30:12 +02:00
<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>
2025-07-09 08:52:18 +02:00
const projectId = '{{ project.id }}';
const viewedIssueKey = `project_${projectId}_viewedIssue`;
const filterIds = [
'statusFilter',
'sprintFilter',
'versionFilter',
'trackerFilter',
'categoryFilter',
'detailFilter'
];
// Pulse
function highlightAndScroll($element) {
if (!$element || !$element.length) return;
if (!$element.is(':visible')) return;
// Scroll vertical (retourne une promesse)
const scrollTopPromise = new Promise(resolve => {
$('html, body').animate({
scrollTop: $element.offset().top - 100
}, 500, resolve);
});
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
// Scroll horizontal (retourne une promesse)
const scrollLeftPromise = new Promise(resolve => {
$('html, body').animate({
scrollLeft: $element.offset().left - 315
}, 500, resolve);
});
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
// Attendre que les deux scrolls soient terminés
Promise.all([scrollTopPromise, scrollLeftPromise]).then(() => {
$element.addClass('pulse-highlight');
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
setTimeout(() => {
$element.removeClass('pulse-highlight');
}, 3000); // Ajuster selon la durée de l'animation CSS
2025-07-07 17:30:12 +02:00
});
}
2025-07-09 08:52:18 +02:00
// Ranger les issues dans status / sprint / version
$(function () {
2025-07-07 17:30:12 +02:00
$('.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();
}
});
$('.scrumContainer').css('display', 'flex');
2025-07-09 08:52:18 +02:00
});
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
// Ajuster les dom en fonction de la window
function adjustHeight() {
console.log('adjustHeight');
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
let ele = document.querySelector(".issueDescription");
if (ele) {
const top = ele.getBoundingClientRect().top;
ele.style.height = (window.innerHeight - top) + "px";
}
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
ele = document.querySelector(".filtreContainer");
if (ele) {
const top = ele.getBoundingClientRect().top;
ele.style.height = (window.innerHeight - top) + "px";
}
}
window.addEventListener("resize", adjustHeight);
window.addEventListener("load", function () {
2025-07-07 17:30:12 +02:00
adjustHeight();
});
2025-07-09 08:52:18 +02:00
// Recherche Issue
$(function () {
$('#issueSearchInput').on('input', function () {
const value = $(this).val().trim();
if (!value || isNaN(value)) return;
const $target = $(`.issueCard[data-id="${value}"]`);
if ($target.length > 0) {
highlightAndScroll($target);
} else {
// Non trouvé → optionnel
// hideIssue();
console.warn('Issue non trouvée');
}
});
});
// Affichage Issue
function viewIssue(issueId) {
localStorage.setItem(viewedIssueKey, issueId);
2025-07-07 17:30:12 +02:00
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 lissue :', error);
$('.issueContainer').html('<div class="alert alert-danger">Erreur lors du chargement de lissue.</div>');
}
});
}
2025-07-09 08:52:18 +02:00
function hideIssue() {
localStorage.removeItem(viewedIssueKey);
$('.issueContainer').hide();
$('.scrumContainer').css('padding-left','0px');
}
// Filtre sur les issues et backup des filtres en localstorage
$(function () {
// Fonction pour construire une clé unique par projet
const getKey = (id) => `project_${projectId}_${id}`;
// Initialiser les sélections depuis localStorage
filterIds.forEach(id => {
const saved = localStorage.getItem(getKey(id));
if (saved) {
const values = JSON.parse(saved);
$(`#${id}`).val(values).trigger('change');
}
});
// Afficher / Cacher les issues
function showhide() {
// Backup des filtres en localstorage
filterIds.forEach(id => {
const selected = $(`#${id}`).val() || [];
localStorage.setItem(getKey(id), JSON.stringify(selected));
});
// 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();
});
// Filtre detail
selected = $('#detailFilter').val();
if(selected=="0")
$('.issueBody').hide();
else
$('.issueBody').show();
// Afficher l'issue
const savedIssueId = localStorage.getItem(viewedIssueKey);
if (savedIssueId) {
viewIssue(savedIssueId);
}
}
// Écouteurs sur tous les filtres
filterIds.forEach(id => {
$(`#${id}`).on('change', showhide);
});
// Lancer une première fois après initialisation
showhide();
});
2025-07-07 17:30:12 +02:00
2025-07-09 08:52:18 +02:00
// Déplacement des issues
2025-07-07 17:30:12 +02:00
$(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 %}