-
-
-
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 = `
`;
+ 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 += ``;
+
+ Object.keys(grouped).sort().forEach(assignee => {
+ html += ``;
+
+ // --- 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