diff --git a/index.html b/index.html
index d18b6f0..5eb7017 100644
--- a/index.html
+++ b/index.html
@@ -76,7 +76,6 @@
-
-
-
-
-
-
Vue :
-
-
-
-
-
-
-
+
+
-
-
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 += `
`;
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