From 25357e52c0ee0f78aa54ecf81cc9800614bbc1f9 Mon Sep 17 00:00:00 2001 From: gautier Date: Wed, 13 May 2026 16:35:20 +0200 Subject: [PATCH] cookies, drawio, roadmap --- index.html | 41 +++--- public/css/style.css | 84 +++++++++++- public/js/script.js | 306 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 378 insertions(+), 53 deletions(-) diff --git a/index.html b/index.html index d18b6f0..5eb7017 100644 --- a/index.html +++ b/index.html @@ -76,7 +76,6 @@
-
@@ -98,32 +97,36 @@
-
-
-
-
-
- Vue : -
- - - -
-
-
+
+
+ Vue : +
+ + + +
+
+ +
+ + EXPORT DRAW.IO (XML) + +
+ +
-
+
- -
diff --git a/public/css/style.css b/public/css/style.css index 3e865c7..76dc0fe 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -977,4 +977,86 @@ GANTT background-color: var(--primary); color: white; border-color: var(--primary); -} \ No newline at end of file +} +/* --- 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; } + + diff --git a/public/js/script.js b/public/js/script.js index 305249e..1404093 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -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 = ' 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 = `
Aucune date disponible pour générer la roadmap.
`; + if (ticketsWithDates.length === 0) { + preview.innerHTML = `
Aucune date de Due Date ou Go Live renseignée pour les tickets filtrés.
`; 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 = `
`; + + // Ligne "Aujourd'hui" const todayX = getXPos(new Date()); if (todayX > leftWidth) { html += `
`; - html += `
Auj.
`; + html += `
Aujourd'hui
`; } - html += `
Assigné / Ticket
`; + // Header + html += `
Assigné / Ticket (Titre)
`; timeScale.forEach(ts => { html += `
${ts.label}
`; }); html += `
`; + // Corps du Gantt Object.keys(grouped).sort().forEach(assignee => { - html += `
${assignee}
`; + // Header de groupe + html += `
+
+ ${assignee} +
+
`; - // --- 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 ? '...' : ''}` : ''; - 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)`; + // Largeur de barre (minimum 150px pour afficher le badge de statut et le titre proprement) + const barWidth = Math.max(150, (x2 - x1) + 10); + + const { shortTitle, type } = parseTicketSummary(t.summary); + const typeColor = getTypeColor(type); + const statusColor = getStatusColor(t.status); html += `
-
${t.key}
+
+ ${escHtml(shortTitle)} +
+
- ${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;"> + +
+ ${escHtml(t.status)} +
+ + ${type} + ${escHtml(shortTitle)}
${timeScale.map(() => `
`).join('')}
`; }); }); + html += `
`; 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 += ` + + `; + + // Lignes verticales de repère + xmlNodes += ` + + `; + } + + // 5. LIGNE TODAY + const xToday = getX(new Date()); + xmlNodes += ` + + `; + + // 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 += ` + + `; + 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 += ` + + `; + + // Barre de Gantt + const label = `[${type}] ${escHtml(shortTitle)}`; + xmlNodes += ` + + `; + + currentY += rowHeight; + }); + currentY += 20; + }); + + // 7. ASSEMBLAGE FINAL + const finalXml = ` + + + + + + + ${xmlNodes} + + + +`; + + 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,'>'); @@ -444,4 +679,9 @@ 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()} }); \ No newline at end of file +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."); +}); \ No newline at end of file -- 2.52.0