Change mapping status and task colors in UX
This commit is contained in:
+31
-31
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -24,53 +24,53 @@
|
|||||||
<div class="header-icon"><i class="fa-solid fa-bolt"></i></div>
|
<div class="header-icon"><i class="fa-solid fa-bolt"></i></div>
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h1>Jira Ticket Extractor</h1>
|
<h1>Jira Ticket Extractor</h1>
|
||||||
<p>Extraction via API locale — Proxy transparent</p>
|
<p>Extraction via local API — Transparent proxy</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="badge-api"><i class="fa-solid fa-server"></i>API : localhost:3000</div>
|
<div class="badge-api"><i class="fa-solid fa-server"></i>API : localhost:3000</div>
|
||||||
<button class="theme-toggle-btn" id="themeToggleBtn" onclick="toggleTheme()" aria-label="Changer de thème" title="Basculer vers le mode clair / sombre">
|
<button class="theme-toggle-btn" id="themeToggleBtn" onclick="toggleTheme()" aria-label="Toggle theme" title="Switch between light and dark mode">
|
||||||
<i class="fa-solid fa-sun" id="themeIcon"></i>
|
<i class="fa-solid fa-sun" id="themeIcon"></i>
|
||||||
<span id="themeLabel">Mode clair</span>
|
<span id="themeLabel">Light mode</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="card-form">
|
<section class="card-form">
|
||||||
<div class="card-title"><i class="fa-solid fa-filter"></i> Paramètres d'extraction</div>
|
<div class="card-title"><i class="fa-solid fa-filter"></i> Extraction Settings</div>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="fa-solid fa-circle-info"></i>
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
<div>Le proxy d'entreprise et l'URL Jira sont gérés par le backend (<code>config.ini</code>). Ici, fournissez uniquement vos identifiants Jira et les filtres du projet.</div>
|
<div>Corporate proxy and Jira URL are handled by the backend (<code>config.ini</code>). Provide only your Jira credentials and project filters below.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Nom d'utilisateur / Email</label>
|
<label for="username">Username / Email</label>
|
||||||
<input type="text" id="username" placeholder="user@mondomaine.be">
|
<input type="text" id="username" placeholder="user@mydomain.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="apiToken">Token API / Mot de passe</label>
|
<label for="apiToken">API Token / Password</label>
|
||||||
<input type="password" id="apiToken" placeholder="votre-token-api-jira">
|
<input type="password" id="apiToken" placeholder="your-jira-api-token">
|
||||||
<span class="hint">Jira Cloud : Personal Access Token. Jira Server : mot de passe.</span>
|
<span class="hint">Jira Cloud : Personal Access Token. Jira Server : password.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="projectKey">Clé du projet</label>
|
<label for="projectKey">Project Key</label>
|
||||||
<input type="text" id="projectKey" class="uppercase-input" placeholder="PROJ" oninput="autoJql()">
|
<input type="text" id="projectKey" class="uppercase-input" placeholder="PROJ" oninput="autoJql()">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="customFields">Champs personnalisés (optionnel)</label>
|
<label for="customFields">Custom Fields (optional)</label>
|
||||||
<input type="text" id="customFields" placeholder="customfield_10001:Production Date">
|
<input type="text" id="customFields" placeholder="customfield_10001:Production Date">
|
||||||
<span class="hint">Format : customfield_ID:NomAffiché, séparés par virgules</span>
|
<span class="hint">Format : customfield_ID:DisplayName, comma separated</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full">
|
<div class="form-group full">
|
||||||
<label for="jqlQuery">Requête JQL (optionnel)</label>
|
<label for="jqlQuery">JQL Query (optional)</label>
|
||||||
<textarea id="jqlQuery" placeholder='project = "PROJ" AND status != Deleted ORDER BY created DESC' rows="2"></textarea>
|
<textarea id="jqlQuery" placeholder='project = "PROJ" AND status != Deleted ORDER BY created DESC' rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group mt-24">
|
<div class="btn-group mt-24">
|
||||||
<button class="btn btn-primary" id="extractBtn" onclick="startExtraction()">
|
<button class="btn btn-primary" id="extractBtn" onclick="startExtraction()">
|
||||||
<i class="fa-solid fa-download"></i> Lancer l'extraction
|
<i class="fa-solid fa-download"></i> Start Extraction
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" onclick="pingApi()">
|
<button class="btn btn-secondary" onclick="pingApi()">
|
||||||
<i class="fa-solid fa-plug"></i> Tester l'API
|
<i class="fa-solid fa-plug"></i> Test API
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,21 +81,21 @@
|
|||||||
<div class="card card-actions">
|
<div class="card card-actions">
|
||||||
<div class="actions-wrapper">
|
<div class="actions-wrapper">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" onclick="switchTab('table', this)"><i class="fa-solid fa-table"></i>Tableau</button>
|
<button class="tab-btn active" onclick="switchTab('table', this)"><i class="fa-solid fa-table"></i>Table</button>
|
||||||
<button class="tab-btn" onclick="switchTab('roadmap', this)"><i class="fa-solid fa-timeline"></i>Roadmap</button>
|
<button class="tab-btn" onclick="switchTab('roadmap', this)"><i class="fa-solid fa-timeline"></i>Roadmap</button>
|
||||||
<button class="tab-btn" onclick="switchTab('json', this)"><i class="fa-solid fa-code"></i>JSON Brut</button>
|
<button class="tab-btn" onclick="switchTab('json', this)"><i class="fa-solid fa-code"></i>Raw JSON</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group no-margin">
|
<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-secondary" onclick="copyJSON()"><i class="fa-solid fa-copy"></i> Copy</button>
|
||||||
<button class="btn btn-primary" onclick="downloadJSON()"><i class="fa-solid fa-file-arrow-down"></i> Telecharger</button>
|
<button class="btn btn-primary" onclick="downloadJSON()"><i class="fa-solid fa-file-arrow-down"></i> Download</button>
|
||||||
<button class="btn btn-danger" onclick="clearResults()"><i class="fa-solid fa-trash"></i> Effacer</button>
|
<button class="btn btn-danger" onclick="clearResults()"><i class="fa-solid fa-trash"></i> Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
<input type="text" id="searchInput" placeholder="Rechercher..." oninput="filterTable()">
|
<input type="text" id="searchInput" placeholder="Search..." oninput="filterTable()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel active" id="panel-table">
|
<div class="tab-panel active" id="panel-table">
|
||||||
@@ -111,15 +111,15 @@
|
|||||||
<div class="tab-panel" id="panel-roadmap">
|
<div class="tab-panel" id="panel-roadmap">
|
||||||
<div class="toolbar-roadmap">
|
<div class="toolbar-roadmap">
|
||||||
<div class="view-controls">
|
<div class="view-controls">
|
||||||
<span class="mr-8"><i class="fa-solid fa-magnifying-glass-plus"></i> Vue :</span>
|
<span class="mr-8"><i class="fa-solid fa-magnifying-glass-plus"></i> View :</span>
|
||||||
<div class="btn-group no-margin">
|
<div class="btn-group no-margin">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="setGanttView('month')">Mois</button>
|
<button class="btn btn-secondary btn-sm" onclick="setGanttView('month')">Months</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="setGanttView('quarter')">Trimestres</button>
|
<button class="btn btn-secondary btn-sm" onclick="setGanttView('quarter')">Quarters</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="setGanttView('year')">Années</button>
|
<button class="btn btn-secondary btn-sm" onclick="setGanttView('year')">Years</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" id="ganttExpandBtn" onclick="toggleGanttExpand()" title="Agrandir ou réduire la hauteur du Gantt">
|
<button class="btn btn-secondary btn-sm" id="ganttExpandBtn" onclick="toggleGanttExpand()" title="Expand or collapse Gantt height">
|
||||||
<i class="fa-solid fa-up-right-and-down-left-from-center" id="ganttExpandIcon"></i>
|
<i class="fa-solid fa-up-right-and-down-left-from-center" id="ganttExpandIcon"></i>
|
||||||
<span id="ganttExpandLabel">Déplier</span>
|
<span id="ganttExpandLabel">Expand</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,10 +129,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('download')">
|
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('download')">
|
||||||
<i class="fa-solid fa-file-download"></i> Télécharger
|
<i class="fa-solid fa-file-download"></i> Download
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('copy')">
|
<button class="btn btn-secondary btn-sm" onclick="exportDrawIOXML('copy')">
|
||||||
<i class="fa-solid fa-copy"></i> Copier XML
|
<i class="fa-solid fa-copy"></i> Copy XML
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+183
-44
@@ -23,6 +23,45 @@
|
|||||||
--gantt-sticky-bg: #1a1a1a;
|
--gantt-sticky-bg: #1a1a1a;
|
||||||
--gantt-group-bg: rgba(255, 255, 255, 0.03);
|
--gantt-group-bg: rgba(255, 255, 255, 0.03);
|
||||||
--gantt-grid-border: rgba(255, 255, 255, 0.05);
|
--gantt-grid-border: rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
/* Status Colors - Dark Mode */
|
||||||
|
--status-opportunity: #c084fc;
|
||||||
|
--status-opportunity-bg: rgba(192, 132, 252, 0.15);
|
||||||
|
--status-opportunity-border: #a855f7;
|
||||||
|
--status-opportunity-badge: #9333ea;
|
||||||
|
|
||||||
|
--status-delayed: #94a3b8;
|
||||||
|
--status-delayed-bg: rgba(148, 163, 184, 0.15);
|
||||||
|
--status-delayed-border: #64748b;
|
||||||
|
--status-delayed-badge: #475569;
|
||||||
|
|
||||||
|
--status-analyse: #fbbf24;
|
||||||
|
--status-analyse-bg: rgba(251, 191, 36, 0.15);
|
||||||
|
--status-analyse-border: #f59e0b;
|
||||||
|
--status-analyse-badge: #d97706;
|
||||||
|
|
||||||
|
--status-analysed: #22d3ee;
|
||||||
|
--status-analysed-bg: rgba(34, 211, 238, 0.15);
|
||||||
|
--status-analysed-border: #06b6d4;
|
||||||
|
--status-analysed-badge: #0891b2;
|
||||||
|
|
||||||
|
--status-ongoing: #60a5fa;
|
||||||
|
--status-ongoing-bg: rgba(96, 165, 250, 0.18);
|
||||||
|
--status-ongoing-border: #3b82f6;
|
||||||
|
--status-ongoing-badge: #2563eb;
|
||||||
|
|
||||||
|
--status-rejected: #f87171;
|
||||||
|
--status-rejected-bg: rgba(248, 113, 113, 0.16);
|
||||||
|
--status-rejected-border: #ef4444;
|
||||||
|
--status-rejected-badge: #dc2626;
|
||||||
|
|
||||||
|
--status-done: #34d399;
|
||||||
|
--status-done-bg: rgba(52, 211, 153, 0.16);
|
||||||
|
--status-done-border: #10b981;
|
||||||
|
--status-done-badge: #059669;
|
||||||
|
|
||||||
|
--gantt-bar-bg: #141f19;
|
||||||
|
--gantt-bar-border: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
[data-theme="light"] {
|
||||||
@@ -48,6 +87,45 @@
|
|||||||
--gantt-sticky-bg: #e6ede8;
|
--gantt-sticky-bg: #e6ede8;
|
||||||
--gantt-group-bg: rgba(0, 0, 0, 0.03);
|
--gantt-group-bg: rgba(0, 0, 0, 0.03);
|
||||||
--gantt-grid-border: rgba(0, 0, 0, 0.06);
|
--gantt-grid-border: rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
/* Status Colors - Light Mode (High Contrast) */
|
||||||
|
--status-opportunity: #6d28d9;
|
||||||
|
--status-opportunity-bg: #f5f3ff;
|
||||||
|
--status-opportunity-border: #7c3aed;
|
||||||
|
--status-opportunity-badge: #7c3aed;
|
||||||
|
|
||||||
|
--status-delayed: #334155;
|
||||||
|
--status-delayed-bg: #f1f5f9;
|
||||||
|
--status-delayed-border: #64748b;
|
||||||
|
--status-delayed-badge: #475569;
|
||||||
|
|
||||||
|
--status-analyse: #92400e;
|
||||||
|
--status-analyse-bg: #fffbeb;
|
||||||
|
--status-analyse-border: #d97706;
|
||||||
|
--status-analyse-badge: #b45309;
|
||||||
|
|
||||||
|
--status-analysed: #0e7490;
|
||||||
|
--status-analysed-bg: #ecfeff;
|
||||||
|
--status-analysed-border: #0891b2;
|
||||||
|
--status-analysed-badge: #0e7490;
|
||||||
|
|
||||||
|
--status-ongoing: #1e40af;
|
||||||
|
--status-ongoing-bg: #eff6ff;
|
||||||
|
--status-ongoing-border: #2563eb;
|
||||||
|
--status-ongoing-badge: #1d4ed8;
|
||||||
|
|
||||||
|
--status-rejected: #991b1b;
|
||||||
|
--status-rejected-bg: #fef2f2;
|
||||||
|
--status-rejected-border: #dc2626;
|
||||||
|
--status-rejected-badge: #dc2626;
|
||||||
|
|
||||||
|
--status-done: #065f46;
|
||||||
|
--status-done-bg: #ecfdf5;
|
||||||
|
--status-done-border: #059669;
|
||||||
|
--status-done-badge: #047857;
|
||||||
|
|
||||||
|
--gantt-bar-bg: #ffffff;
|
||||||
|
--gantt-bar-border: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -543,58 +621,69 @@ tr:hover td {
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-opportunity {
|
||||||
|
background: var(--status-opportunity-bg);
|
||||||
|
color: var(--status-opportunity);
|
||||||
|
border-color: var(--status-opportunity-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-delayed {
|
||||||
|
background: var(--status-delayed-bg);
|
||||||
|
color: var(--status-delayed);
|
||||||
|
border-color: var(--status-delayed-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-analyse, .s-to-be-analysed {
|
||||||
|
background: var(--status-analyse-bg);
|
||||||
|
color: var(--status-analyse);
|
||||||
|
border-color: var(--status-analyse-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-analysed {
|
||||||
|
background: var(--status-analysed-bg);
|
||||||
|
color: var(--status-analysed);
|
||||||
|
border-color: var(--status-analysed-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-ongoing, .s-project-ongoing, .s-progress {
|
||||||
|
background: var(--status-ongoing-bg);
|
||||||
|
color: var(--status-ongoing);
|
||||||
|
border-color: var(--status-ongoing-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-rejected {
|
||||||
|
background: var(--status-rejected-bg);
|
||||||
|
color: var(--status-rejected);
|
||||||
|
border-color: var(--status-rejected-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-done {
|
.s-done {
|
||||||
background: rgba(0, 230, 138, 0.15);
|
background: var(--status-done-bg);
|
||||||
color: #00e68a;
|
color: var(--status-done);
|
||||||
}
|
border-color: var(--status-done-border);
|
||||||
|
|
||||||
.s-progress {
|
|
||||||
background: rgba(77, 200, 255, 0.15);
|
|
||||||
color: #4dc8ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-todo {
|
.s-todo {
|
||||||
background: rgba(255, 184, 77, 0.15);
|
background: var(--status-opportunity-bg);
|
||||||
color: #ffb84d;
|
color: var(--status-opportunity);
|
||||||
|
border-color: var(--status-opportunity-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-blocked {
|
.s-blocked {
|
||||||
background: rgba(255, 77, 106, 0.15);
|
background: var(--status-rejected-bg);
|
||||||
color: #ff4d6a;
|
color: var(--status-rejected);
|
||||||
|
border-color: var(--status-rejected-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-other {
|
.s-other {
|
||||||
background: rgba(122, 148, 136, 0.15);
|
background: var(--status-delayed-bg);
|
||||||
color: #7a9488;
|
color: var(--status-delayed);
|
||||||
}
|
border-color: var(--status-delayed-border);
|
||||||
|
|
||||||
[data-theme="light"] .s-done {
|
|
||||||
background: rgba(0, 153, 92, 0.12);
|
|
||||||
color: #007a49;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .s-progress {
|
|
||||||
background: rgba(2, 132, 199, 0.12);
|
|
||||||
color: #0284c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .s-todo {
|
|
||||||
background: rgba(217, 119, 6, 0.12);
|
|
||||||
color: #b45309;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .s-blocked {
|
|
||||||
background: rgba(220, 38, 38, 0.12);
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .s-other {
|
|
||||||
background: rgba(83, 110, 97, 0.12);
|
|
||||||
color: #4e6b5d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-cell {
|
.empty-cell {
|
||||||
@@ -1009,18 +1098,68 @@ GANTT
|
|||||||
/* La barre Gantt elle-même */
|
/* La barre Gantt elle-même */
|
||||||
.gantt-bar {
|
.gantt-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px;
|
gap: 8px;
|
||||||
font-size: 10px;
|
padding: 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
cursor: help;
|
cursor: pointer;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .gantt-bar {
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-bar:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .gantt-bar:hover {
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-status-badge {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-type-tag {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-bar-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colonne assigné/ticket figée à gauche */
|
/* Colonne assigné/ticket figée à gauche */
|
||||||
|
|||||||
+276
-116
@@ -10,7 +10,192 @@ let currentSortCol = null;
|
|||||||
let isAsc = true;
|
let isAsc = true;
|
||||||
|
|
||||||
|
|
||||||
// --- GESTION DU THÈME (CLAIR / SOMBRE) ---
|
// --- JIRA STATUS CONFIG & MAPPING ---
|
||||||
|
const STATUS_DEFINITIONS = {
|
||||||
|
'Opportunity': {
|
||||||
|
label: 'Opportunity',
|
||||||
|
cssClass: 's-opportunity',
|
||||||
|
dark: {
|
||||||
|
color: '#c084fc',
|
||||||
|
bg: 'rgba(192, 132, 252, 0.16)',
|
||||||
|
border: '#a855f7',
|
||||||
|
badgeBg: '#9333ea',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#6d28d9',
|
||||||
|
bg: '#f5f3ff',
|
||||||
|
border: '#7c3aed',
|
||||||
|
badgeBg: '#7c3aed',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#9333ea'
|
||||||
|
},
|
||||||
|
'Delayed': {
|
||||||
|
label: 'Delayed',
|
||||||
|
cssClass: 's-delayed',
|
||||||
|
dark: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
bg: 'rgba(148, 163, 184, 0.16)',
|
||||||
|
border: '#64748b',
|
||||||
|
badgeBg: '#475569',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#334155',
|
||||||
|
bg: '#f1f5f9',
|
||||||
|
border: '#64748b',
|
||||||
|
badgeBg: '#475569',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#64748b'
|
||||||
|
},
|
||||||
|
'To be Analysed': {
|
||||||
|
label: 'To be Analysed',
|
||||||
|
cssClass: 's-analyse',
|
||||||
|
dark: {
|
||||||
|
color: '#fbbf24',
|
||||||
|
bg: 'rgba(251, 191, 36, 0.16)',
|
||||||
|
border: '#f59e0b',
|
||||||
|
badgeBg: '#d97706',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#92400e',
|
||||||
|
bg: '#fffbeb',
|
||||||
|
border: '#d97706',
|
||||||
|
badgeBg: '#b45309',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#d97706'
|
||||||
|
},
|
||||||
|
'Analysed': {
|
||||||
|
label: 'Analysed',
|
||||||
|
cssClass: 's-analysed',
|
||||||
|
dark: {
|
||||||
|
color: '#22d3ee',
|
||||||
|
bg: 'rgba(34, 211, 238, 0.16)',
|
||||||
|
border: '#06b6d4',
|
||||||
|
badgeBg: '#0891b2',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#0e7490',
|
||||||
|
bg: '#ecfeff',
|
||||||
|
border: '#0891b2',
|
||||||
|
badgeBg: '#0e7490',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#0891b2'
|
||||||
|
},
|
||||||
|
'Project Ongoing': {
|
||||||
|
label: 'Project Ongoing',
|
||||||
|
cssClass: 's-ongoing',
|
||||||
|
dark: {
|
||||||
|
color: '#60a5fa',
|
||||||
|
bg: 'rgba(96, 165, 250, 0.20)',
|
||||||
|
border: '#3b82f6',
|
||||||
|
badgeBg: '#2563eb',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#1e40af',
|
||||||
|
bg: '#eff6ff',
|
||||||
|
border: '#2563eb',
|
||||||
|
badgeBg: '#1d4ed8',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#2563eb'
|
||||||
|
},
|
||||||
|
'Rejected': {
|
||||||
|
label: 'Rejected',
|
||||||
|
cssClass: 's-rejected',
|
||||||
|
dark: {
|
||||||
|
color: '#f87171',
|
||||||
|
bg: 'rgba(248, 113, 113, 0.18)',
|
||||||
|
border: '#ef4444',
|
||||||
|
badgeBg: '#dc2626',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#991b1b',
|
||||||
|
bg: '#fef2f2',
|
||||||
|
border: '#dc2626',
|
||||||
|
badgeBg: '#dc2626',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#dc2626'
|
||||||
|
},
|
||||||
|
'Done': {
|
||||||
|
label: 'Done',
|
||||||
|
cssClass: 's-done',
|
||||||
|
dark: {
|
||||||
|
color: '#34d399',
|
||||||
|
bg: 'rgba(52, 211, 153, 0.18)',
|
||||||
|
border: '#10b981',
|
||||||
|
badgeBg: '#059669',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
color: '#065f46',
|
||||||
|
bg: '#ecfdf5',
|
||||||
|
border: '#059669',
|
||||||
|
badgeBg: '#047857',
|
||||||
|
badgeText: '#ffffff'
|
||||||
|
},
|
||||||
|
drawioColor: '#059669'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapJiraStatus(rawStatus) {
|
||||||
|
if (!rawStatus) return 'Opportunity';
|
||||||
|
const s = String(rawStatus).trim().toLowerCase();
|
||||||
|
|
||||||
|
// "Resolved" and "Closed" => DONE
|
||||||
|
if (s.includes('done') || s.includes('resolv') || s.includes('clos') || s.includes('ferm') || s.includes('termin') || s.includes('rsolu')) {
|
||||||
|
return 'Done';
|
||||||
|
}
|
||||||
|
// "Rejected" => Rejected
|
||||||
|
if (s.includes('reject') || s.includes('rejet') || s.includes('refus') || s.includes('abandon')) {
|
||||||
|
return 'Rejected';
|
||||||
|
}
|
||||||
|
// "In Progress" => Project Ongoing
|
||||||
|
if (s.includes('progress') || s.includes('en cours') || s.includes('ongoing') || s.includes('review') || s.includes('dev')) {
|
||||||
|
return 'Project Ongoing';
|
||||||
|
}
|
||||||
|
// "On Hold" => Analysed
|
||||||
|
if (s.includes('hold') || s.includes('analysé') || s.includes('analysed') || s.includes('attente')) {
|
||||||
|
return 'Analysed';
|
||||||
|
}
|
||||||
|
// "Open" and "SPQI" => To be Analysed
|
||||||
|
if (s === 'open' || s.includes('spqi') || s.includes('ouvert') || s.includes('to analyse') || s.includes('to be analysed')) {
|
||||||
|
return 'To be Analysed';
|
||||||
|
}
|
||||||
|
// "Delayed" => Delayed
|
||||||
|
if (s.includes('delay') || s.includes('retard') || s.includes('reporté')) {
|
||||||
|
return 'Delayed';
|
||||||
|
}
|
||||||
|
// "New" and "Reopened" => Opportunity
|
||||||
|
if (s.includes('new') || s.includes('reopen') || s.includes('nouveau') || s.includes('opportunit') || s.includes('backlog')) {
|
||||||
|
return 'Opportunity';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Opportunity';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusThemeConfig(mappedStatus) {
|
||||||
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||||
|
const def = STATUS_DEFINITIONS[mappedStatus] || STATUS_DEFINITIONS['Opportunity'];
|
||||||
|
return isLight ? def.light : def.dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(s) {
|
||||||
|
const mapped = mapJiraStatus(s);
|
||||||
|
const def = STATUS_DEFINITIONS[mapped];
|
||||||
|
return def ? def.cssClass : 's-opportunity';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- THEME MANAGEMENT (LIGHT / DARK) ---
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
const savedTheme = localStorage.getItem('jira_extractor_theme') || 'dark';
|
const savedTheme = localStorage.getItem('jira_extractor_theme') || 'dark';
|
||||||
setTheme(savedTheme, false);
|
setTheme(savedTheme, false);
|
||||||
@@ -31,19 +216,23 @@ function setTheme(theme, withToast = false) {
|
|||||||
if (icon && label) {
|
if (icon && label) {
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
icon.className = 'fa-solid fa-moon';
|
icon.className = 'fa-solid fa-moon';
|
||||||
label.textContent = 'Mode sombre';
|
label.textContent = 'Dark mode';
|
||||||
} else {
|
} else {
|
||||||
icon.className = 'fa-solid fa-sun';
|
icon.className = 'fa-solid fa-sun';
|
||||||
label.textContent = 'Mode clair';
|
label.textContent = 'Light mode';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('panel-roadmap') && document.getElementById('panel-roadmap').classList.contains('active')) {
|
||||||
|
renderRoadmap();
|
||||||
|
}
|
||||||
|
|
||||||
if (withToast) {
|
if (withToast) {
|
||||||
showToast(theme === 'light' ? 'Mode clair activé' : 'Mode sombre activé');
|
showToast(theme === 'light' ? 'Light mode enabled' : 'Dark mode enabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sauvegarde les paramètres dans le stockage local du navigateur
|
// Save settings to local storage
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
const settings = {
|
const settings = {
|
||||||
projectKey: document.getElementById('projectKey').value,
|
projectKey: document.getElementById('projectKey').value,
|
||||||
@@ -53,7 +242,7 @@ function saveSettings() {
|
|||||||
localStorage.setItem('jira_roadmap_settings', JSON.stringify(settings));
|
localStorage.setItem('jira_roadmap_settings', JSON.stringify(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charge les paramètres au démarrage
|
// Load settings on startup
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
const saved = localStorage.getItem('jira_roadmap_settings');
|
const saved = localStorage.getItem('jira_roadmap_settings');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -65,7 +254,7 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- UTILITAIRES ---
|
// --- UTILITIES ---
|
||||||
function showToast(msg, type='success') {
|
function showToast(msg, type='success') {
|
||||||
const c = document.getElementById('toastContainer');
|
const c = document.getElementById('toastContainer');
|
||||||
const t = document.createElement('div');
|
const t = document.createElement('div');
|
||||||
@@ -87,13 +276,13 @@ async function pingApi() {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
|
|
||||||
const btn = event.target.closest('.btn');
|
const btn = event.target.closest('.btn');
|
||||||
const orig = btn.innerHTML; btn.innerHTML = '<span class="spinner"></span> Test...'; btn.disabled = true;
|
const orig = btn.innerHTML; btn.innerHTML = '<span class="spinner"></span> Testing...'; btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username:'_', apiToken:'_', projectKey:'_'}) });
|
const r = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username:'_', apiToken:'_', projectKey:'_'}) });
|
||||||
if (r.status === 400) showToast('API locale connectée !', 'success');
|
if (r.status === 400) showToast('Local API connected!', 'success');
|
||||||
else showToast('API répond : ' + r.status, 'success');
|
else showToast('API responded: ' + r.status, 'success');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
showToast('API inaccessible. Lancez "npm start".', 'error');
|
showToast('API unreachable. Please run "npm start".', 'error');
|
||||||
} finally { btn.innerHTML = orig; btn.disabled = false; }
|
} finally { btn.innerHTML = orig; btn.disabled = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +296,9 @@ async function startExtraction() {
|
|||||||
const jql = document.getElementById('jqlQuery').value.trim();
|
const jql = document.getElementById('jqlQuery').value.trim();
|
||||||
const custom = document.getElementById('customFields').value.trim();
|
const custom = document.getElementById('customFields').value.trim();
|
||||||
|
|
||||||
if (!user || !token || !project) { showToast('Champs requis manquants.', 'error'); return; }
|
if (!user || !token || !project) { showToast('Required fields missing.', 'error'); return; }
|
||||||
|
|
||||||
btn.innerHTML = '<span class="spinner"></span> Extraction...'; btn.disabled = true;
|
btn.innerHTML = '<span class="spinner"></span> Extracting...'; btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API_BASE}/api/extract`, {
|
const resp = await fetch(`${API_BASE}/api/extract`, {
|
||||||
@@ -118,7 +307,7 @@ async function startExtraction() {
|
|||||||
body: JSON.stringify({ username: user, apiToken: token, projectKey: project, jql: jql, customFields: custom })
|
body: JSON.stringify({ username: user, apiToken: token, projectKey: project, jql: jql, customFields: custom })
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!resp.ok) throw new Error(data.error || `Erreur ${resp.status}`);
|
if (!resp.ok) throw new Error(data.error || `Error ${resp.status}`);
|
||||||
|
|
||||||
allTickets = data.tickets;
|
allTickets = data.tickets;
|
||||||
filteredTickets = [...allTickets];
|
filteredTickets = [...allTickets];
|
||||||
@@ -127,11 +316,11 @@ async function startExtraction() {
|
|||||||
lastApiToken = token;
|
lastApiToken = token;
|
||||||
|
|
||||||
renderResults();
|
renderResults();
|
||||||
showToast(`${allTickets.length} tickets extraits.`, 'success');
|
showToast(`${allTickets.length} tickets extracted.`, 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
btn.innerHTML = '<i class="fa-solid fa-download"></i> Lancer l\'extraction'; btn.disabled = false;
|
btn.innerHTML = '<i class="fa-solid fa-download"></i> Start Extraction'; btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +336,6 @@ function parseCustomFields(customFields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortTable(colKey) {
|
function sortTable(colKey) {
|
||||||
// Si on clique sur la même colonne, on inverse l'ordre, sinon on passe en croissant
|
|
||||||
if (currentSortCol === colKey) {
|
if (currentSortCol === colKey) {
|
||||||
isAsc = !isAsc;
|
isAsc = !isAsc;
|
||||||
} else {
|
} else {
|
||||||
@@ -159,7 +347,6 @@ function sortTable(colKey) {
|
|||||||
let valA = a[colKey] || '';
|
let valA = a[colKey] || '';
|
||||||
let valB = b[colKey] || '';
|
let valB = b[colKey] || '';
|
||||||
|
|
||||||
// Gestion spécifique pour les dates pour un tri chronologique correct
|
|
||||||
if (colKey.toLowerCase().includes('date') || colKey === 'created' || colKey === 'dueDate') {
|
if (colKey.toLowerCase().includes('date') || colKey === 'created' || colKey === 'dueDate') {
|
||||||
valA = new Date(valA);
|
valA = new Date(valA);
|
||||||
valB = new Date(valB);
|
valB = new Date(valB);
|
||||||
@@ -173,10 +360,10 @@ function sortTable(colKey) {
|
|||||||
renderTable();
|
renderTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MISE À JOUR TICKET ---
|
// --- TICKET UPDATE ---
|
||||||
async function updateTicket(ticketKey, dueDate, goLiveDate) {
|
async function updateTicket(ticketKey, dueDate, goLiveDate) {
|
||||||
if (!lastUsername || !lastApiToken) {
|
if (!lastUsername || !lastApiToken) {
|
||||||
showToast('Veuillez d\'abord extraire les tickets.', 'error');
|
showToast('Please extract tickets first.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +385,7 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
throw new Error(data.error || `Erreur HTTP ${resp.status}`);
|
throw new Error(data.error || `HTTP Error ${resp.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticket = allTickets.find(t => t.key === ticketKey);
|
const ticket = allTickets.find(t => t.key === ticketKey);
|
||||||
@@ -209,7 +396,7 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) {
|
|||||||
|
|
||||||
renderTable();
|
renderTable();
|
||||||
renderRoadmap();
|
renderRoadmap();
|
||||||
showToast(`Ticket ${ticketKey} mis à jour !`, 'success');
|
showToast(`Ticket ${ticketKey} updated!`, 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -235,7 +422,7 @@ function handleUpdateClick(ticketKey, goLiveColName) {
|
|||||||
updateTicket(ticketKey, dueDate, goLiveDate);
|
updateTicket(ticketKey, dueDate, goLiveDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RENDU ---
|
// --- RENDERING ---
|
||||||
function renderResults() {
|
function renderResults() {
|
||||||
document.getElementById('results-section').classList.add('visible');
|
document.getElementById('results-section').classList.add('visible');
|
||||||
renderStats(); renderTable(); renderJSON(); renderRoadmap();
|
renderStats(); renderTable(); renderJSON(); renderRoadmap();
|
||||||
@@ -244,18 +431,17 @@ function renderResults() {
|
|||||||
function renderStats() {
|
function renderStats() {
|
||||||
const unassigned = allTickets.filter(t => !t.assignee).length;
|
const unassigned = allTickets.filter(t => !t.assignee).length;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const overdue = allTickets.filter(t => t.dueDate && t.statusCategory !== 'Done' && new Date(t.dueDate) < now).length;
|
const overdue = allTickets.filter(t => t.dueDate && mapJiraStatus(t.status) !== 'Done' && new Date(t.dueDate) < now).length;
|
||||||
document.getElementById('statsRow').innerHTML = `
|
document.getElementById('statsRow').innerHTML = `
|
||||||
<div class="stat-card"><div class="stat-value">${allTickets.length}</div><div class="stat-label">Tickets</div></div>
|
<div class="stat-card"><div class="stat-value">${allTickets.length}</div><div class="stat-label">Tickets</div></div>
|
||||||
<div class="stat-card"><div class="stat-value">${new Set(allTickets.map(t=>t.status)).size}</div><div class="stat-label">Statuts</div></div>
|
<div class="stat-card"><div class="stat-value">${new Set(allTickets.map(t=>mapJiraStatus(t.status))).size}</div><div class="stat-label">Statuses</div></div>
|
||||||
<div class="stat-card"><div class="stat-value">${unassigned}</div><div class="stat-label">Non assignés</div></div>
|
<div class="stat-card"><div class="stat-value">${unassigned}</div><div class="stat-label">Unassigned</div></div>
|
||||||
<div class="stat-card"><div class="stat-value" style="color:${overdue>0?'var(--danger)':'var(--accent)'}">${overdue}</div><div class="stat-label">En retard</div></div>
|
<div class="stat-card"><div class="stat-value" style="color:${overdue>0?'var(--danger)':'var(--accent)'}">${overdue}</div><div class="stat-label">Overdue</div></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escHtml(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=String(s); return d.innerHTML; }
|
function escHtml(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=String(s); return d.innerHTML; }
|
||||||
function fmtDate(d) { if(!d) return '<span class="empty-cell">-</span>'; try { return new Date(d).toLocaleDateString('fr-FR',{day:'2-digit',month:'short',year:'numeric'}); } catch { return escHtml(d); } }
|
function fmtDate(d) { if(!d) return '<span class="empty-cell">-</span>'; try { return new Date(d).toLocaleDateString('en-US',{day:'2-digit',month:'short',year:'numeric'}); } catch { return escHtml(d); } }
|
||||||
function getStatusClass(s) { if(!s) return 's-other'; const l=s.toLowerCase(); if(l.includes('done')||l.includes('ferm')||l.includes('résolu')) return 's-done'; if(l.includes('progress')||l.includes('en cours')||l.includes('review')) return 's-progress'; if(l.includes('to do')||l.includes('open')||l.includes('backlog')) return 's-todo'; if(l.includes('block')) return 's-blocked'; return 's-other'; }
|
|
||||||
|
|
||||||
function renderTable() {
|
function renderTable() {
|
||||||
let goLiveColName = null;
|
let goLiveColName = null;
|
||||||
@@ -265,28 +451,26 @@ function renderTable() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapping pour faire le lien entre le texte de l'en-tête et la propriété de l'objet ticket
|
|
||||||
const colMapping = {
|
const colMapping = {
|
||||||
'Ticket': 'key',
|
'Ticket': 'key',
|
||||||
'Summary': 'summary',
|
'Summary': 'summary',
|
||||||
'Statut': 'status',
|
'Status': 'status',
|
||||||
'Assigné': 'assignee',
|
'Assignee': 'assignee',
|
||||||
'Priorité': 'priority',
|
'Priority': 'priority',
|
||||||
'Type': 'issueType',
|
'Type': 'issueType',
|
||||||
'Due Date': 'dueDate'
|
'Due Date': 'dueDate'
|
||||||
};
|
};
|
||||||
|
|
||||||
const cols = ['Ticket','Summary','Statut','Assigné','Priorité','Type','Due Date'];
|
const cols = ['Ticket','Summary','Status','Assignee','Priority','Type','Due Date'];
|
||||||
if (goLiveColName) {
|
if (goLiveColName) {
|
||||||
cols.push(goLiveColName);
|
cols.push(goLiveColName);
|
||||||
colMapping[goLiveColName] = goLiveColName; // Ajout dynamique du champ personnalisé
|
colMapping[goLiveColName] = goLiveColName;
|
||||||
}
|
}
|
||||||
cols.push('Actions');
|
cols.push('Actions');
|
||||||
|
|
||||||
// Génération des en-têtes avec gestion du clic pour le tri
|
|
||||||
document.getElementById('tableHead').innerHTML = '<tr>' + cols.map(c => {
|
document.getElementById('tableHead').innerHTML = '<tr>' + cols.map(c => {
|
||||||
const key = colMapping[c];
|
const key = colMapping[c];
|
||||||
if (!key) return `<th>${c}</th>`; // Pour la colonne 'Actions'
|
if (!key) return `<th>${c}</th>`;
|
||||||
|
|
||||||
const icon = currentSortCol === key ? (isAsc ? ' ▴' : ' ▾') : '';
|
const icon = currentSortCol === key ? (isAsc ? ' ▴' : ' ▾') : '';
|
||||||
return `<th onclick="sortTable('${key}')" style="cursor:pointer" class="sortable-th">
|
return `<th onclick="sortTable('${key}')" style="cursor:pointer" class="sortable-th">
|
||||||
@@ -297,10 +481,15 @@ function renderTable() {
|
|||||||
let body = '';
|
let body = '';
|
||||||
filteredTickets.forEach(t => {
|
filteredTickets.forEach(t => {
|
||||||
const dueDateValue = t.dueDate ? t.dueDate.substring(0, 10) : '';
|
const dueDateValue = t.dueDate ? t.dueDate.substring(0, 10) : '';
|
||||||
|
const mappedStatus = mapJiraStatus(t.status);
|
||||||
body += `<tr>
|
body += `<tr>
|
||||||
<td><span class="ticket-key" onclick="window.open('${t.url}', '_blank')">${escHtml(t.key)}</span></td>
|
<td><span class="ticket-key" onclick="window.open('${t.url}', '_blank')">${escHtml(t.key)}</span></td>
|
||||||
<td title="${escHtml(t.summary)}">${escHtml(t.summary)}</td>
|
<td title="${escHtml(t.summary)}">${escHtml(t.summary)}</td>
|
||||||
<td><span class="status-badge ${getStatusClass(t.status)}">${escHtml(t.status)}</span></td>
|
<td>
|
||||||
|
<span class="status-badge ${getStatusClass(t.status)}" title="Jira: ${escHtml(t.status)}">
|
||||||
|
${escHtml(mappedStatus)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>${escHtml(t.assignee) || '-'}</td>
|
<td>${escHtml(t.assignee) || '-'}</td>
|
||||||
<td>${escHtml(t.priority)}</td>
|
<td>${escHtml(t.priority)}</td>
|
||||||
<td>${escHtml(t.issueType)}</td>
|
<td>${escHtml(t.issueType)}</td>
|
||||||
@@ -317,16 +506,7 @@ function renderTable() {
|
|||||||
</button>
|
</button>
|
||||||
</td></tr>`;
|
</td></tr>`;
|
||||||
});
|
});
|
||||||
document.getElementById('tableBody').innerHTML = body || '<tr><td colspan="10">Aucun résultat</td></tr>';
|
document.getElementById('tableBody').innerHTML = body || '<tr><td colspan="10">No results found</td></tr>';
|
||||||
}
|
|
||||||
|
|
||||||
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) ---
|
// --- ROADMAP (GANTT) ---
|
||||||
@@ -357,64 +537,62 @@ function updateGanttExpandState() {
|
|||||||
if (icon && label) {
|
if (icon && label) {
|
||||||
if (isGanttExpanded) {
|
if (isGanttExpanded) {
|
||||||
icon.className = 'fa-solid fa-down-left-and-up-right-to-center';
|
icon.className = 'fa-solid fa-down-left-and-up-right-to-center';
|
||||||
label.textContent = 'Replier';
|
label.textContent = 'Collapse';
|
||||||
if (btn) btn.classList.add('active');
|
if (btn) btn.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
icon.className = 'fa-solid fa-up-right-and-down-left-from-center';
|
icon.className = 'fa-solid fa-up-right-and-down-left-from-center';
|
||||||
label.textContent = 'Déplier';
|
label.textContent = 'Expand';
|
||||||
if (btn) btn.classList.remove('active');
|
if (btn) btn.classList.remove('active');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FONCTIONS UTILITAIRES POUR LE GANTT ---
|
// --- GANTT UTILITIES ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrait le titre court et le type selon le pattern : "Titre - Type - Montant - ID"
|
* Extracts short title and project type according to the pattern: "Title - Type - Amount - ID"
|
||||||
*/
|
*/
|
||||||
function parseTicketSummary(summary) {
|
function parseTicketSummary(summary) {
|
||||||
if (!summary) return { shortTitle: 'Sans titre', type: 'OTHER' };
|
if (!summary) return { shortTitle: 'Untitled', type: 'OTHER' };
|
||||||
const parts = summary.split(' - ');
|
const parts = summary.split(' - ');
|
||||||
const shortTitle = parts[0].trim();
|
const shortTitle = parts[0].trim();
|
||||||
const typePart = parts[1] ? parts[1].trim().toUpperCase() : 'OTHER';
|
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';
|
const type = ['PR', 'CR', 'TR'].includes(typePart) ? typePart : 'OTHER';
|
||||||
return { shortTitle, type };
|
return { shortTitle, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Définit la couleur de la barre selon le type de projet
|
* Defines activity type accent color
|
||||||
*/
|
*/
|
||||||
function getTypeColor(type) {
|
function getTypeColor(type) {
|
||||||
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'PR': return '#4dc8ff'; // Bleu info
|
case 'PR': return isLight ? '#0284c7' : '#38bdf8'; // Blue
|
||||||
case 'CR': return '#ffb84d'; // Orange warning
|
case 'CR': return isLight ? '#b45309' : '#fbbf24'; // Amber
|
||||||
case 'TR': return '#a371f7'; // Violet
|
case 'TR': return isLight ? '#6d28d9' : '#c084fc'; // Purple
|
||||||
default: return '#7a9488'; // Gris par défaut
|
default: return isLight ? '#475569' : '#94a3b8'; // Gray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FONCTION PRINCIPALE : RENDER ROADMAP
|
* MAIN ROADMAP RENDERING FUNCTION
|
||||||
*/
|
*/
|
||||||
function renderRoadmap() {
|
function renderRoadmap() {
|
||||||
const preview = document.getElementById('roadmapPreview');
|
const preview = document.getElementById('roadmapPreview');
|
||||||
if (!filteredTickets || filteredTickets.length === 0) {
|
if (!filteredTickets || filteredTickets.length === 0) {
|
||||||
preview.innerHTML = `<div style="text-align:center; padding:60px; color:var(--fg-dim);">Aucun ticket à afficher.</div>`;
|
preview.innerHTML = `<div style="text-align:center; padding:60px; color:var(--fg-dim);">No tickets to display.</div>`;
|
||||||
return;
|
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 goLiveColName = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "Go Live Date";
|
||||||
|
|
||||||
// Configuration des largeurs
|
const leftWidth = 280;
|
||||||
const leftWidth = 280; // Un peu plus large pour les titres split 0
|
|
||||||
let colWidth = 140;
|
let colWidth = 140;
|
||||||
if (currentGanttView === 'quarter') colWidth = 200;
|
if (currentGanttView === 'quarter') colWidth = 200;
|
||||||
if (currentGanttView === 'year') colWidth = 250;
|
if (currentGanttView === 'year') colWidth = 250;
|
||||||
|
|
||||||
// 1. Filtrer les tickets : on ne garde que ceux qui ont au moins une date
|
// Filter tickets with at least one date
|
||||||
const ticketsWithDates = filteredTickets.filter(t => {
|
const ticketsWithDates = filteredTickets.filter(t => {
|
||||||
const d1 = t.dueDate ? new Date(t.dueDate) : null;
|
const d1 = t.dueDate ? new Date(t.dueDate) : null;
|
||||||
const d2 = t[goLiveColName] ? new Date(t[goLiveColName]) : null;
|
const d2 = t[goLiveColName] ? new Date(t[goLiveColName]) : null;
|
||||||
@@ -422,11 +600,10 @@ function renderRoadmap() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (ticketsWithDates.length === 0) {
|
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>`;
|
preview.innerHTML = `<div style="padding:40px; text-align:center; color:var(--fg-dim);">No Due Date or Go Live date found for filtered tickets.</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcul des bornes du calendrier
|
|
||||||
const allDates = ticketsWithDates.flatMap(t => [
|
const allDates = ticketsWithDates.flatMap(t => [
|
||||||
t.dueDate ? new Date(t.dueDate) : null,
|
t.dueDate ? new Date(t.dueDate) : null,
|
||||||
t[goLiveColName] ? new Date(t[goLiveColName]) : null
|
t[goLiveColName] ? new Date(t[goLiveColName]) : null
|
||||||
@@ -439,16 +616,15 @@ function renderRoadmap() {
|
|||||||
if (currentGanttView === 'quarter') minDate.setMonth(Math.floor(minDate.getMonth() / 3) * 3);
|
if (currentGanttView === 'quarter') minDate.setMonth(Math.floor(minDate.getMonth() / 3) * 3);
|
||||||
if (currentGanttView === 'year') minDate.setMonth(0);
|
if (currentGanttView === 'year') minDate.setMonth(0);
|
||||||
|
|
||||||
maxDate.setMonth(maxDate.getMonth() + 2); // Un peu de marge au bout
|
maxDate.setMonth(maxDate.getMonth() + 2);
|
||||||
maxDate.setDate(0);
|
maxDate.setDate(0);
|
||||||
|
|
||||||
// Construction de l'échelle de temps
|
|
||||||
const timeScale = [];
|
const timeScale = [];
|
||||||
let tempDate = new Date(minDate);
|
let tempDate = new Date(minDate);
|
||||||
while (tempDate <= maxDate) {
|
while (tempDate <= maxDate) {
|
||||||
let label = "", key = "";
|
let label = "", key = "";
|
||||||
if (currentGanttView === 'month') {
|
if (currentGanttView === 'month') {
|
||||||
label = tempDate.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' });
|
label = tempDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||||
key = `${tempDate.getFullYear()}-${tempDate.getMonth()}`;
|
key = `${tempDate.getFullYear()}-${tempDate.getMonth()}`;
|
||||||
tempDate.setMonth(tempDate.getMonth() + 1);
|
tempDate.setMonth(tempDate.getMonth() + 1);
|
||||||
} else if (currentGanttView === 'quarter') {
|
} else if (currentGanttView === 'quarter') {
|
||||||
@@ -482,32 +658,26 @@ function renderRoadmap() {
|
|||||||
return leftWidth + (index * colWidth) + (ratio * colWidth);
|
return leftWidth + (index * colWidth) + (ratio * colWidth);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Groupement par assigné (uniquement ceux qui ont des tickets visibles)
|
|
||||||
const grouped = ticketsWithDates.reduce((acc, t) => {
|
const grouped = ticketsWithDates.reduce((acc, t) => {
|
||||||
const name = t.assignee || "Non assigné";
|
const name = t.assignee || "Unassigned";
|
||||||
if (!acc[name]) acc[name] = [];
|
if (!acc[name]) acc[name] = [];
|
||||||
acc[name].push(t);
|
acc[name].push(t);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Début HTML
|
|
||||||
let html = `<div class="gantt-container ${isGanttExpanded ? 'expanded' : ''}"><div class="gantt-canvas" style="width: ${leftWidth + (timeScale.length * colWidth)}px;">`;
|
let html = `<div class="gantt-container ${isGanttExpanded ? 'expanded' : ''}"><div class="gantt-canvas" style="width: ${leftWidth + (timeScale.length * colWidth)}px;">`;
|
||||||
|
|
||||||
// Ligne "Aujourd'hui"
|
|
||||||
const todayX = getXPos(new Date());
|
const todayX = getXPos(new Date());
|
||||||
if (todayX > leftWidth) {
|
if (todayX > leftWidth) {
|
||||||
html += `<div class="gantt-today-line" style="left: ${todayX}px;"></div>`;
|
html += `<div class="gantt-today-line" style="left: ${todayX}px;"></div>`;
|
||||||
html += `<div class="gantt-today-label" id="todayMarker" style="left: ${todayX}px;">Aujourd'hui</div>`;
|
html += `<div class="gantt-today-label" id="todayMarker" style="left: ${todayX}px;">Today</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header
|
html += `<div class="gantt-header"><div class="gantt-sticky-col gantt-group-name" style="width:${leftWidth}px">Assignee / Ticket (Title)</div>`;
|
||||||
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>`; });
|
timeScale.forEach(ts => { html += `<div class="gantt-month-header" style="width:${colWidth}px;">${ts.label}</div>`; });
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
// Corps du Gantt
|
|
||||||
Object.keys(grouped).sort().forEach(assignee => {
|
Object.keys(grouped).sort().forEach(assignee => {
|
||||||
// Header de groupe
|
|
||||||
html += `<div class="gantt-group-header">
|
html += `<div class="gantt-group-header">
|
||||||
<div class="gantt-sticky-col gantt-group-name" style="width:${leftWidth}px">
|
<div class="gantt-sticky-col gantt-group-name" style="width:${leftWidth}px">
|
||||||
<i class="fa-solid fa-user-circle mr-8"></i>${assignee}
|
<i class="fa-solid fa-user-circle mr-8"></i>${assignee}
|
||||||
@@ -525,28 +695,31 @@ function renderRoadmap() {
|
|||||||
const x1 = getXPos(start);
|
const x1 = getXPos(start);
|
||||||
const x2 = getXPos(end);
|
const x2 = getXPos(end);
|
||||||
|
|
||||||
// Largeur de barre (minimum 150px pour afficher le badge de statut et le titre proprement)
|
const barWidth = Math.max(170, (x2 - x1) + 10);
|
||||||
const barWidth = Math.max(150, (x2 - x1) + 10);
|
|
||||||
|
|
||||||
const { shortTitle, type } = parseTicketSummary(t.summary);
|
const { shortTitle, type } = parseTicketSummary(t.summary);
|
||||||
|
const mappedStatus = mapJiraStatus(t.status);
|
||||||
|
const statusCfg = getStatusThemeConfig(mappedStatus);
|
||||||
const typeColor = getTypeColor(type);
|
const typeColor = getTypeColor(type);
|
||||||
const statusColor = getStatusColor(t.status);
|
|
||||||
|
|
||||||
html += `<div class="gantt-row">
|
html += `<div class="gantt-row">
|
||||||
<div class="gantt-sticky-col gantt-ticket-id" style="width:${leftWidth}px; font-size:11px;" title="${t.key}: ${t.summary}">
|
<div class="gantt-sticky-col gantt-ticket-id" style="width:${leftWidth}px;" title="${escHtml(t.key)}: ${escHtml(t.summary)}">
|
||||||
${escHtml(shortTitle)}
|
${escHtml(shortTitle)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gantt-bar"
|
<div class="gantt-bar"
|
||||||
onclick="window.open('${t.url}', '_blank')"
|
onclick="window.open('${t.url}', '_blank')"
|
||||||
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;">
|
title="${escHtml(t.key)}: ${escHtml(t.summary)} | Status: ${escHtml(t.status)} (${mappedStatus}) | Type: ${type}"
|
||||||
|
style="left:${x1}px; width:${barWidth}px; background:${statusCfg.bg}; border-color:${statusCfg.border}; border-left:5px solid ${statusCfg.border};">
|
||||||
|
|
||||||
<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);">
|
<div class="gantt-status-badge" style="background:${statusCfg.badgeBg}; color:${statusCfg.badgeText};">
|
||||||
${escHtml(t.status)}
|
${escHtml(mappedStatus)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style="color:${typeColor}; font-weight:bold; flex-shrink:0;">${type}</span>
|
<span class="gantt-type-tag" style="background:${typeColor}20; color:${typeColor}; border:1px solid ${typeColor}66;">
|
||||||
<span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:12px;">${escHtml(shortTitle)}</span>
|
${type}
|
||||||
|
</span>
|
||||||
|
<span class="gantt-bar-title">${escHtml(shortTitle)}</span>
|
||||||
</div>
|
</div>
|
||||||
${timeScale.map(() => `<div class="gantt-grid-col" style="width:${colWidth}px;"></div>`).join('')}
|
${timeScale.map(() => `<div class="gantt-grid-col" style="width:${colWidth}px;"></div>`).join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -556,7 +729,6 @@ function renderRoadmap() {
|
|||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
preview.innerHTML = html;
|
preview.innerHTML = html;
|
||||||
|
|
||||||
// Focus sur la date du jour
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const m = document.getElementById('todayMarker');
|
const m = document.getElementById('todayMarker');
|
||||||
if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||||
@@ -581,9 +753,8 @@ function filterTable() {
|
|||||||
|
|
||||||
function exportDrawIOXML(mode) {
|
function exportDrawIOXML(mode) {
|
||||||
try {
|
try {
|
||||||
// 1. Sécurité : Vérification des données
|
|
||||||
if (!filteredTickets || filteredTickets.length === 0) {
|
if (!filteredTickets || filteredTickets.length === 0) {
|
||||||
showToast("Aucun ticket à exporter", "error");
|
showToast("No tickets to export", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,18 +762,16 @@ function exportDrawIOXML(mode) {
|
|||||||
const ticketsWithDates = filteredTickets.filter(t => t.dueDate || t[goLiveCol]);
|
const ticketsWithDates = filteredTickets.filter(t => t.dueDate || t[goLiveCol]);
|
||||||
|
|
||||||
if (ticketsWithDates.length === 0) {
|
if (ticketsWithDates.length === 0) {
|
||||||
showToast("Aucune date de fin trouvée", "error");
|
showToast("No end dates found", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Paramètres de vue (Zoom)
|
|
||||||
const leftWidth = 250;
|
const leftWidth = 250;
|
||||||
const rowHeight = 50;
|
const rowHeight = 50;
|
||||||
let pixelPerDay = 12; // Mois
|
let pixelPerDay = 12;
|
||||||
if (currentGanttView === 'quarter') pixelPerDay = 4;
|
if (currentGanttView === 'quarter') pixelPerDay = 4;
|
||||||
if (currentGanttView === 'year') pixelPerDay = 1.2;
|
if (currentGanttView === 'year') pixelPerDay = 1.2;
|
||||||
|
|
||||||
// 3. Calcul des bornes temporelles
|
|
||||||
const allDates = ticketsWithDates.map(t => new Date(t.dueDate || t[goLiveCol]));
|
const allDates = ticketsWithDates.map(t => new Date(t.dueDate || t[goLiveCol]));
|
||||||
let minDate = new Date(Math.min(...allDates));
|
let minDate = new Date(Math.min(...allDates));
|
||||||
minDate.setDate(1);
|
minDate.setDate(1);
|
||||||
@@ -617,14 +786,13 @@ function exportDrawIOXML(mode) {
|
|||||||
|
|
||||||
let xmlNodes = '';
|
let xmlNodes = '';
|
||||||
|
|
||||||
// 4. TIMELINE & LIGNES VERTICALES
|
|
||||||
let curr = new Date(minDate);
|
let curr = new Date(minDate);
|
||||||
while (curr < maxDate) {
|
while (curr < maxDate) {
|
||||||
const xPos = getX(curr);
|
const xPos = getX(curr);
|
||||||
let label = "";
|
let label = "";
|
||||||
|
|
||||||
if (currentGanttView === 'month') {
|
if (currentGanttView === 'month') {
|
||||||
label = curr.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' }).toUpperCase();
|
label = curr.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }).toUpperCase();
|
||||||
curr.setMonth(curr.getMonth() + 1);
|
curr.setMonth(curr.getMonth() + 1);
|
||||||
} else if (currentGanttView === 'quarter') {
|
} else if (currentGanttView === 'quarter') {
|
||||||
label = "Q" + (Math.floor(curr.getMonth() / 3) + 1) + " " + curr.getFullYear();
|
label = "Q" + (Math.floor(curr.getMonth() / 3) + 1) + " " + curr.getFullYear();
|
||||||
@@ -634,34 +802,29 @@ function exportDrawIOXML(mode) {
|
|||||||
curr.setFullYear(curr.getFullYear() + 1);
|
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">
|
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"/>
|
<mxGeometry x="${xPos + 5}" y="5" width="150" height="30" as="geometry"/>
|
||||||
</mxCell>`;
|
</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">
|
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"/>
|
<mxGeometry x="${xPos}" y="0" width="10" height="4000" as="geometry"/>
|
||||||
</mxCell>`;
|
</mxCell>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. LIGNE TODAY
|
|
||||||
const xToday = getX(new Date());
|
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">
|
xmlNodes += `<mxCell id="today" value="TODAY" 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"/>
|
<mxGeometry x="${xToday}" y="0" width="10" height="4000" as="geometry"/>
|
||||||
</mxCell>`;
|
</mxCell>`;
|
||||||
|
|
||||||
// 6. GÉNÉRATION DES TICKETS
|
|
||||||
let currentY = 80;
|
let currentY = 80;
|
||||||
const groups = ticketsWithDates.reduce((acc, t) => {
|
const groups = ticketsWithDates.reduce((acc, t) => {
|
||||||
const a = t.assignee || 'Non assigné';
|
const a = t.assignee || 'Unassigned';
|
||||||
if (!acc[a]) acc[a] = [];
|
if (!acc[a]) acc[a] = [];
|
||||||
acc[a].push(t);
|
acc[a].push(t);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
Object.keys(groups).sort().forEach(assignee => {
|
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">
|
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"/>
|
<mxGeometry x="10" y="${currentY}" width="200" height="30" as="geometry"/>
|
||||||
</mxCell>`;
|
</mxCell>`;
|
||||||
@@ -669,9 +832,10 @@ function exportDrawIOXML(mode) {
|
|||||||
|
|
||||||
groups[assignee].forEach(t => {
|
groups[assignee].forEach(t => {
|
||||||
const { shortTitle, type } = parseTicketSummary(t.summary);
|
const { shortTitle, type } = parseTicketSummary(t.summary);
|
||||||
const color = getTypeColor(type);
|
const mappedStatus = mapJiraStatus(t.status);
|
||||||
|
const statusDef = STATUS_DEFINITIONS[mappedStatus] || STATUS_DEFINITIONS['Opportunity'];
|
||||||
|
const barColor = statusDef.drawioColor;
|
||||||
|
|
||||||
// Durée réelle (Création -> DueDate)
|
|
||||||
const startDate = t.created ? new Date(t.created) : minDate;
|
const startDate = t.created ? new Date(t.created) : minDate;
|
||||||
const endDate = new Date(t.dueDate || t[goLiveCol]);
|
const endDate = new Date(t.dueDate || t[goLiveCol]);
|
||||||
|
|
||||||
@@ -680,14 +844,12 @@ function exportDrawIOXML(mode) {
|
|||||||
let barWidth = xEnd - xStart;
|
let barWidth = xEnd - xStart;
|
||||||
if (barWidth < 50) barWidth = 140;
|
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">
|
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"/>
|
<mxGeometry x="30" y="${currentY}" width="200" height="30" as="geometry"/>
|
||||||
</mxCell>`;
|
</mxCell>`;
|
||||||
|
|
||||||
// Barre de Gantt
|
const label = `[${mappedStatus}] [${type}] ${escHtml(shortTitle)}`;
|
||||||
const label = `[${type}] ${escHtml(shortTitle)}`;
|
xmlNodes += `<mxCell id="bar_${currentY}" value="${label}" style="rounded=1;fillColor=${barColor};strokeColor=#FFFFFF;fontColor=#FFFFFF;fontSize=10;fontStyle=1;spacingLeft=8;align=left" vertex="1" parent="1">
|
||||||
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"/>
|
<mxGeometry x="${xStart}" y="${currentY}" width="${barWidth}" height="28" as="geometry"/>
|
||||||
</mxCell>`;
|
</mxCell>`;
|
||||||
|
|
||||||
@@ -696,7 +858,6 @@ function exportDrawIOXML(mode) {
|
|||||||
currentY += 20;
|
currentY += 20;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. ASSEMBLAGE FINAL
|
|
||||||
const finalXml = `<?xml version="1.0" encoding="UTF-8"?>
|
const finalXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<mxfile host="app.diagrams.net">
|
<mxfile host="app.diagrams.net">
|
||||||
<diagram id="Jira" name="Roadmap">
|
<diagram id="Jira" name="Roadmap">
|
||||||
@@ -717,12 +878,12 @@ function exportDrawIOXML(mode) {
|
|||||||
a.download = "jira_export.drawio";
|
a.download = "jira_export.drawio";
|
||||||
a.click();
|
a.click();
|
||||||
} else {
|
} else {
|
||||||
navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copié !"));
|
navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copied!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showToast("Erreur lors de l'export XML", "error");
|
showToast("Error during XML export", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,14 +902,13 @@ function downloadJSON() {
|
|||||||
a.download = `export-${Date.now()}.json`; a.click();
|
a.download = `export-${Date.now()}.json`; a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(() => showToast('Copié !')); }
|
function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(() => showToast('Copied!')); }
|
||||||
|
|
||||||
function clearResults() { allTickets=[]; filteredTickets=[]; document.getElementById('results-section').classList.remove('visible'); }
|
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()} });
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initTheme();
|
initTheme();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
console.log("Paramètres chargés depuis le stockage local.");
|
console.log("Settings loaded from local storage.");
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user