Files
ninemine/templates/project/view.html.twig
2025-07-25 16:09:36 +02:00

751 lines
25 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;
}
simplemain {
padding-top:80px;
}
content {
padding:0px;
overflow:auto;
}
.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;
width:10000px;
}
.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;
}
.issueTitle{
zoom: 70%;
flex-grow: 1;
cursor: pointer;
line-height:18px;
}
.issueParent{
font-size:12px;
line-height:12px;
border-bottom: 1px solid;
padding-bottom:5px;
margin-bottom:5px;
font-style:italic;
}
.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 {
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;
}
.through {
text-decoration: line-through;
}
</style>
{% endblock %}
{% block simplebody %}
<div class='issueContainer'>
</div>
<div class='filtreContainer'>
<label>Issue</label>
<input type="number" id="issueSearchInput" class="form-control" placeholder="Rechercher une issue" />
<a href="{{redmineUrl}}/projects/{{project.id}}/issues/new" target="_blank" class="btn btn-primary btn-sm"><i class="fas fa-file"></i> Nouvelle Demande</a>
<label style="margin-top:30px">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>
<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'>
{% for status in project.redmine.issue_statuses %}
{% if status.id not in project.hiddenstatuses %}
<div class='statusCard statusCard{{status.id}}'>
<h2>
{% set label= label(status.name) %}
{{ label}}
<br><small>{{ label != status.name ? status.name : '&nbsp;' }}</small>
</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 %}
{% for issue in issues %}
<div class="issueCard {{issue.color}} 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.versionId}}' data-id='{{issue.id}}'>
<div class='issueHeader'>
<div class='issueId'>#{{issue.id}}</div>
<div class='issueTitle'>
{% if issue.parent %}
<div class='issueParent' data-id='{{issue.parent.id}}' data-bs-placement="right" data-bs-toggle="tooltip" data-bs-html="true" title="
<strong>Tracker :</strong> {{issue.parent.redmine.tracker.name}}<br>
<strong>Catégorie :</strong> {{issue.parent.categoryName}}<br>
<strong>Statut :</strong> {{issue.parent.redmine.status.name}}<br>
<strong>Sprint :</strong> {{issue.parent.sprintName}}<br>
<strong>Version :</strong> {{issue.parent.versionName}}<br>">
#{{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'>
<div><strong>Tracker =</strong> {{issue.redmine.tracker.name}}</div>
<div><strong>Catégorie =</strong> {{issue.categoryName}}</div>
<div><strong>Affecté à =</strong> {{issue.assignedName}}</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}}' data-bs-placement="right" data-bs-toggle="tooltip" data-bs-html="true" title="
<strong>Tracker :</strong> {{child.redmine.tracker.name}}<br>
<strong>Catégorie :</strong> {{child.redmine.category is defined?child.redmine.category.name:'Aucune'}}<br>
<strong>Statut :</strong> {{child.redmine.status.name}}<br>
<strong>Sprint :</strong> {{child.sprintName}}<br>
<strong>Version :</strong> {{child.versionName}}<br>">
<div>#{{child.id}}</div>
<div style='padding-left:5px;'>{{child.redmine.subject}}</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}
{% block javascripts %}
<script>
const projectId = '{{ project.id }}';
const viewedIssueKey = `project_${projectId}_viewedIssue`;
const bodyIssueKey = `project_${projectId}_bodyIssue`;
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 - 150
}, 500, resolve);
});
// Scroll horizontal (retourne une promesse)
let paddingLeft = parseInt($('.scrumContainer').css('padding-left').replace('px', ''), 10);
const scrollLeftPromise = new Promise(resolve => {
$('html, body').animate({
scrollLeft: $element.offset().left - paddingLeft
}, 500, resolve);
});
// Attendre que les deux scrolls soient terminés
Promise.all([scrollTopPromise, scrollLeftPromise]).then(() => {
$element.addClass('pulse-highlight');
setTimeout(() => {
$element.removeClass('pulse-highlight');
}, 1500); // 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 ='+$(this).data('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 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);
url='{{path('app_issue_view',{id:'xxx'})}}';
url=url.replace('xxx',issueId);
$.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','300px');
}
// 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);
}
// 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
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,
scroll: true, // 👈 Active le scroll auto
scrollSensitivity: 50, // 👈 distance (px) à partir du bord pour déclencher le scroll
scrollSpeed: 100, // 👈 vitesse de défilement
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 %}