';
let body = '';
filteredTickets.forEach(t => {
const dueDateValue = t.dueDate ? t.dueDate.substring(0, 10) : '';
body += `
${escHtml(t.key)}
${escHtml(t.summary)}
${escHtml(t.status)}
${escHtml(t.assignee) || '-'}
${escHtml(t.priority)}
${escHtml(t.issueType)}
`;
if (goLiveColName) {
const glVal = t[goLiveColName] ? t[goLiveColName].substring(0, 10) : '';
body += `
`;
}
body += `
`;
});
document.getElementById('tableBody').innerHTML = body || '
Aucun résultat
';
}
function getStatusColor(status) {
const s = (status || '').toLowerCase();
if (s.includes('done') || s.includes('ferm') || s.includes('rsolu')) return '#00e68a';
if (s.includes('progress') || s.includes('en cours') || s.includes('review')) return '#4dc8ff';
if (s.includes('to do') || s.includes('open') || s.includes('backlog')) return '#ffb84d';
if (s.includes('block')) return '#ff4d6a';
return '#7a9488';
}
// --- ROADMAP (GANTT) ---
function setGanttView(view) {
currentGanttView = 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) {
preview.innerHTML = `
Aucun ticket à afficher.
`;
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";
// 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;
// 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 && !isNaN(d1.getTime())) || (d2 && !isNaN(d2.getTime()));
});
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() + 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) {
let label = "", key = "";
if (currentGanttView === 'month') {
label = tempDate.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' });
key = `${tempDate.getFullYear()}-${tempDate.getMonth()}`;
tempDate.setMonth(tempDate.getMonth() + 1);
} else if (currentGanttView === 'quarter') {
const q = Math.floor(tempDate.getMonth() / 3) + 1;
label = `Q${q} ${tempDate.getFullYear()}`;
key = `${tempDate.getFullYear()}-Q${q}`;
tempDate.setMonth(tempDate.getMonth() + 3);
} else if (currentGanttView === 'year') {
label = `${tempDate.getFullYear()}`;
key = `${tempDate.getFullYear()}`;
tempDate.setFullYear(tempDate.getFullYear() + 1);
}
timeScale.push({ key, label });
}
const getXPos = (date) => {
if (!date) return 0;
const d = new Date(date);
let index = -1;
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);
};
// 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 += `
Aujourd'hui
`;
}
// Header
html += `
Assigné / Ticket (Titre)
`;
timeScale.forEach(ts => { html += `
${ts.label}
`; });
html += `
`;
// Corps du Gantt
Object.keys(grouped).sort().forEach(assignee => {
// Header de groupe
html += `
${assignee}
`;
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;
const start = dDue || dLive;
const end = dLive || dDue;
const x1 = getXPos(start);
const x2 = getXPos(end);
// 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 += `
${escHtml(shortTitle)}
${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', block: 'nearest' });
}, 150);
}
// --- TAB & FILTER ---
function switchTab(id, btn) {
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
btn.classList.add('active'); document.getElementById(`panel-${id}`).classList.add('active');
if(id === 'roadmap') renderRoadmap();
}
function filterTable() {
const q = document.getElementById('searchInput').value.toLowerCase();
filteredTickets = q ? allTickets.filter(t => Object.values(t).some(v => v && String(v).toLowerCase().includes(q))) : [...allTickets];
renderTable();
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,'>');
return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, m => {
let c = 'json-number'; if(/^"/.test(m)) c = /:$/.test(m)?'json-key':'json-string'; else if(/true|false/.test(m)) c='json-bool'; else if(/null/.test(m)) c='json-null';
return `${m}`;
});
}
function downloadJSON() {
const blob = new Blob([JSON.stringify(allTickets, null, 2)], {type:'application/json'});
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `export-${Date.now()}.json`; a.click();
}
function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(() => showToast('Copié !')); }
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.");
});