From ef90251b5c60c312b911d626b481dbdfd1379de9 Mon Sep 17 00:00:00 2001 From: gautier Date: Fri, 8 May 2026 18:35:50 +0200 Subject: [PATCH] gant reviewed, added links, css improvements --- index.html | 69 ++- public/css/style.css | 1048 ++++++++++++++++++++++++++++++++++++++---- public/js/script.js | 602 +++++++++++------------- 3 files changed, 1267 insertions(+), 452 deletions(-) diff --git a/index.html b/index.html index c57b601..d18b6f0 100644 --- a/index.html +++ b/index.html @@ -20,10 +20,10 @@

Jira Ticket Extractor

Extraction via API locale — Proxy transparent

-
API : localhost:3000
+
API : localhost:3000
-
+
Paramètres d'extraction
@@ -41,7 +41,7 @@
- +
@@ -53,7 +53,7 @@
-
+
@@ -65,36 +65,65 @@
-
-
+ +
+
- - - + + +
-
+
- +
- -
+ + + +
+
+ + + +
+
+
+ +
-
-
- Affichage par mois pour plus de lisibilité - + + +
+
+
+
+
-
-
+
+
+ + +
diff --git a/public/css/style.css b/public/css/style.css index 4e473ee..3e865c7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,106 +1,980 @@ :root { - --bg: #0f0a0f; --bg-elevated: #111a16; --bg-card: #152019; --bg-input: #0d1511; - --fg: #e8f0ec; --fg-muted: #7a9488; --fg-dim: #4a6558; - --accent: #00e68a; --accent-dim: #00b36b; --accent-glow: rgba(0,230,138,0.15); --accent-glow-strong: rgba(0,230,138,0.3); - --border: #1e3028; --danger: #ff4d6a; --warning: #ffb84d; --info: #4dc8ff; - --radius: 10px; --radius-lg: 16px; --shadow: 0 4px 24px rgba(0,0,0,0.4); + --bg: #0f0a0f; + --bg-elevated: #111a16; + --bg-card: #152019; + --bg-input: #0d1511; + --fg: #e8f0ec; + --fg-muted: #7a9488; + --fg-dim: #4a6558; + --accent: #00e68a; + --accent-dim: #00b36b; + --accent-glow: rgba(0, 230, 138, 0.15); + --accent-glow-strong: rgba(0, 230, 138, 0.3); + --border: #1e3028; + --danger: #ff4d6a; + --warning: #ffb84d; + --info: #4dc8ff; + --radius: 10px; + --radius-lg: 16px; + --shadow: 0 4px 24px rgba(0, 0, 0, 0.4); } -* { margin: 0; padding: 0; box-sizing: border-box; } -body { font-family: 'DM Sans', sans-serif; background: var(--bg); color: var(--fg); min-height: 100vh; } -.bg-grid { position: fixed; inset: 0; z-index: 0; pointer-events: none; background-image: linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px); background-size: 60px 60px; opacity: 0.3; mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black 20%, transparent 70%); } -.bg-glow { position: fixed; z-index: 0; pointer-events: none; width: 600px; height: 600px; border-radius: 50%; filter: blur(120px); opacity: 0.12; } -.bg-glow--1 { top: -200px; right: -100px; background: var(--accent); animation: gf 12s ease-in-out infinite; } -@keyframes gf { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(30px,-40px) scale(1.1)} } -.app { position: relative; z-index: 1; max-width: 90%; margin: 0 auto; padding: 32px 24px 80px; } -.header { display: flex; align-items: center; gap: 16px; margin-bottom: 40px; padding-bottom: 24px; border-bottom: 1px solid var(--border); } -.header-icon { width: 52px; height: 52px; background: linear-gradient(135deg, var(--accent), var(--accent-dim)); border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--bg); box-shadow: 0 0 30px var(--accent-glow-strong); } -.header-text h1 { font-size: 26px; font-weight: 900; letter-spacing: -0.5px; } -.header-text p { color: var(--fg-muted); font-size: 14px; margin-top: 4px; } -.badge-api { background: var(--accent-glow); color: var(--accent); font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 20px; border: 1px solid rgba(0,230,138,0.2); margin-left: auto; font-family: 'JetBrains Mono', monospace; } +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} -.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 28px; margin-bottom: 24px; box-shadow: var(--shadow); max-width: 60%; margin-left:auto; margin-right: auto; } -.card-title { font-size: 16px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; } -.card-title i { color: var(--accent); font-size: 14px; } +body { + font-family: 'DM Sans', sans-serif; + background: var(--bg); + color: var(--fg); + min-height: 100vh; +} -.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } -.form-grid .full { grid-column: 1 / -1; } -.form-group { display: flex; flex-direction: column; gap: 6px; } -.form-group label { font-size: 12px; font-weight: 600; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.8px; } -.form-group input, .form-group textarea { background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 14px; color: var(--fg); font-family: 'DM Sans', sans-serif; font-size: 14px; outline: none; transition: border-color 0.25s, box-shadow 0.25s; } -.form-group input:focus, .form-group textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } -.form-group textarea { resize: vertical; min-height: 60px; font-family: 'JetBrains Mono', monospace; font-size: 12px; } -.hint { font-size: 11px; color: var(--fg-dim); font-style: italic; } +.bg-grid { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px); + background-size: 60px 60px; + opacity: 0.3; + mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black 20%, transparent 70%); +} -.btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; border-radius: var(--radius); border: none; font-family: 'DM Sans', sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.25s; outline: none; } -.btn:focus-visible { box-shadow: 0 0 0 3px var(--accent-glow-strong); } -.btn-primary { background: linear-gradient(135deg, var(--accent), var(--accent-dim)); color: var(--bg); box-shadow: 0 4px 20px var(--accent-glow-strong); } -.btn-primary:hover { transform: translateY(-1px); } -.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } -.btn-secondary { background: var(--bg-input); color: var(--fg); border: 1px solid var(--border); } -.btn-secondary:hover { border-color: var(--fg-dim); } -.btn-danger { background: rgba(255,77,106,0.1); color: var(--danger); border: 1px solid rgba(255,77,106,0.2); } -.btn-group { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; } +.bg-glow { + position: fixed; + z-index: 0; + pointer-events: none; + width: 600px; + height: 600px; + border-radius: 50%; + filter: blur(120px); + opacity: 0.12; +} -.alert { padding: 14px 18px; border-radius: var(--radius); font-size: 13px; display: flex; align-items: flex-start; gap: 10px; margin-bottom: 16px; animation: fadeIn 0.3s ease; } -@keyframes fadeIn { from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:translateY(0)} } -.alert-info { background: rgba(77,200,255,0.1); border: 1px solid rgba(77,200,255,0.2); color: var(--info); } -.alert-danger { background: rgba(255,77,106,0.1); border: 1px solid rgba(255,77,106,0.2); color: var(--danger); } +.bg-glow--1 { + top: -200px; + right: -100px; + background: var(--accent); + animation: gf 12s ease-in-out infinite; +} -.spinner { display: inline-block; width: 18px; height: 18px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: spin 0.7s linear infinite; } -@keyframes spin { to{transform:rotate(360deg)} } +@keyframes gf { -.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; } -.stat-card { background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; text-align: center; transition: transform 0.25s; } -.stat-card:hover { transform: translateY(-2px); } -.stat-value { font-size: 28px; font-weight: 900; color: var(--accent); font-family: 'JetBrains Mono', monospace; } -.stat-label { font-size: 11px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.8px; margin-top: 4px; } + 0%, + 100% { + transform: translate(0, 0) scale(1) + } -.tabs { display: flex; gap: 4px; background: var(--bg-input); border-radius: var(--radius); padding: 4px; margin-bottom: 20px; border: 1px solid var(--border); width: fit-content; } -.tab-btn { padding: 9px 20px; border: none; background: transparent; color: var(--fg-muted); font-family: 'DM Sans', sans-serif; font-size: 13px; font-weight: 600; border-radius: 7px; cursor: pointer; transition: all 0.25s; outline: none; } -.tab-btn.active { background: var(--accent); color: var(--bg); box-shadow: 0 2px 12px var(--accent-glow-strong); } -.tab-panel { display: none; } .tab-panel.active { display: block; animation: fadeIn 0.3s ease; } + 50% { + transform: translate(30px, -40px) scale(1.1) + } +} -.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); } -table { width: 100%; border-collapse: collapse; font-size: 13px; } -thead { background: var(--bg-elevated); position: sticky; top: 0; z-index: 2; } -th { padding: 12px 14px; text-align: left; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--fg-muted); border-bottom: 2px solid var(--border); white-space: nowrap; } -td { padding: 10px 14px; border-bottom: 1px solid var(--border); color: var(--fg); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -tr:hover td { background: rgba(0,230,138,0.03); } -.ticket-key { font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--accent); font-size: 12px; } -.status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; white-space: nowrap; } -.s-done { background: rgba(0,230,138,0.15); color: #00e68a; } .s-progress { background: rgba(77,200,255,0.15); color: #4dc8ff; } -.s-todo { background: rgba(255,184,77,0.15); color: #ffb84d; } .s-blocked { background: rgba(255,77,106,0.15); color: #ff4d6a; } .s-other { background: rgba(122,148,136,0.15); color: #7a9488; } -.empty-cell { color: var(--fg-dim); font-style: italic; } +.app { + position: relative; + z-index: 1; + max-width: 90%; + margin: 0 auto; + padding: 32px 24px 80px; +} + +.header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 40px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border); +} + +.header-icon { + width: 52px; + height: 52px; + background: linear-gradient(135deg, var(--accent), var(--accent-dim)); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + color: var(--bg); + box-shadow: 0 0 30px var(--accent-glow-strong); +} + +.header-text h1 { + font-size: 26px; + font-weight: 900; + letter-spacing: -0.5px; +} + +.header-text p { + color: var(--fg-muted); + font-size: 14px; + margin-top: 4px; +} + +.badge-api { + background: var(--accent-glow); + color: var(--accent); + font-size: 11px; + font-weight: 700; + padding: 4px 10px; + border-radius: 20px; + border: 1px solid rgba(0, 230, 138, 0.2); + margin-left: auto; + font-family: 'JetBrains Mono', monospace; +} + +.card-form { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 28px; + margin-bottom: 24px; + box-shadow: var(--shadow); + max-width: 60%; + margin-left: auto; + margin-right: auto; +} + +.card-title { + font-size: 16px; + font-weight: 700; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.card-title i { + color: var(--accent); + font-size: 14px; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-grid .full { + grid-column: 1 / -1; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 12px; + font-weight: 600; + color: var(--fg-muted); + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.form-group input, +.form-group textarea { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 11px 14px; + color: var(--fg); + font-family: 'DM Sans', sans-serif; + font-size: 14px; + outline: none; + transition: border-color 0.25s, box-shadow 0.25s; +} + +.form-group input:focus, +.form-group textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.form-group textarea { + resize: vertical; + min-height: 60px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; +} + +.hint { + font-size: 11px; + color: var(--fg-dim); + font-style: italic; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + border-radius: var(--radius); + border: none; + font-family: 'DM Sans', sans-serif; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.25s; + outline: none; +} + +.btn:focus-visible { + box-shadow: 0 0 0 3px var(--accent-glow-strong); +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent), var(--accent-dim)); + color: var(--bg); + box-shadow: 0 4px 20px var(--accent-glow-strong); +} + +.btn-primary:hover { + transform: translateY(-1px); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: var(--bg-input); + color: var(--fg); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + border-color: var(--fg-dim); +} + +.btn-danger { + background: rgba(255, 77, 106, 0.1); + color: var(--danger); + border: 1px solid rgba(255, 77, 106, 0.2); +} + +.btn-group { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 8px; +} + +.alert { + padding: 14px 18px; + border-radius: var(--radius); + font-size: 13px; + display: flex; + align-items: flex-start; + gap: 10px; + margin-bottom: 16px; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-8px) + } + + to { + opacity: 1; + transform: translateY(0) + } +} + +.alert-info { + background: rgba(77, 200, 255, 0.1); + border: 1px solid rgba(77, 200, 255, 0.2); + color: var(--info); +} + +.alert-danger { + background: rgba(255, 77, 106, 0.1); + border: 1px solid rgba(255, 77, 106, 0.2); + color: var(--danger); +} + +.spinner { + display: inline-block; + width: 18px; + height: 18px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg) + } +} + +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + text-align: center; + transition: transform 0.25s; +} + +.stat-card:hover { + transform: translateY(-2px); +} + +.stat-value { + font-size: 28px; + font-weight: 900; + color: var(--accent); + font-family: 'JetBrains Mono', monospace; +} + +.stat-label { + font-size: 11px; + color: var(--fg-muted); + text-transform: uppercase; + letter-spacing: 0.8px; + margin-top: 4px; +} + +.tabs { + display: flex; + gap: 4px; + background: var(--bg-input); + border-radius: var(--radius); + padding: 4px; + margin-bottom: 20px; + border: 1px solid var(--border); + width: fit-content; +} + +.tab-btn { + padding: 9px 20px; + border: none; + background: transparent; + color: var(--fg-muted); + font-family: 'DM Sans', sans-serif; + font-size: 13px; + font-weight: 600; + border-radius: 7px; + cursor: pointer; + transition: all 0.25s; + outline: none; +} + +.tab-btn.active { + background: var(--accent); + color: var(--bg); + box-shadow: 0 2px 12px var(--accent-glow-strong); +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; + animation: fadeIn 0.3s ease; +} + +.table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +thead { + background: var(--bg-elevated); + position: sticky; + top: 0; + z-index: 2; +} + +th { + padding: 12px 14px; + text-align: left; + font-weight: 700; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--fg-muted); + border-bottom: 2px solid var(--border); + white-space: nowrap; +} + +td { + padding: 10px 14px; + border-bottom: 1px solid var(--border); + color: var(--fg); + max-width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +tr:hover td { + background: rgba(0, 230, 138, 0.03); +} + +.ticket-key { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + color: var(--accent); + font-size: 12px; + text-decoration: underline; + cursor: pointer; +} + +.status-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.s-done { + background: rgba(0, 230, 138, 0.15); + color: #00e68a; +} + +.s-progress { + background: rgba(77, 200, 255, 0.15); + color: #4dc8ff; +} + +.s-todo { + background: rgba(255, 184, 77, 0.15); + color: #ffb84d; +} + +.s-blocked { + background: rgba(255, 77, 106, 0.15); + color: #ff4d6a; +} + +.s-other { + background: rgba(122, 148, 136, 0.15); + color: #7a9488; +} + +.empty-cell { + color: var(--fg-dim); + font-style: italic; +} /* Date input styling */ -.date-input { background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; color: var(--fg); font-family: 'DM Sans', sans-serif; font-size: 12px; outline: none; transition: border-color 0.25s, box-shadow 0.25s; width: 100px; } -.date-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); } -.date-input::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.6; cursor: pointer; } -.date-input::-webkit-calendar-picker-indicator:hover { opacity: 1; } +.date-input { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + padding: 4px 8px; + color: var(--fg); + font-family: 'DM Sans', sans-serif; + font-size: 12px; + outline: none; + transition: border-color 0.25s, box-shadow 0.25s; + width: 150px; +} + +.date-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +.date-input::-webkit-calendar-picker-indicator { + filter: invert(1); + opacity: 0.6; + cursor: pointer; +} + +.date-input::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} /* Update button styling */ -.btn-update { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 6px; border: none; font-family: 'DM Sans', sans-serif; font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.25s; background: linear-gradient(135deg, var(--accent), var(--accent-dim)); color: var(--bg); } -.btn-update:hover { transform: translateY(-1px); box-shadow: 0 2px 8px var(--accent-glow-strong); } -.btn-update:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; } -.btn-update .spinner { width: 12px; height: 12px; border-width: 2px; } -.btn-update i { font-size: 10px; } +.btn-update { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + border: none; + font-family: 'DM Sans', sans-serif; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.25s; + background: linear-gradient(135deg, var(--accent), var(--accent-dim)); + color: var(--bg); +} -.json-viewer { background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; max-height: 600px; overflow: auto; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7; white-space: pre-wrap; word-break: break-all; } -.json-key { color: #4dc8ff; } .json-string { color: #00e68a; } .json-number { color: #ffb84d; } .json-bool { color: #ff4d6a; } .json-null { color: var(--fg-dim); } +.btn-update:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px var(--accent-glow-strong); +} -.search-bar { position: relative; margin-bottom: 16px; } -.search-bar i { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--fg-dim); } -.search-bar input { width: 100%; background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 14px 11px 38px; color: var(--fg); font-family: 'DM Sans', sans-serif; font-size: 14px; outline: none; transition: border-color 0.25s, box-shadow 0.25s; } -.search-bar input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } +.btn-update:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: none; +} -#results-section { display: none; } #results-section.visible { display: block; animation: fadeIn 0.4s ease; } -.toast-container { position: fixed; top: 24px; right: 24px; z-index: 9999; display: flex; flex-direction: column; gap: 10px; } -.toast { padding: 14px 20px; border-radius: var(--radius); font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 10px; box-shadow: 0 8px 30px rgba(0,0,0,0.5); animation: tIn 0.35s ease, tOut 0.35s ease 3s forwards; max-width: 400px; } -.toast-success { background: #0d2818; border: 1px solid rgba(0,230,138,0.3); color: var(--accent); } -.toast-error { background: #280d14; border: 1px solid rgba(255,77,106,0.3); color: var(--danger); } -@keyframes tIn { from{opacity:0;transform:translateX(40px)} to{opacity:1;transform:translateX(0)} } -@keyframes tOut { to{opacity:0;transform:translateX(40px)} } +.btn-update .spinner { + width: 12px; + height: 12px; + border-width: 2px; +} -@media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } .btn-group { flex-direction: column; } .btn { width: 100%; justify-content: center; } } -@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } \ No newline at end of file +.btn-update i { + font-size: 10px; +} + +.json-viewer { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + max-height: 600px; + overflow: auto; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + line-height: 1.7; + white-space: pre-wrap; + word-break: break-all; +} + +.json-key { + color: #4dc8ff; +} + +.json-string { + color: #00e68a; +} + +.json-number { + color: #ffb84d; +} + +.json-bool { + color: #ff4d6a; +} + +.json-null { + color: var(--fg-dim); +} + +.search-bar { + position: relative; + margin-bottom: 16px; +} + +.search-bar i { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--fg-dim); +} + +.search-bar input { + width: 100%; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 11px 14px 11px 38px; + color: var(--fg); + font-family: 'DM Sans', sans-serif; + font-size: 14px; + outline: none; + transition: border-color 0.25s, box-shadow 0.25s; +} + +.search-bar input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +#results-section { + display: none; +} + +#results-section.visible { + display: block; + animation: fadeIn 0.4s ease; +} + +.toast-container { + position: fixed; + top: 24px; + right: 24px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; +} + +.toast { + padding: 14px 20px; + border-radius: var(--radius); + font-size: 13px; + font-weight: 500; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5); + animation: tIn 0.35s ease, tOut 0.35s ease 3s forwards; + max-width: 400px; +} + +.toast-success { + background: #0d2818; + border: 1px solid rgba(0, 230, 138, 0.3); + color: var(--accent); +} + +.toast-error { + background: #280d14; + border: 1px solid rgba(255, 77, 106, 0.3); + color: var(--danger); +} + +@keyframes tIn { + from { + opacity: 0; + transform: translateX(40px) + } + + to { + opacity: 1; + transform: translateX(0) + } +} + +@keyframes tOut { + to { + opacity: 0; + transform: translateX(40px) + } +} + +@media (max-width: 768px) { + .form-grid { + grid-template-columns: 1fr; + } + + .btn-group { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } +} + +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + + +/* --- Classes Utilitaires --- */ +.mt-24 { margin-top: 24px; } +.mb-16 { margin-bottom: 16px; } +.p-20 { padding: 20px; } +.p-small { padding: 12px 20px; } +.no-margin { margin-top: 0 !important; } +.uppercase-input { text-transform: uppercase; } + +/* --- Alignements --- */ +.flex-center-between { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +/* --- Boutons et Badges Spécifiques --- */ +.badge-api i { + margin-right: 6px; +} + +.btn-drawio { + background: linear-gradient(135deg, #4dc8ff, #0d8bbf); +} + +.btn-sm { + padding: 8px 16px; + font-size: 12px; +} + +.info-icon { + color: var(--info); + margin-right: 8px; +} + +/* --- Organisation de la barre d'actions --- */ +.card-actions { + padding: 16px 20px; +} + +.actions-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +.tabs .tab-btn i { + margin-right: 6px; +} + +/* --- Ajustement Roadmap Preview --- */ +#roadmapPreview { + /* Suppression du padding ici car il est dans le parent .card p-20 */ + background: var(--bg-input); + border-radius: var(--radius); + min-height: 300px; +} + + +/* +========== +GANTT +========== +*/ + +/* Conteneur principal avec scrollbar */ +.gantt-container { + position: relative; + height: 600px; + overflow: auto; /* Un seul scroll géré ici */ + background: #0a0f0d; + border: 1px solid #1e3028; + border-radius: 8px; +} + +/* Ligne du temps (Header) */ +.gantt-header { + display: flex; + position: sticky; + top: 0; + z-index: 100; + background: #111a16; + border-bottom: 2px solid #1e3028; +} + +.gantt-header-label { + position: sticky; + left: 0; + z-index: 101; + background: #1a1a1a; + border-right: 2px solid var(--border); + padding-left: 10px; + display: flex; + align-items: center; + font-weight: bold; + color: var(--primary); +} + +/* Groupes par Assigné */ +.gantt-group-row { + display: flex; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border); + font-weight: 600; +} + +.gantt-assignee-sticky { + position: sticky; + left: 0; + z-index: 50; + background: #1a1a1a; + padding: 8px 10px; + border-right: 2px solid var(--border); + color: var(--accent); + font-size: 12px; +} + +/* Lignes de tickets */ +.gantt-ticket-row { + display: flex; + border-bottom: 1px solid var(--border); + height: 45px; + position: relative; + align-items: center; +} + +.gantt-key-sticky { + position: sticky; + left: 0; + z-index: 50; + background: var(--bg-input); + border-right: 2px solid var(--border); + height: 100%; + display: flex; + align-items: center; + padding-left: 25px; + font-size: 11px; + color: var(--fg-dim); +} + +/* La barre Gantt elle-même */ +.gantt-bar { + position: absolute; + height: 28px; + border-radius: 4px; + display: flex; + align-items: center; + padding: 0 8px; + font-size: 10px; + color: white; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: help; + z-index: 10; +} + +/* Colonne assigné/ticket figée à gauche */ +.gantt-sticky-col { + position: sticky; + left: 0; + z-index: 110; + background: #111a16; + border-right: 2px solid #1e3028; + padding: 0 10px; + display: flex; + align-items: center; +} + +/* La ligne rouge verticale pour aujourd'hui */ +.gantt-today-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background-color: #ff4d6a; + z-index: 80; + pointer-events: none; +} + +.gantt-today-label { + position: absolute; + top: 2px; + transform: translateX(-50%); + background: #ff4d6a; + color: white; + padding: 2px 5px; + border-radius: 3px; + font-size: 9px; + font-weight: bold; + z-index: 81; +} + +/* --- Structure de la Grille Gantt --- */ + +/* Conteneur de la largeur totale du canvas */ +.gantt-canvas { + position: relative; + display: inline-block; + min-width: 100%; +} + +/* Colonnes des mois dans le header */ +.gantt-month-header { + min-width: 140px; /* Doit correspondre à monthWidth dans le JS */ + width: 140px; + text-align: center; + font-size: 11px; + color: #7a9488; + display: flex; + align-items: center; + justify-content: center; + border-left: 1px solid #1e3028; +} + +/* Ligne de groupe (Assigné) */ +.gantt-group-header { + display: flex; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid #1e3028; +} + +.gantt-group-name { + width: 250px; /* Doit correspondre à leftWidth dans le JS */ + padding: 8px 10px; + color: var(--accent); + font-weight: bold; +} + +/* Ligne de ticket */ +.gantt-row { + display: flex; + height: 45px; + border-bottom: 1px solid #1e3028; + position: relative; + align-items: center; +} + +.gantt-ticket-id { + width: 250px; /* leftWidth */ + height: 100%; + font-size: 11px; + color: #7a9488; + padding-left: 20px; +} + +/* Grille de fond (colonnes verticales sous les barres) */ +.gantt-grid-col { + min-width: 140px; + border-right: 1px solid rgba(255, 255, 255, 0.05); + height: 100%; +} + +.view-controls { + display: flex; + align-items: center; + gap: 10px; +} + +.mr-8 { margin-right: 8px; } + +/* Optionnel : style pour le bouton de vue actif */ +.view-controls .btn.active { + background-color: var(--primary); + color: white; + border-color: var(--primary); +} \ No newline at end of file diff --git a/public/js/script.js b/public/js/script.js index 7d27fb0..305249e 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -1,11 +1,14 @@ - const API_BASE = ''; let allTickets = []; let filteredTickets = []; let customFieldsMap = []; let lastUsername = ''; let lastApiToken = ''; +let currentGanttView = 'month'; +let currentSortCol = null; +let isAsc = true; +// --- UTILITAIRES --- function showToast(msg, type='success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); @@ -28,11 +31,10 @@ async function pingApi() { const orig = btn.innerHTML; btn.innerHTML = ' Test...'; btn.disabled = true; try { const r = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username:'_', apiToken:'_', projectKey:'_'}) }); - const d = await r.json(); - if (r.status === 400) showToast('API locale connectée ! (Erreur 400 = normal, on a envoyé de fausses données)', 'success'); + if (r.status === 400) showToast('API locale connectée !', 'success'); else showToast('API répond : ' + r.status, 'success'); } catch(e) { - showToast('API inaccessible. Lancez "npm start" dans le dossier du serveur.', 'error'); + showToast('API inaccessible. Lancez "npm start".', 'error'); } finally { btn.innerHTML = orig; btn.disabled = false; } } @@ -44,9 +46,9 @@ async function startExtraction() { const jql = document.getElementById('jqlQuery').value.trim(); const custom = document.getElementById('customFields').value.trim(); - if (!user || !token || !project) { showToast('Remplissez utilisateur, token et clé du projet.', 'error'); return; } + if (!user || !token || !project) { showToast('Champs requis manquants.', 'error'); return; } - btn.innerHTML = ' Extraction via API...'; btn.disabled = true; + btn.innerHTML = ' Extraction...'; btn.disabled = true; try { const resp = await fetch(`${API_BASE}/api/extract`, { @@ -55,19 +57,16 @@ async function startExtraction() { body: JSON.stringify({ username: user, apiToken: token, projectKey: project, jql: jql, customFields: custom }) }); const data = await resp.json(); - - if (!resp.ok) { - throw new Error(data.error || `Erreur HTTP ${resp.status}`); - } + if (!resp.ok) throw new Error(data.error || `Erreur ${resp.status}`); allTickets = data.tickets; filteredTickets = [...allTickets]; customFieldsMap = parseCustomFields(custom); lastUsername = user; lastApiToken = token; + renderResults(); - showToast(`${allTickets.length} tickets extraits avec succès.`, 'success'); - + showToast(`${allTickets.length} tickets extraits.`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { @@ -86,6 +85,34 @@ function parseCustomFields(customFields) { return map; } +function sortTable(colKey) { + // Si on clique sur la même colonne, on inverse l'ordre, sinon on passe en croissant + if (currentSortCol === colKey) { + isAsc = !isAsc; + } else { + currentSortCol = colKey; + isAsc = true; + } + + filteredTickets.sort((a, b) => { + let valA = a[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') { + valA = new Date(valA); + valB = new Date(valB); + } + + if (valA < valB) return isAsc ? -1 : 1; + if (valA > valB) return isAsc ? 1 : -1; + return 0; + }); + + renderTable(); +} + +// --- MISE À JOUR TICKET --- async function updateTicket(ticketKey, dueDate, goLiveDate) { if (!lastUsername || !lastApiToken) { showToast('Veuillez d\'abord extraire les tickets.', 'error'); @@ -98,27 +125,18 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) { btn.disabled = true; const goLiveField = customFieldsMap.find(cf => cf.label.toLowerCase().includes('go live')); - const updates = {}; if (dueDate !== undefined) updates.duedate = dueDate; - if (goLiveDate !== undefined && goLiveField) { - updates[goLiveField.id] = goLiveDate; - } + if (goLiveDate !== undefined && goLiveField) updates[goLiveField.id] = goLiveDate; try { const resp = await fetch(`${API_BASE}/api/update`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: lastUsername, - apiToken: lastApiToken, - ticketKey, - updates - }) + body: JSON.stringify({ username: lastUsername, apiToken: lastApiToken, ticketKey, updates }) }); - const data = await resp.json(); - if (!resp.ok) { + const data = await resp.json(); throw new Error(data.error || `Erreur HTTP ${resp.status}`); } @@ -129,8 +147,8 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) { } renderTable(); - showToast(`Ticket ${ticketKey} mis à jour avec succès !`, 'success'); - + renderRoadmap(); + showToast(`Ticket ${ticketKey} mis à jour !`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { @@ -139,9 +157,27 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) { } } +function enableUpdateBtn(ticketKey) { + const btn = document.getElementById(`update-${ticketKey}`); + if (btn) btn.disabled = false; +} + +function handleUpdateClick(ticketKey, goLiveColName) { + const dueInput = document.getElementById(`due-${ticketKey}`); + const dueDate = dueInput && dueInput.value ? dueInput.value : null; + + let goLiveDate = null; + if (goLiveColName) { + const goLiveInput = document.getElementById(`gl-${ticketKey}`); + goLiveDate = goLiveInput && goLiveInput.value ? goLiveInput.value : null; + } + updateTicket(ticketKey, dueDate, goLiveDate); +} + +// --- RENDU --- function renderResults() { document.getElementById('results-section').classList.add('visible'); - renderStats(); renderTable(); renderJSON(); + renderStats(); renderTable(); renderJSON(); renderRoadmap(); } function renderStats() { @@ -161,42 +197,57 @@ function fmtDate(d) { if(!d) return '-'; try { r 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() { - // Trouver le champ Go Live Date let goLiveColName = null; if (allTickets.length > 0) { - Object.keys(allTickets[0]).forEach(key => { - if (!['key','url','summary','description','status','statusCategory','assignee','reporter','priority','issueType','created','updated','dueDate','resolution','resolutionDate','labels','components','fixVersions','projectName'].includes(key)) { - goLiveColName = key; - } - }); + goLiveColName = Object.keys(allTickets[0]).find(key => + !['key','url','summary','description','status','statusCategory','assignee','reporter','priority','issueType','created','updated','dueDate','resolution','resolutionDate','labels','components','fixVersions','projectName'].includes(key) + ); } - const cols = ['Ticket','Summary','Statut','Assigné','Priorité','Type','Due Date','Créé le','Résolu le','Actions']; + // Mapping pour faire le lien entre le texte de l'en-tête et la propriété de l'objet ticket + const colMapping = { + 'Ticket': 'key', + 'Summary': 'summary', + 'Statut': 'status', + 'Assigné': 'assignee', + 'Priorité': 'priority', + 'Type': 'issueType', + 'Due Date': 'dueDate' + }; + + const cols = ['Ticket','Summary','Statut','Assigné','Priorité','Type','Due Date']; if (goLiveColName) { - cols.splice(cols.indexOf('Résolu le'), 0, goLiveColName, 'Actions'); + cols.push(goLiveColName); + colMapping[goLiveColName] = goLiveColName; // Ajout dynamique du champ personnalisé } - document.getElementById('tableHead').innerHTML = '' + cols.map(c=>`${c}`).join('') + ''; + cols.push('Actions'); + + // Génération des en-têtes avec gestion du clic pour le tri + document.getElementById('tableHead').innerHTML = '' + cols.map(c => { + const key = colMapping[c]; + if (!key) return `${c}`; // Pour la colonne 'Actions' + + const icon = currentSortCol === key ? (isAsc ? ' ▴' : ' ▾') : ''; + return ` + ${c}${icon} + `; + }).join('') + ''; + let body = ''; filteredTickets.forEach(t => { - // Due Date éditable const dueDateValue = t.dueDate ? t.dueDate.substring(0, 10) : ''; - const dueDateInput = ``; - body += ` - ${escHtml(t.key)} + ${escHtml(t.key)} ${escHtml(t.summary)} ${escHtml(t.status)} - ${escHtml(t.assignee) || '-'} + ${escHtml(t.assignee) || '-'} ${escHtml(t.priority)} ${escHtml(t.issueType)} - ${dueDateInput} - ${fmtDate(t.created)} - ${fmtDate(t.resolutionDate)}`; + `; if (goLiveColName) { - const goLiveDateValue = t[goLiveColName] ? t[goLiveColName].substring(0, 10) : ''; - const goLiveDateInput = ``; - body += `${goLiveDateInput}`; + const glVal = t[goLiveColName] ? t[goLiveColName].substring(0, 10) : ''; + body += ``; } body += ` @@ -205,74 +256,7 @@ function renderTable() { `; }); - if(!filteredTickets.length) body = `Aucun ticket`; - document.getElementById('tableBody').innerHTML = body; -} - -function enableUpdateBtn(ticketKey) { - const btn = document.getElementById(`update-${ticketKey}`); - if (btn) btn.disabled = false; -} - -function handleUpdateClick(ticketKey, goLiveColName) { - const dueInput = document.getElementById(`due-${ticketKey}`); - const dueDate = dueInput && dueInput.value ? dueInput.value : null; - - let goLiveDate = null; - if (goLiveColName) { - const goLiveInput = document.getElementById(`${goLiveColName.replace(/\s+/g, '-')}-${ticketKey}`); - goLiveDate = goLiveInput && goLiveInput.value ? goLiveInput.value : null; - } - - updateTicket(ticketKey, dueDate, goLiveDate); -} - -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 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'); -} - -function filterTable() { - const q = document.getElementById('searchInput').value.toLowerCase(); - filteredTickets = q ? allTickets.filter(t => Object.values(t).some(v => v && typeof v === 'string' && v.toLowerCase().includes(q))) : [...allTickets]; - renderTable(); -} - -function copyJSON() { - navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(()=>showToast('JSON copié.')).catch(()=>showToast('Erreur copie.','error')); -} - -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 = `jira-${document.getElementById('projectKey').value||'export'}-${Date.now()}.json`; - a.click(); URL.revokeObjectURL(a.href); showToast('Fichier téléchargé.'); -} - -function clearResults() { - allTickets=[]; filteredTickets=[]; document.getElementById('results-section').classList.remove('visible'); - document.getElementById('searchInput').value=''; -} - -// Fonction d'arrondi par mois pour la roadmap -function roundToMonth(date) { - if (!date) return null; - const d = new Date(date); - // Arrondir au 1er jour du mois - return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().split('T')[0]; + document.getElementById('tableBody').innerHTML = body || 'Aucun résultat'; } function getStatusColor(status) { @@ -284,252 +268,180 @@ function getStatusColor(status) { return '#7a9488'; } +// --- ROADMAP (GANTT) --- +function setGanttView(view) { + currentGanttView = view; + renderRoadmap(); +} + function renderRoadmap() { const preview = document.getElementById('roadmapPreview'); - if (filteredTickets.length === 0) { - preview.innerHTML = `

Aucun ticket a afficher

`; + if (!filteredTickets || filteredTickets.length === 0) { + preview.innerHTML = `
Aucun ticket à afficher.
`; return; } - // Trouver le champ Go Live Date - const goLiveColName = filteredTickets.length > 0 ? Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) : null; - - // Calculer les dates min et max avec arrondi par mois - const allDates = filteredTickets.flatMap(t => [t.dueDate, goLiveColName ? t[goLiveColName] : null].filter(d => d)); + const goLiveColName = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "Go Live Date"; + const leftWidth = 250; + let colWidth = 140; + if (currentGanttView === 'quarter') colWidth = 200; + if (currentGanttView === 'year') colWidth = 250; + + const allDates = filteredTickets.flatMap(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)); + }); + if (allDates.length === 0) { - preview.innerHTML = `

Aucune date disponible pour la roadmap

`; + preview.innerHTML = `
Aucune date disponible pour générer la roadmap.
`; return; } - const minDate = new Date(Math.min(...allDates.map(d => new Date(d)))); - const maxDate = new Date(Math.max(...allDates.map(d => new Date(d)))); - - // Arrondir aux mois entiers + 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.setDate(0); - - // Calculer le nombre de mois - const totalMonths = (maxDate.getFullYear() - minDate.getFullYear()) * 12 + (maxDate.getMonth() - minDate.getMonth()); - const monthWidth = 120; - const rowHeight = 60; - const headerHeight = 40; - const leftWidth = 280; - const canvasWidth = leftWidth + totalMonths * monthWidth + 50; - - // Grouper les tickets par mois de début et fin - const monthlyTickets = {}; - filteredTickets.forEach(ticket => { - const due = ticket.dueDate ? new Date(ticket.dueDate) : null; - const goLive = goLiveColName && ticket[goLiveColName] ? new Date(ticket[goLiveColName]) : null; - if (!due && !goLive) return; - - const start = due ? due : goLive; - const end = goLive ? goLive : due; - - const startMonth = `${start.getFullYear()}-${start.getMonth()}`; - const endMonth = `${end.getFullYear()}-${end.getMonth()}`; - - if (!monthlyTickets[startMonth]) monthlyTickets[startMonth] = []; - monthlyTickets[startMonth].push({ ticket, start, end }); - }); - - let html = `
`; - - // En-tête avec noms de mois - html += `
Mois
`; - - const months = []; - const currentMonth = new Date(minDate); - for (let m = 0; m <= totalMonths; m++) { - const monthKey = `${currentMonth.getFullYear()}-${currentMonth.getMonth()}`; - months.push(monthKey); - - const monthName = currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); - html += `
${monthName}
`; - - currentMonth.setMonth(currentMonth.getMonth() + 1); - } - - // Afficher les tickets par ligne (chaque ligne = un mois de début) - Object.keys(monthlyTickets).forEach((monthKey, idx) => { - const y = headerHeight + idx * rowHeight + 20; - const [year, month] = monthKey.split('-').map(Number); - const monthName = new Date(year, month, 1).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); - - // Ligne de séparation du mois - html += `
${monthName}
`; - - // Tickets de ce mois - const monthTickets = monthlyTickets[monthKey] || []; - monthTickets.forEach(({ticket, start, end}) => { - const startMonthIdx = months.indexOf(`${start.getFullYear()}-${start.getMonth()}`); - const endMonthIdx = months.indexOf(`${end.getFullYear()}-${end.getMonth()}`); - const barLeft = leftWidth + startMonthIdx * monthWidth + 10; - const barWidth = (endMonthIdx - startMonthIdx + 1) * monthWidth - 20; - - const color = getStatusColor(ticket.status); - - html += `
-
${escHtml(ticket.key)}
-
${escHtml(ticket.summary.substring(0, 40))}
-
${escHtml(ticket.assignee || 'N/A')} - ${escHtml(ticket.status || 'N/A')}
-
`; - }); - }); - - html += `
`; - preview.innerHTML = html; -} -function generateDrawioXML(tickets) { - // Trouver le champ Go Live Date - const goLiveColName = tickets.length > 0 ? Object.keys(tickets[0]).find(k => k.toLowerCase().includes('go live')) : null; - - // Calculer les dates min et max avec arrondi par mois - const allDates = tickets.flatMap(t => [t.dueDate, goLiveColName ? t[goLiveColName] : null].filter(d => d)); - if (allDates.length === 0) return null; - - const minDate = new Date(Math.min(...allDates.map(d => new Date(d)))); - const maxDate = new Date(Math.max(...allDates.map(d => new Date(d)))); - - // Arrondir aux mois entiers - minDate.setDate(1); - maxDate.setMonth(maxDate.getMonth() + 1); - maxDate.setDate(0); - - const totalMonths = (maxDate.getFullYear() - minDate.getFullYear()) * 12 + (maxDate.getMonth() - minDate.getMonth()); - const monthWidth = 100; - const rowHeight = 70; - const headerHeight = 50; - const leftWidth = 200; - const canvasWidth = Math.max(827, leftWidth + totalMonths * monthWidth + 100); - const canvasHeight = Math.max(1169, headerHeight + tickets.length * rowHeight + 50); - - let cellId = 2; - let cells = []; - - // Header background - cells.push({ - id: 'header', - value: 'Roadmap Jira (par mois)', - style: 'rounded=0;whiteSpace=wrap;html=1;fillColor=#111a16;strokeColor=#1e3028;fontColor=#e8f0ec;fontSize=14;fontStyle=1', - x: 0, - y: 0, - width: canvasWidth, - height: headerHeight - }); - - // En-têtes de mois - const months = []; - const currentMonth = new Date(minDate); - for (let m = 0; m <= totalMonths; m++) { - const monthName = currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); - cells.push({ - id: `month${cellId}`, - value: monthName, - style: 'rounded=0;whiteSpace=wrap;html=1;fillColor=#152019;strokeColor=#1e3028;fontColor=#7a9488;fontSize=10;align=center;', - x: leftWidth + m * monthWidth, - y: headerHeight + 10, - width: monthWidth, - height: 30 - }); - cellId++; - currentMonth.setMonth(currentMonth.getMonth() + 1); - } - - // Tickets - tickets.forEach((ticket, idx) => { - const y = headerHeight + idx * rowHeight + 50; - const due = ticket.dueDate ? new Date(ticket.dueDate) : null; - const goLive = goLiveColName && ticket[goLiveColName] ? new Date(ticket[goLiveColName]) : null; - - if (due || goLive) { - const start = due ? due : goLive; - const end = goLive ? goLive : due; - - const startMonthIdx = months.indexOf(`${start.getFullYear()}-${start.getMonth()}`); - const endMonthIdx = months.indexOf(`${end.getFullYear()}-${end.getMonth()}`); - const barWidth = Math.max(monthWidth, (endMonthIdx - startMonthIdx + 1) * monthWidth); - - const color = getStatusColor(ticket.status); - const labelParts = [ - `${ticket.key}: ${ticket.summary.substring(0, 40)}`, - `${ticket.assignee || 'N/A'} - ${ticket.status || 'N/A'}` - ]; - const label = labelParts.join('\\n'); - - cells.push({ - id: `ticket${cellId}`, - value: label, - style: `rounded=1;whiteSpace=wrap;html=1;fillColor=${color};strokeColor=#000000;strokeWidth=2;fontColor=#e8f0ec;fontSize=11;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;`, - x: leftWidth + startMonthIdx * monthWidth + 10, - y: y, - width: barWidth - 20, - height: rowHeight - 10 - }); - cellId++; + 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); + }; + + const grouped = filteredTickets.reduce((acc, t) => { + const name = t.assignee || "Non assigné"; + if (!acc[name]) acc[name] = []; + acc[name].push(t); + return acc; + }, {}); + + let html = `
`; + const todayX = getXPos(new Date()); + if (todayX > leftWidth) { + html += `
`; + html += `
Auj.
`; + } + + html += `
Assigné / Ticket
`; + timeScale.forEach(ts => { html += `
${ts.label}
`; }); + html += `
`; + + Object.keys(grouped).sort().forEach(assignee => { + html += `
${assignee}
`; + + // --- TRI CHRONOLOGIQUE PAR DUE DATE --- + const sortedTickets = grouped[assignee].sort((a, b) => { + const dateA = a.dueDate ? new Date(a.dueDate) : new Date(0); + const dateB = b.dueDate ? new Date(b.dueDate) : new Date(0); + return dateA - dateB; + }); + + sortedTickets.forEach(t => { + const dDue = t.dueDate ? new Date(t.dueDate) : null; + const dLive = t[goLiveColName] ? new Date(t[goLiveColName]) : null; + if (!dDue && !dLive) return; + + const start = dDue || dLive; + const end = dLive || dDue; + + const x1 = getXPos(start); + const x2 = getXPos(end); + const barWidth = Math.max(35, (x2 - x1) + 10); + const color = getStatusColor(t.status); + + // Préparation des infos pour le tooltip + const fmtDue = dDue ? dDue.toLocaleDateString('fr-FR') : 'Non définie'; + const fmtLive = dLive ? dLive.toLocaleDateString('fr-FR') : 'Non définie'; + const description = t.description ? `\n\nDescription: ${t.description.substring(0, 150)}${t.description.length > 150 ? '...' : ''}` : ''; + + const tooltipText = `Ticket: ${t.key}\nRésumé: ${t.summary}${description}\n\n📅 Due Date: ${fmtDue}\n🚀 Go Live: ${fmtLive}\n\n(Cliquez pour ouvrir dans Jira)`; + + html += `
+
${t.key}
+
+ ${escHtml(t.summary)} +
+ ${timeScale.map(() => `
`).join('')} +
`; + }); }); - - // Construire le XML - let xml = ` - - - - - -`; - - // Background - xml += ` - - -`; - - // Ajouter toutes les cells - cells.forEach(cell => { - const safeValue = cell.value - .replace(/&/g, '\x26amp;') - .replace(//g, '\x26gt;') - .replace(/"/g, '\x26quot;') - .replace(/'/g, '\x26apos;'); - xml += ` - - -`; - }); - - xml += ` - - - -`; - - return xml; + html += `
`; + preview.innerHTML = html; + setTimeout(() => { + const m = document.getElementById('todayMarker'); + if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center' }); + }, 150); } -function downloadRoadmap() { - if (allTickets.length === 0) { - showToast('Aucun ticket a exporter.', 'error'); - return; - } - - const xml = generateDrawioXML(filteredTickets); - - if (!xml) { - showToast('Impossible de generer la roadmap : aucune date disponible.', 'error'); - return; - } - - const blob = new Blob([xml], { type: 'text/xml;charset=utf-8' }); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = `roadmap-${document.getElementById('projectKey').value || 'export'}.xml`; - a.click(); - URL.revokeObjectURL(a.href); - showToast('Roadmap Draw.io telechargee avec succes !'); +// --- 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(); } -document.addEventListener('keydown', e => { if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();startExtraction()} }); +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 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()} }); \ No newline at end of file -- 2.52.0