Merge pull request 'cookies, drawio, roadmap' (#3) from gautier into main
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
+14
-11
@@ -76,7 +76,6 @@
|
||||
<div class="btn-group no-margin">
|
||||
<button class="btn btn-secondary" onclick="copyJSON()"><i class="fa-solid fa-copy"></i> Copier</button>
|
||||
<button class="btn btn-primary" onclick="downloadJSON()"><i class="fa-solid fa-file-arrow-down"></i> Telecharger</button>
|
||||
<button class="btn btn-drawio" onclick="downloadRoadmap()"><i class="fa-solid fa-diagram-project"></i> Roadmap Draw.io</button>
|
||||
<button class="btn btn-danger" onclick="clearResults()"><i class="fa-solid fa-trash"></i> Effacer</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,10 +97,7 @@
|
||||
|
||||
|
||||
<div class="tab-panel" id="panel-roadmap">
|
||||
<div class="card mb-16 p-small">
|
||||
<div class="flex-center-between">
|
||||
<div class="card mb-16 p-small">
|
||||
<div class="flex-center-between">
|
||||
<div class="toolbar-roadmap">
|
||||
<div class="view-controls">
|
||||
<span class="mr-8"><i class="fa-solid fa-magnifying-glass-plus"></i> Vue :</span>
|
||||
<div class="btn-group no-margin">
|
||||
@@ -110,20 +106,27 @@
|
||||
<button class="btn btn-secondary btn-sm" onclick="setGanttView('year')">Années</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="downloadRoadmap()">
|
||||
<i class="fa-solid fa-download"></i>Télécharger le .drawio
|
||||
|
||||
<div class="export-drawio-box">
|
||||
<span class="mr-8 export-label">
|
||||
<i class="fa-solid fa-diagram-project"></i> EXPORT DRAW.IO (XML)
|
||||
</span>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('download')">
|
||||
<i class="fa-solid fa-file-download"></i> Télécharger
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('copy')">
|
||||
<i class="fa-solid fa-copy"></i> Copier XML
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-20">
|
||||
<div id="roadmapPreview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tab-panel" id="panel-json"><div class="json-viewer" id="jsonViewer"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -978,3 +978,85 @@ GANTT
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
/* --- ALIGNEMENT ROADMAP --- */
|
||||
|
||||
/* Alignement horizontal : Vues à gauche, Export à droite */
|
||||
.toolbar-roadmap {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* On s'assure que les deux blocs enfants se placent bien */
|
||||
.toolbar-roadmap > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Style de la boîte Draw.io (le deuxième enfant) */
|
||||
.toolbar-roadmap > div:last-child {
|
||||
margin-left: auto; /* Sécurité supplémentaire pour pousser à droite */
|
||||
border: 1px solid var(--accent); /* Optionnel: pour bien la voir */
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(0, 230, 138, 0.05);
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
/* Style de la boîte verte d'export */
|
||||
.export-drawio-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
background: rgba(0, 230, 138, 0.1); /* Fond vert transparent */
|
||||
border: 1px solid var(--accent); /* Bordure verte */
|
||||
padding: 6px 15px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.export-label {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* On s'assure que les contrôles de vue restent simples */
|
||||
.view-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zoom-controls-card, .drawio-export-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.drawio-export-card {
|
||||
border-color: var(--accent-dim);
|
||||
background: rgba(0, 230, 138, 0.03);
|
||||
}
|
||||
|
||||
.zoom-label, .export-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.zoom-label { color: var(--fg-muted); }
|
||||
.export-label { color: var(--accent); }
|
||||
|
||||
.mr-4 { margin-right: 4px; }
|
||||
|
||||
|
||||
|
||||
+270
-30
@@ -8,6 +8,29 @@ let currentGanttView = 'month';
|
||||
let currentSortCol = null;
|
||||
let isAsc = true;
|
||||
|
||||
|
||||
// Sauvegarde les paramètres dans le stockage local du navigateur
|
||||
function saveSettings() {
|
||||
const settings = {
|
||||
projectKey: document.getElementById('projectKey').value,
|
||||
customFields: document.getElementById('customFields').value,
|
||||
jqlQuery: document.getElementById('jqlQuery').value
|
||||
};
|
||||
localStorage.setItem('jira_roadmap_settings', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
// Charge les paramètres au démarrage
|
||||
function loadSettings() {
|
||||
const saved = localStorage.getItem('jira_roadmap_settings');
|
||||
if (saved) {
|
||||
const settings = JSON.parse(saved);
|
||||
document.getElementById('projectKey').value = settings.projectKey || '';
|
||||
document.getElementById('customFields').value = settings.customFields || '';
|
||||
document.getElementById('jqlQuery').value = settings.jqlQuery || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- UTILITAIRES ---
|
||||
function showToast(msg, type='success') {
|
||||
const c = document.getElementById('toastContainer');
|
||||
@@ -27,6 +50,8 @@ function autoJql() {
|
||||
}
|
||||
|
||||
async function pingApi() {
|
||||
saveSettings();
|
||||
|
||||
const btn = event.target.closest('.btn');
|
||||
const orig = btn.innerHTML; btn.innerHTML = '<span class="spinner"></span> Test...'; btn.disabled = true;
|
||||
try {
|
||||
@@ -39,6 +64,8 @@ async function pingApi() {
|
||||
}
|
||||
|
||||
async function startExtraction() {
|
||||
saveSettings();
|
||||
|
||||
const btn = document.getElementById('extractBtn');
|
||||
const user = document.getElementById('username').value.trim();
|
||||
const token = document.getElementById('apiToken').value.trim();
|
||||
@@ -274,6 +301,37 @@ function setGanttView(view) {
|
||||
renderRoadmap();
|
||||
}
|
||||
|
||||
// --- FONCTIONS UTILITAIRES POUR LE GANTT ---
|
||||
|
||||
/**
|
||||
* Extrait le titre court et le type selon le pattern : "Titre - Type - Montant - ID"
|
||||
*/
|
||||
function parseTicketSummary(summary) {
|
||||
if (!summary) return { shortTitle: 'Sans titre', type: 'OTHER' };
|
||||
const parts = summary.split(' - ');
|
||||
const shortTitle = parts[0].trim();
|
||||
const typePart = parts[1] ? parts[1].trim().toUpperCase() : 'OTHER';
|
||||
|
||||
// On valide si c'est un type connu, sinon OTHER
|
||||
const type = ['PR', 'CR', 'TR'].includes(typePart) ? typePart : 'OTHER';
|
||||
return { shortTitle, type };
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la couleur de la barre selon le type de projet
|
||||
*/
|
||||
function getTypeColor(type) {
|
||||
switch(type) {
|
||||
case 'PR': return '#4dc8ff'; // Bleu info
|
||||
case 'CR': return '#ffb84d'; // Orange warning
|
||||
case 'TR': return '#a371f7'; // Violet
|
||||
default: return '#7a9488'; // Gris par défaut
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FONCTION PRINCIPALE : RENDER ROADMAP
|
||||
*/
|
||||
function renderRoadmap() {
|
||||
const preview = document.getElementById('roadmapPreview');
|
||||
if (!filteredTickets || filteredTickets.length === 0) {
|
||||
@@ -281,31 +339,44 @@ function renderRoadmap() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Détection dynamique de la colonne Go Live
|
||||
const goLiveColName = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "Go Live Date";
|
||||
const leftWidth = 250;
|
||||
|
||||
// Configuration des largeurs
|
||||
const leftWidth = 280; // Un peu plus large pour les titres split 0
|
||||
let colWidth = 140;
|
||||
if (currentGanttView === 'quarter') colWidth = 200;
|
||||
if (currentGanttView === 'year') colWidth = 250;
|
||||
|
||||
const allDates = filteredTickets.flatMap(t => {
|
||||
// 1. Filtrer les tickets : on ne garde que ceux qui ont au moins une date
|
||||
const ticketsWithDates = filteredTickets.filter(t => {
|
||||
const d1 = t.dueDate ? new Date(t.dueDate) : null;
|
||||
const d2 = t[goLiveColName] ? new Date(t[goLiveColName]) : null;
|
||||
return [d1, d2].filter(d => d && !isNaN(d));
|
||||
return (d1 && !isNaN(d1.getTime())) || (d2 && !isNaN(d2.getTime()));
|
||||
});
|
||||
|
||||
if (allDates.length === 0) {
|
||||
preview.innerHTML = `<div style="padding:40px; text-align:center;">Aucune date disponible pour générer la roadmap.</div>`;
|
||||
if (ticketsWithDates.length === 0) {
|
||||
preview.innerHTML = `<div style="padding:40px; text-align:center; color:var(--fg-dim);">Aucune date de Due Date ou Go Live renseignée pour les tickets filtrés.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcul des bornes du calendrier
|
||||
const allDates = ticketsWithDates.flatMap(t => [
|
||||
t.dueDate ? new Date(t.dueDate) : null,
|
||||
t[goLiveColName] ? new Date(t[goLiveColName]) : null
|
||||
].filter(d => d !== null));
|
||||
|
||||
let minDate = new Date(Math.min(...allDates));
|
||||
let maxDate = new Date(Math.max(...allDates));
|
||||
|
||||
minDate.setDate(1);
|
||||
if (currentGanttView === 'quarter') minDate.setMonth(Math.floor(minDate.getMonth() / 3) * 3);
|
||||
if (currentGanttView === 'year') minDate.setMonth(0);
|
||||
maxDate.setMonth(maxDate.getMonth() + 1);
|
||||
|
||||
maxDate.setMonth(maxDate.getMonth() + 2); // Un peu de marge au bout
|
||||
maxDate.setDate(0);
|
||||
|
||||
// Construction de l'échelle de temps
|
||||
const timeScale = [];
|
||||
let tempDate = new Date(minDate);
|
||||
while (tempDate <= maxDate) {
|
||||
@@ -334,82 +405,99 @@ function renderRoadmap() {
|
||||
if (currentGanttView === 'month') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}-${d.getMonth()}`);
|
||||
else if (currentGanttView === 'quarter') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}-Q${Math.floor(d.getMonth() / 3) + 1}`);
|
||||
else if (currentGanttView === 'year') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}`);
|
||||
|
||||
if (index === -1) return 0;
|
||||
|
||||
let ratio = 0;
|
||||
if (currentGanttView === 'month') ratio = (d.getDate() - 1) / 30;
|
||||
else if (currentGanttView === 'quarter') ratio = ((d.getMonth() % 3) * 30 + d.getDate()) / 90;
|
||||
else if (currentGanttView === 'year') ratio = (d.getMonth() * 30 + d.getDate()) / 365;
|
||||
|
||||
return leftWidth + (index * colWidth) + (ratio * colWidth);
|
||||
};
|
||||
|
||||
const grouped = filteredTickets.reduce((acc, t) => {
|
||||
// 2. Groupement par assigné (uniquement ceux qui ont des tickets visibles)
|
||||
const grouped = ticketsWithDates.reduce((acc, t) => {
|
||||
const name = t.assignee || "Non assigné";
|
||||
if (!acc[name]) acc[name] = [];
|
||||
acc[name].push(t);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Début HTML
|
||||
let html = `<div class="gantt-container"><div class="gantt-canvas" style="width: ${leftWidth + (timeScale.length * colWidth)}px;">`;
|
||||
|
||||
// Ligne "Aujourd'hui"
|
||||
const todayX = getXPos(new Date());
|
||||
if (todayX > leftWidth) {
|
||||
html += `<div class="gantt-today-line" style="left: ${todayX}px;"></div>`;
|
||||
html += `<div class="gantt-today-label" id="todayMarker" style="left: ${todayX}px;">Auj.</div>`;
|
||||
html += `<div class="gantt-today-label" id="todayMarker" style="left: ${todayX}px;">Aujourd'hui</div>`;
|
||||
}
|
||||
|
||||
html += `<div class="gantt-header"><div class="gantt-sticky-col gantt-group-name">Assigné / Ticket</div>`;
|
||||
// Header
|
||||
html += `<div class="gantt-header"><div class="gantt-sticky-col gantt-group-name" style="width:${leftWidth}px">Assigné / Ticket (Titre)</div>`;
|
||||
timeScale.forEach(ts => { html += `<div class="gantt-month-header" style="width:${colWidth}px;">${ts.label}</div>`; });
|
||||
html += `</div>`;
|
||||
|
||||
// Corps du Gantt
|
||||
Object.keys(grouped).sort().forEach(assignee => {
|
||||
html += `<div class="gantt-group-header"><div class="gantt-sticky-col gantt-group-name"><i class="fa-solid fa-user-circle mr-8"></i>${assignee}</div></div>`;
|
||||
// Header de groupe
|
||||
html += `<div class="gantt-group-header">
|
||||
<div class="gantt-sticky-col gantt-group-name" style="width:${leftWidth}px">
|
||||
<i class="fa-solid fa-user-circle mr-8"></i>${assignee}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// --- TRI CHRONOLOGIQUE PAR DUE DATE ---
|
||||
const sortedTickets = grouped[assignee].sort((a, b) => {
|
||||
const dateA = a.dueDate ? new Date(a.dueDate) : new Date(0);
|
||||
const dateB = b.dueDate ? new Date(b.dueDate) : new Date(0);
|
||||
return dateA - dateB;
|
||||
});
|
||||
const sortedTickets = grouped[assignee].sort((a, b) => (new Date(a.dueDate || 0)) - (new Date(b.dueDate || 0)));
|
||||
|
||||
sortedTickets.forEach(t => {
|
||||
const dDue = t.dueDate ? new Date(t.dueDate) : null;
|
||||
const dLive = t[goLiveColName] ? new Date(t[goLiveColName]) : null;
|
||||
if (!dDue && !dLive) return;
|
||||
|
||||
const start = dDue || dLive;
|
||||
const end = dLive || dDue;
|
||||
|
||||
const x1 = getXPos(start);
|
||||
const x2 = getXPos(end);
|
||||
const barWidth = Math.max(35, (x2 - x1) + 10);
|
||||
const color = getStatusColor(t.status);
|
||||
|
||||
// Préparation des infos pour le tooltip
|
||||
const fmtDue = dDue ? dDue.toLocaleDateString('fr-FR') : 'Non définie';
|
||||
const fmtLive = dLive ? dLive.toLocaleDateString('fr-FR') : 'Non définie';
|
||||
const description = t.description ? `\n\nDescription: ${t.description.substring(0, 150)}${t.description.length > 150 ? '...' : ''}` : '';
|
||||
// Largeur de barre (minimum 150px pour afficher le badge de statut et le titre proprement)
|
||||
const barWidth = Math.max(150, (x2 - x1) + 10);
|
||||
|
||||
const tooltipText = `Ticket: ${t.key}\nRésumé: ${t.summary}${description}\n\n📅 Due Date: ${fmtDue}\n🚀 Go Live: ${fmtLive}\n\n(Cliquez pour ouvrir dans Jira)`;
|
||||
const { shortTitle, type } = parseTicketSummary(t.summary);
|
||||
const typeColor = getTypeColor(type);
|
||||
const statusColor = getStatusColor(t.status);
|
||||
|
||||
html += `<div class="gantt-row">
|
||||
<div class="gantt-sticky-col gantt-ticket-id">${t.key}</div>
|
||||
<div class="gantt-sticky-col gantt-ticket-id" style="width:${leftWidth}px; font-size:11px;" title="${t.key}: ${t.summary}">
|
||||
${escHtml(shortTitle)}
|
||||
</div>
|
||||
|
||||
<div class="gantt-bar"
|
||||
title="${escHtml(tooltipText)}"
|
||||
onclick="window.open('${t.url}', '_blank')"
|
||||
style="left:${x1}px; width:${barWidth}px; background:${color}33; border-left:4px solid ${color}; cursor:pointer;">
|
||||
${escHtml(t.summary)}
|
||||
style="left:${x1}px; width:${barWidth}px; background:${typeColor}22; border-left:4px solid ${typeColor}; cursor:pointer; display:flex; align-items:center; gap:6px; padding:0 8px;">
|
||||
|
||||
<div style="background:${statusColor}; color:#fff; font-size:9px; padding:2px 6px; border-radius:4px; font-weight:bold; text-transform:uppercase; white-space:nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
||||
${escHtml(t.status)}
|
||||
</div>
|
||||
|
||||
<span style="color:${typeColor}; font-weight:bold; flex-shrink:0;">${type}</span>
|
||||
<span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:12px;">${escHtml(shortTitle)}</span>
|
||||
</div>
|
||||
${timeScale.map(() => `<div class="gantt-grid-col" style="width:${colWidth}px;"></div>`).join('')}
|
||||
</div>`;
|
||||
});
|
||||
});
|
||||
|
||||
html += `</div></div>`;
|
||||
preview.innerHTML = html;
|
||||
|
||||
// Focus sur la date du jour
|
||||
setTimeout(() => {
|
||||
const m = document.getElementById('todayMarker');
|
||||
if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
||||
if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}, 150);
|
||||
}
|
||||
|
||||
|
||||
// --- TAB & FILTER ---
|
||||
function switchTab(id, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||
@@ -425,6 +513,153 @@ function filterTable() {
|
||||
if(document.getElementById('panel-roadmap').classList.contains('active')) renderRoadmap();
|
||||
}
|
||||
|
||||
function exportDrawIOXML(mode) {
|
||||
try {
|
||||
// 1. Sécurité : Vérification des données
|
||||
if (!filteredTickets || filteredTickets.length === 0) {
|
||||
showToast("Aucun ticket à exporter", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const goLiveCol = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "dueDate";
|
||||
const ticketsWithDates = filteredTickets.filter(t => t.dueDate || t[goLiveCol]);
|
||||
|
||||
if (ticketsWithDates.length === 0) {
|
||||
showToast("Aucune date de fin trouvée", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Paramètres de vue (Zoom)
|
||||
const leftWidth = 250;
|
||||
const rowHeight = 50;
|
||||
let pixelPerDay = 12; // Mois
|
||||
if (currentGanttView === 'quarter') pixelPerDay = 4;
|
||||
if (currentGanttView === 'year') pixelPerDay = 1.2;
|
||||
|
||||
// 3. Calcul des bornes temporelles
|
||||
const allDates = ticketsWithDates.map(t => new Date(t.dueDate || t[goLiveCol]));
|
||||
let minDate = new Date(Math.min(...allDates));
|
||||
minDate.setDate(1);
|
||||
let maxDate = new Date(Math.max(...allDates));
|
||||
maxDate.setMonth(maxDate.getMonth() + 4);
|
||||
|
||||
const getX = (dateStr) => {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return leftWidth;
|
||||
return Math.floor(leftWidth + ((d - minDate) / 86400000 * pixelPerDay));
|
||||
};
|
||||
|
||||
let xmlNodes = '';
|
||||
|
||||
// 4. TIMELINE & LIGNES VERTICALES
|
||||
let curr = new Date(minDate);
|
||||
while (curr < maxDate) {
|
||||
const xPos = getX(curr);
|
||||
let label = "";
|
||||
|
||||
if (currentGanttView === 'month') {
|
||||
label = curr.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' }).toUpperCase();
|
||||
curr.setMonth(curr.getMonth() + 1);
|
||||
} else if (currentGanttView === 'quarter') {
|
||||
label = "Q" + (Math.floor(curr.getMonth() / 3) + 1) + " " + curr.getFullYear();
|
||||
curr.setMonth(curr.getMonth() + 3);
|
||||
} else {
|
||||
label = curr.getFullYear();
|
||||
curr.setFullYear(curr.getFullYear() + 1);
|
||||
}
|
||||
|
||||
// Titres Timeline agrandis (fontSize=14)
|
||||
xmlNodes += `<mxCell id="m_${curr.getTime()}" value="${label}" style="text;fontColor=#7a9488;fontSize=14;fontStyle=1;align=left;verticalAlign=middle" vertex="1" parent="1">
|
||||
<mxGeometry x="${xPos + 5}" y="5" width="150" height="30" as="geometry"/>
|
||||
</mxCell>`;
|
||||
|
||||
// Lignes verticales de repère
|
||||
xmlNodes += `<mxCell id="l_${curr.getTime()}" value="" style="line;strokeColor=#E0E0E0;direction=south;html=1;opacity=20" vertex="1" parent="1">
|
||||
<mxGeometry x="${xPos}" y="0" width="10" height="4000" as="geometry"/>
|
||||
</mxCell>`;
|
||||
}
|
||||
|
||||
// 5. LIGNE TODAY
|
||||
const xToday = getX(new Date());
|
||||
xmlNodes += `<mxCell id="today" value="AUJOURD'HUI" style="line;strokeColor=#FF0000;direction=south;html=1;dashed=1;strokeWidth=2;fontColor=#FF0000;fontSize=12;fontStyle=1;verticalAlign=top" vertex="1" parent="1">
|
||||
<mxGeometry x="${xToday}" y="0" width="10" height="4000" as="geometry"/>
|
||||
</mxCell>`;
|
||||
|
||||
// 6. GÉNÉRATION DES TICKETS
|
||||
let currentY = 80;
|
||||
const groups = ticketsWithDates.reduce((acc, t) => {
|
||||
const a = t.assignee || 'Non assigné';
|
||||
if (!acc[a]) acc[a] = [];
|
||||
acc[a].push(t);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.keys(groups).sort().forEach(assignee => {
|
||||
// Header Assigné
|
||||
xmlNodes += `<mxCell id="as_${currentY}" value="${assignee.toUpperCase()}" style="text;fontColor=#00e68a;fontStyle=1;fontSize=14" vertex="1" parent="1">
|
||||
<mxGeometry x="10" y="${currentY}" width="200" height="30" as="geometry"/>
|
||||
</mxCell>`;
|
||||
currentY += 40;
|
||||
|
||||
groups[assignee].forEach(t => {
|
||||
const { shortTitle, type } = parseTicketSummary(t.summary);
|
||||
const color = getTypeColor(type);
|
||||
|
||||
// Durée réelle (Création -> DueDate)
|
||||
const startDate = t.created ? new Date(t.created) : minDate;
|
||||
const endDate = new Date(t.dueDate || t[goLiveCol]);
|
||||
|
||||
const xStart = getX(startDate);
|
||||
const xEnd = getX(endDate);
|
||||
let barWidth = xEnd - xStart;
|
||||
if (barWidth < 50) barWidth = 140;
|
||||
|
||||
// Label de gauche
|
||||
xmlNodes += `<mxCell id="lbl_${currentY}" value="${escHtml(shortTitle)}" style="text;fontColor=#555555;fontSize=10;align=left;whiteSpace=wrap" vertex="1" parent="1">
|
||||
<mxGeometry x="30" y="${currentY}" width="200" height="30" as="geometry"/>
|
||||
</mxCell>`;
|
||||
|
||||
// Barre de Gantt
|
||||
const label = `[${type}] ${escHtml(shortTitle)}`;
|
||||
xmlNodes += `<mxCell id="bar_${currentY}" value="${label}" style="rounded=1;fillColor=${color};strokeColor=#FFFFFF;fontColor=#000000;fontSize=10;fontStyle=1;spacingLeft=8;align=left" vertex="1" parent="1">
|
||||
<mxGeometry x="${xStart}" y="${currentY}" width="${barWidth}" height="28" as="geometry"/>
|
||||
</mxCell>`;
|
||||
|
||||
currentY += rowHeight;
|
||||
});
|
||||
currentY += 20;
|
||||
});
|
||||
|
||||
// 7. ASSEMBLAGE FINAL
|
||||
const finalXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net">
|
||||
<diagram id="Jira" name="Roadmap">
|
||||
<mxGraphModel dx="1000" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" background="none">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
${xmlNodes}
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>`;
|
||||
|
||||
if (mode === 'download') {
|
||||
const blob = new Blob([finalXml], { type: 'text/xml' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "jira_export.drawio";
|
||||
a.click();
|
||||
} else {
|
||||
navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copié !"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast("Erreur lors de l'export XML", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function renderJSON() { document.getElementById('jsonViewer').innerHTML = syntaxHL(JSON.stringify(allTickets, null, 2)); }
|
||||
function syntaxHL(json) {
|
||||
json = json.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
@@ -445,3 +680,8 @@ function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, n
|
||||
function clearResults() { allTickets=[]; filteredTickets=[]; document.getElementById('results-section').classList.remove('visible'); }
|
||||
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();startExtraction()} });
|
||||
// Cet événement se déclenche quand le navigateur a fini de charger le HTML
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
console.log("Paramètres chargés depuis le stockage local.");
|
||||
});
|
||||
Reference in New Issue
Block a user