Files
ninemine/templates/project/view.html.twig
2025-07-09 08:52:18 +02:00

605 lines
19 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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{
position:fixed;
display:flex;
width:300px;
flex-direction:column;
background-color: var(--bs-dark);
padding:5px;
z-index: 900;
}
.scrumContainer {
display:none;
padding-left:300px;
}
.containerStatus{
display:flex;
//width:10000px;
overflow-x:scroll;
}
.statusCard {
width:300px;
padding: 15px 10px 0px 10px;
}
.statusCard 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;
}
.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;
}
@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;
}
</style>
{% endblock %}
{% block body %}
<div class='issueContainer'>
</div>
<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>
<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'>
<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='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>
</div>
{% endfor %}
</div>
{% endblock %}
{% block javascripts %}
<script>
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);
});
// Scroll horizontal (retourne une promesse)
const scrollLeftPromise = new Promise(resolve => {
$('html, body').animate({
scrollLeft: $element.offset().left - 315
}, 500, resolve);
});
// Attendre que les deux scrolls soient terminés
Promise.all([scrollTopPromise, scrollLeftPromise]).then(() => {
$element.addClass('pulse-highlight');
setTimeout(() => {
$element.removeClass('pulse-highlight');
}, 3000); // Ajuster selon la durée de l'animation CSS
});
}
// Ranger les issues dans status / sprint / version
$(function () {
$('.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');
});
// Ajuster les dom en fonction de la window
function adjustHeight() {
console.log('adjustHeight');
let ele = document.querySelector(".issueDescription");
if (ele) {
const top = ele.getBoundingClientRect().top;
ele.style.height = (window.innerHeight - top) + "px";
}
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 () {
adjustHeight();
});
// 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);
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>');
}
});
}
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();
});
// Déplacement des issues
$(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) {
console.log("UPDATE");
if (!event.originalEvent) return;
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) {
console.log(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));
}
}
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();
}
});
}
});
</script>
{% endblock %}