-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtechniciens.html
More file actions
288 lines (264 loc) · 15.6 KB
/
Copy pathtechniciens.html
File metadata and controls
288 lines (264 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Techniciens — LOTO Loiret | Vestas</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--vestas-blue:#003F6B; --vestas-sky:#009FDF; --vestas-light:#E8F4FB;
--vestas-dark:#001F35; --vestas-grey:#F4F6F8; --vestas-border:#D0E4F0;
--vestas-green:#2E7D32; --vestas-lgreen:#E8F5E9;
--vestas-red:#C62828; --vestas-lred:#FFEBEE;
--text-primary:#001F35; --text-secondary:#4A6275; --white:#FFFFFF;
}
* { box-sizing:border-box; margin:0; padding:0; }
body { font-family:'Inter',sans-serif; background:var(--vestas-grey); color:var(--text-primary); }
nav { background:var(--vestas-blue); padding:0 32px; display:flex; align-items:center; height:60px; gap:32px; position:sticky; top:0; z-index:100; box-shadow:0 2px 12px rgba(0,0,0,.25); }
.nav-brand { font-family:'Rajdhani',sans-serif; font-size:20px; font-weight:700; color:#fff; letter-spacing:1px; margin-right:auto; display:flex; align-items:center; gap:10px; }
.nav-brand svg { width:28px; height:28px; fill:var(--vestas-sky); }
nav a { color:rgba(255,255,255,.75); text-decoration:none; font-size:13px; font-weight:500; letter-spacing:.5px; padding:6px 14px; border-radius:4px; transition:all .2s; }
nav a:hover { color:#fff; background:rgba(255,255,255,.1); }
nav a.active { color:var(--vestas-sky); border-bottom:2px solid var(--vestas-sky); border-radius:0; }
.page-header { background:linear-gradient(135deg,var(--vestas-blue) 0%,#005A9E 100%); padding:36px 40px 28px; color:#fff; }
.page-header h1 { font-family:'Rajdhani',sans-serif; font-size:32px; font-weight:700; letter-spacing:1px; }
.page-header p { font-size:14px; opacity:.75; margin-top:4px; }
main { padding:32px 40px; max-width:1400px; margin:0 auto; }
/* Filter bar */
.filter-bar { display:flex; gap:12px; margin-bottom:28px; flex-wrap:wrap; align-items:center; }
.filter-bar label { font-size:12px; font-weight:600; letter-spacing:.5px; text-transform:uppercase; color:var(--text-secondary); }
select, .btn-filter {
border:1.5px solid var(--vestas-border); background:var(--white);
padding:8px 14px; border-radius:6px; font-family:'Inter',sans-serif; font-size:13px;
color:var(--text-primary); cursor:pointer; transition:border-color .2s;
}
select:focus { outline:none; border-color:var(--vestas-sky); }
.btn-filter { background:var(--vestas-blue); color:#fff; border-color:var(--vestas-blue); font-weight:600; }
.btn-filter:hover { background:#005A9E; }
/* Tech cards */
.tech-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:20px; margin-bottom:32px; }
.tech-card {
background:var(--white); border-radius:8px; overflow:hidden;
box-shadow:0 1px 6px rgba(0,63,107,.08); transition:transform .2s,box-shadow .2s;
cursor:pointer;
}
.tech-card:hover { transform:translateY(-3px); box-shadow:0 6px 20px rgba(0,63,107,.14); }
.tech-card.selected { border:2px solid var(--vestas-sky); }
.tc-header { background:var(--vestas-blue); padding:14px 18px; display:flex; align-items:center; gap:12px; }
.tc-avatar { width:40px; height:40px; border-radius:50%; background:var(--vestas-sky); display:flex; align-items:center; justify-content:center; font-family:'Rajdhani',sans-serif; font-size:16px; font-weight:700; color:#fff; flex-shrink:0; }
.tc-name { color:#fff; }
.tc-initial { font-family:'Rajdhani',sans-serif; font-size:18px; font-weight:700; }
.tc-fullname { font-size:11px; opacity:.75; margin-top:1px; }
.tc-body { padding:16px 18px; }
.tc-stats { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
.tc-stat { text-align:center; }
.tc-stat-val { font-family:'Rajdhani',sans-serif; font-size:26px; font-weight:700; color:var(--vestas-blue); }
.tc-stat-lbl { font-size:10px; color:var(--text-secondary); text-transform:uppercase; letter-spacing:.5px; margin-top:2px; }
.tc-trend { margin-top:12px; }
.tc-trend-val { font-size:13px; font-weight:600; }
.tc-trend-val.up { color:var(--vestas-green); }
.tc-trend-val.down { color:var(--vestas-red); }
.tc-mini-bar { height:6px; background:var(--vestas-light); border-radius:3px; margin-top:6px; overflow:hidden; }
.tc-mini-fill { height:100%; background:var(--vestas-sky); border-radius:3px; transition:width .8s ease; }
/* Detail section */
.detail-section { background:var(--white); border-radius:8px; padding:28px; box-shadow:0 1px 6px rgba(0,63,107,.08); margin-bottom:24px; display:none; }
.detail-section.visible { display:block; }
.detail-title { font-family:'Rajdhani',sans-serif; font-size:20px; font-weight:700; color:var(--vestas-blue); margin-bottom:4px; }
.detail-sub { font-size:12px; color:var(--text-secondary); margin-bottom:24px; }
.detail-charts { display:grid; grid-template-columns:2fr 1fr; gap:24px; }
.chart-wrap { position:relative; height:260px; }
/* Big matrix table */
.matrix-wrap { overflow-x:auto; margin-top:32px; }
.matrix-table { border-collapse:collapse; width:100%; font-size:12px; }
.matrix-table th { background:var(--vestas-blue); color:#fff; padding:10px 14px; text-align:center; font-family:'Rajdhani',sans-serif; font-size:13px; font-weight:600; white-space:nowrap; }
.matrix-table th:first-child { text-align:left; min-width:100px; }
.matrix-table td { padding:9px 14px; border-bottom:1px solid var(--vestas-border); text-align:center; }
.matrix-table td:first-child { font-weight:600; color:var(--vestas-blue); text-align:left; }
.matrix-table tr:nth-child(even) td { background:var(--vestas-light); }
.matrix-table .cell-high { background:#C8E6C9 !important; color:var(--vestas-green); font-weight:700; }
.matrix-table .cell-zero { color:#ccc; }
.matrix-table tfoot td { background:var(--vestas-blue); color:#fff; font-weight:700; font-family:'Rajdhani',sans-serif; }
#loading { position:fixed; inset:0; background:var(--vestas-blue); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:999; transition:opacity .5s; }
.spinner { width:48px; height:48px; border:4px solid rgba(255,255,255,.2); border-top-color:var(--vestas-sky); border-radius:50%; animation:spin .8s linear infinite; }
#loading p { color:rgba(255,255,255,.8); margin-top:16px; font-size:14px; }
@keyframes spin { to { transform:rotate(360deg); } }
@media(max-width:768px) { main{padding:20px;} .detail-charts{grid-template-columns:1fr;} }
</style>
</head>
<body>
<div id="loading"><div class="spinner"></div><p>Chargement des données…</p></div>
<nav>
<div class="nav-brand">
<svg viewBox="0 0 24 24"><path d="M12 2L8 8H3l4 3-1.5 5L12 13l6.5 3L17 11l4-3h-5z"/></svg>
LOTO Loiret
</div>
<a href="dashboard.html">Dashboard</a>
<a href="techniciens.html" class="active">Techniciens</a>
<a href="lotos.html">Types LOTO</a>
<a href="tendances.html">Tendances</a>
</nav>
<div class="page-header">
<h1>ACTIVITÉ PAR TECHNICIEN</h1>
<p>Suivi individuel — 12 mois glissants</p>
</div>
<main>
<div class="filter-bar">
<label>Période :</label>
<select id="filter-period">
<option value="12">12 mois glissants</option>
<option value="6">6 mois glissants</option>
<option value="3">3 mois glissants</option>
<option value="all">Tout</option>
</select>
<button class="btn-filter" onclick="refreshCards()">Actualiser</button>
</div>
<div class="tech-grid" id="tech-grid"></div>
<div class="detail-section" id="detail-section">
<div class="detail-title" id="detail-title"></div>
<div class="detail-sub" id="detail-sub"></div>
<div class="detail-charts">
<div><div style="font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:12px;">LOTO PAR MOIS</div><div class="chart-wrap"><canvas id="chart-detail-monthly"></canvas></div></div>
<div><div style="font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:12px;">TOP TYPES DE LOTO</div><div class="chart-wrap"><canvas id="chart-detail-loto"></canvas></div></div>
</div>
</div>
<div style="background:var(--white);border-radius:8px;padding:28px;box-shadow:0 1px 6px rgba(0,63,107,.08);">
<div style="font-family:'Rajdhani',sans-serif;font-size:18px;font-weight:700;color:var(--vestas-blue);margin-bottom:4px;">MATRICE TECHNICIEN × MOIS</div>
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:20px;">12 mois glissants — cellules vertes ≥ 5 LOTO</div>
<div class="matrix-wrap"><table class="matrix-table" id="matrix-table"></table></div>
</div>
</main>
<script>
const DATA_URL = 'loto_data.json';
const COLORS = ['#009FDF','#003F6B','#2E7D32','#E65100','#6A1B9A','#00695C','#AD1457','#1565C0','#558B2F','#4E342E','#37474F','#F57F17'];
let ALL_ROWS = [];
let detailChart1 = null, detailChart2 = null;
function getRolling(n) {
const now = new Date(); const months = [];
for (let i = n-1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth()-i, 1);
months.push({ year: String(d.getFullYear()), month: String(d.getMonth()+1).padStart(2,'0'), label: d.toLocaleDateString('fr-FR',{month:'short',year:'numeric'}) });
}
return months;
}
function filterRows(rows, period) {
if (period === 'all') return rows;
const rolling = getRolling(parseInt(period));
return rows.filter(r => rolling.some(m => m.year == r.year && m.month === r.month));
}
function refreshCards() {
const period = document.getElementById('filter-period').value;
const rows = filterRows(ALL_ROWS, period);
const rolling = period === 'all' ? null : getRolling(parseInt(period));
renderCards(rows, rolling);
}
function renderCards(rows, rolling) {
const inits = [...new Set(ALL_ROWS.map(r=>r.initial).filter(Boolean))].sort();
const maxTotal = Math.max(...inits.map(init => rows.filter(r=>r.initial===init).length));
const now = new Date();
const thisM = String(now.getMonth()+1).padStart(2,'0'), thisY = String(now.getFullYear());
const prevM = now.getMonth()===0?'12':String(now.getMonth()).padStart(2,'0');
const prevY = now.getMonth()===0?String(now.getFullYear()-1):thisY;
document.getElementById('tech-grid').innerHTML = inits.map((init,idx) => {
const techRows = rows.filter(r=>r.initial===init);
const total = techRows.length;
const thisMo = ALL_ROWS.filter(r=>r.initial===init&&r.month===thisM&&r.year===thisY).length;
const prevMo = ALL_ROWS.filter(r=>r.initial===init&&r.month===prevM&&r.year===prevY).length;
const delta = thisMo - prevMo;
const pct = maxTotal > 0 ? (total/maxTotal*100) : 0;
const parcs = [...new Set(techRows.map(r=>r.parc).filter(Boolean))].length;
const fullname = ALL_ROWS.find(r=>r.initial===init)?.expediteur || '';
const trendClass = delta > 0 ? 'up' : delta < 0 ? 'down' : '';
const trendStr = delta > 0 ? `▲ +${delta} vs mois préc.` : delta < 0 ? `▼ ${delta} vs mois préc.` : `= stable vs mois préc.`;
const color = COLORS[idx % COLORS.length];
return `
<div class="tech-card" onclick="showDetail('${init}')" id="card-${init}">
<div class="tc-header">
<div class="tc-avatar" style="background:${color}">${init.slice(0,2)}</div>
<div class="tc-name">
<div class="tc-initial">${init}</div>
<div class="tc-fullname">${fullname}</div>
</div>
</div>
<div class="tc-body">
<div class="tc-stats">
<div class="tc-stat"><div class="tc-stat-val">${total}</div><div class="tc-stat-lbl">Total</div></div>
<div class="tc-stat"><div class="tc-stat-val">${thisMo}</div><div class="tc-stat-lbl">Ce mois</div></div>
<div class="tc-stat"><div class="tc-stat-val">${parcs}</div><div class="tc-stat-lbl">Parcs</div></div>
</div>
<div class="tc-trend">
<div class="tc-trend-val ${trendClass}">${trendStr}</div>
<div class="tc-mini-bar"><div class="tc-mini-fill" style="width:${pct}%;background:${color}"></div></div>
</div>
</div>
</div>`;
}).join('');
}
function showDetail(init) {
document.querySelectorAll('.tech-card').forEach(c => c.classList.remove('selected'));
document.getElementById('card-'+init)?.classList.add('selected');
const rolling = getRolling(12);
const techRows = ALL_ROWS.filter(r => r.initial === init);
const fullname = techRows[0]?.expediteur || init;
document.getElementById('detail-title').textContent = init + ' — ' + fullname;
document.getElementById('detail-sub').textContent = techRows.length + ' LOTO au total · Détail 12 mois glissants';
document.getElementById('detail-section').classList.add('visible');
document.getElementById('detail-section').scrollIntoView({behavior:'smooth',block:'nearest'});
const monthCounts = rolling.map(m => techRows.filter(r=>r.month===m.month&&r.year==m.year).length);
if (detailChart1) detailChart1.destroy();
detailChart1 = new Chart(document.getElementById('chart-detail-monthly'), {
type: 'bar',
data: { labels: rolling.map(m=>m.label), datasets: [{ data: monthCounts, backgroundColor: 'rgba(0,159,223,0.6)', borderColor: '#009FDF', borderWidth: 1.5, borderRadius: 4 }] },
options: { responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{ y:{beginAtZero:true,grid:{color:'rgba(0,63,107,.08)'}}, x:{grid:{display:false},ticks:{font:{size:10}}} } }
});
const lotoMap = {};
techRows.forEach(r => { if(r.loto) lotoMap[r.loto] = (lotoMap[r.loto]||0)+1; });
const top5 = Object.entries(lotoMap).sort((a,b)=>b[1]-a[1]).slice(0,5);
if (detailChart2) detailChart2.destroy();
detailChart2 = new Chart(document.getElementById('chart-detail-loto'), {
type: 'doughnut',
data: { labels: top5.map(([l])=>l.length>25?l.slice(0,23)+'…':l), datasets: [{ data: top5.map(e=>e[1]), backgroundColor: COLORS, borderWidth: 2, borderColor: '#fff' }] },
options: { responsive:true, maintainAspectRatio:false, plugins:{ legend:{ position:'bottom', labels:{font:{size:10},boxWidth:12,padding:8} } } }
});
}
function renderMatrix(rows) {
const rolling = getRolling(12);
const inits = [...new Set(ALL_ROWS.map(r=>r.initial).filter(Boolean))].sort();
let html = '<thead><tr><th>Technicien</th><th>Total</th>';
rolling.forEach(m => html += `<th>${m.label}</th>`);
html += '</tr></thead><tbody>';
const colTotals = rolling.map(() => 0);
inits.forEach((init,idx) => {
const bg = idx%2===0?'':'';
html += `<tr><td>${init}</td>`;
const totT = rows.filter(r=>r.initial===init).length;
html += `<td style="font-weight:700;color:var(--vestas-sky)">${totT}</td>`;
rolling.forEach((m,mi) => {
const cnt = rows.filter(r=>r.initial===init&&r.month===m.month&&r.year==m.year).length;
colTotals[mi] += cnt;
html += cnt >= 5 ? `<td class="cell-high">${cnt}</td>` : cnt > 0 ? `<td>${cnt}</td>` : `<td class="cell-zero">·</td>`;
});
html += '</tr>';
});
html += '</tbody><tfoot><tr><td>TOTAL</td><td>' + rows.length + '</td>';
colTotals.forEach(v => html += `<td>${v}</td>`);
html += '</tr></tfoot>';
document.getElementById('matrix-table').innerHTML = html;
}
async function main() {
const res = await fetch(DATA_URL + '?t=' + Date.now()).catch(()=>null);
document.getElementById('loading').style.opacity = '0';
setTimeout(()=>document.getElementById('loading').style.display='none',500);
if (!res || !res.ok) return;
const data = await res.json();
ALL_ROWS = data.rows || [];
const rolling = getRolling(12);
const rollingRows = ALL_ROWS.filter(r => rolling.some(m=>m.year==r.year&&m.month===r.month));
renderCards(rollingRows, rolling);
renderMatrix(rollingRows);
}
main();
</script>
</body>
</html>