hlsv 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +676 -0
- data/README.md +356 -0
- data/bin/hlsv +4 -0
- data/config.default.yaml +19 -0
- data/lib/hlsv/cli.rb +85 -0
- data/lib/hlsv/find_keys.rb +979 -0
- data/lib/hlsv/html2word.rb +602 -0
- data/lib/hlsv/mon_script.rb +169 -0
- data/lib/hlsv/version.rb +5 -0
- data/lib/hlsv/web_app.rb +569 -0
- data/lib/hlsv/xpt/dataset.rb +38 -0
- data/lib/hlsv/xpt/library.rb +28 -0
- data/lib/hlsv/xpt/reader.rb +367 -0
- data/lib/hlsv/xpt/variable.rb +130 -0
- data/lib/hlsv/xpt.rb +11 -0
- data/lib/hlsv.rb +49 -0
- data/public/Contact-LOGO.png +0 -0
- data/public/app.js +569 -0
- data/public/styles.css +586 -0
- data/public/styles_csv.css +448 -0
- data/views/csv_view.erb +85 -0
- data/views/index.erb +233 -0
- data/views/report_template.erb +1144 -0
- metadata +176 -0
data/public/app.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 AdClin
|
|
3
|
+
Licensed under the GNU Affero General Public License v3.0 or later.
|
|
4
|
+
See the LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Configuration Management
|
|
8
|
+
|
|
9
|
+
// Save configuration
|
|
10
|
+
async function sauvegarderConfig() {
|
|
11
|
+
// Validate required fields
|
|
12
|
+
const requiredFields = [
|
|
13
|
+
'study_name', 'output_directory', 'data_directory',
|
|
14
|
+
'define_path', 'event_key', 'intervention_key', 'finding_key',
|
|
15
|
+
'finding_about_key', 'ds_key', 'relrec_key', 'CO_key',
|
|
16
|
+
'TA_key', 'TE_key', 'TI_key', 'TS_key', 'TV_key'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const missingFields = [];
|
|
20
|
+
for (const field of requiredFields) {
|
|
21
|
+
const element = document.getElementById(field);
|
|
22
|
+
if (!element.value.trim()) {
|
|
23
|
+
missingFields.push(field.replace(/_/g, ' ').toUpperCase());
|
|
24
|
+
element.classList.add('error-field');
|
|
25
|
+
} else {
|
|
26
|
+
element.classList.remove('error-field');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (missingFields.length > 0) {
|
|
31
|
+
showMessage('message-config', 'error',
|
|
32
|
+
`⚠️ Missing required fields: ${missingFields.join(', ')}`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = {
|
|
37
|
+
study_name: document.getElementById('study_name').value.trim(),
|
|
38
|
+
output_type: 'csv',
|
|
39
|
+
output_directory: document.getElementById('output_directory').value.trim(),
|
|
40
|
+
data_directory: document.getElementById('data_directory').value.trim(),
|
|
41
|
+
define_path: document.getElementById('define_path').value.trim(),
|
|
42
|
+
excluded_ds: document.getElementById('excluded_ds').value.trim(),
|
|
43
|
+
event_key: document.getElementById('event_key').value.trim(),
|
|
44
|
+
intervention_key: document.getElementById('intervention_key').value.trim(),
|
|
45
|
+
finding_key: document.getElementById('finding_key').value.trim(),
|
|
46
|
+
finding_about_key: document.getElementById('finding_about_key').value.trim(),
|
|
47
|
+
ds_key: document.getElementById('ds_key').value.trim(),
|
|
48
|
+
relrec_key: document.getElementById('relrec_key').value.trim(),
|
|
49
|
+
CO_key: document.getElementById('CO_key').value.trim(),
|
|
50
|
+
TA_key: document.getElementById('TA_key').value.trim(),
|
|
51
|
+
TE_key: document.getElementById('TE_key').value.trim(),
|
|
52
|
+
TI_key: document.getElementById('TI_key').value.trim(),
|
|
53
|
+
TS_key: document.getElementById('TS_key').value.trim(),
|
|
54
|
+
TV_key: document.getElementById('TV_key').value.trim()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
showMessage('message-config', 'processing', '⏳ Saving configuration...');
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch('/config', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(config)
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (data.success) {
|
|
69
|
+
showMessage('message-config', 'success', '✅ ' + data.message);
|
|
70
|
+
return true;
|
|
71
|
+
} else {
|
|
72
|
+
showMessage('message-config', 'error', '❌ ' + data.erreur);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
showMessage('message-config', 'error', '❌ Connection error: ' + error.message);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Display full configuration
|
|
82
|
+
async function afficherConfigComplete() {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch('/config');
|
|
85
|
+
const config = await response.json();
|
|
86
|
+
|
|
87
|
+
const configWindow = window.open('', 'Configuration', 'width=700,height=500');
|
|
88
|
+
configWindow.document.write(`
|
|
89
|
+
<!DOCTYPE html>
|
|
90
|
+
<html>
|
|
91
|
+
<head>
|
|
92
|
+
<title>Current Configuration</title>
|
|
93
|
+
<style>
|
|
94
|
+
body {
|
|
95
|
+
font-family: 'Courier New', monospace;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
background: #f5f5f5;
|
|
98
|
+
}
|
|
99
|
+
pre {
|
|
100
|
+
background: white;
|
|
101
|
+
padding: 20px;
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
104
|
+
overflow: auto;
|
|
105
|
+
}
|
|
106
|
+
h1 { color: #333; }
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<h1>Current Configuration</h1>
|
|
111
|
+
<pre>${JSON.stringify(config, null, 2)}</pre>
|
|
112
|
+
</body>
|
|
113
|
+
</html>
|
|
114
|
+
`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
showMessage('message-config', 'error', '❌ Error loading configuration: ' + error.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clear all fields
|
|
121
|
+
async function effacerChamps() {
|
|
122
|
+
if (!confirm('⚠️ Are you sure you want to clear the current configuration (config.yaml)?')) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
showMessage('message-config', 'processing', '⏳ Clearing configuration...');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch('/config/clear', {
|
|
130
|
+
method: 'POST'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
|
|
135
|
+
if (data.success) {
|
|
136
|
+
showMessage('message-config', 'success', '✅ ' + data.message);
|
|
137
|
+
setTimeout(() => location.reload(true), 1000);
|
|
138
|
+
} else {
|
|
139
|
+
showMessage('message-config', 'error', '❌ ' + data.erreur);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
showMessage('message-config', 'error', '❌ Error: ' + error.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Load default configuration
|
|
147
|
+
async function chargerDefaut() {
|
|
148
|
+
if (!confirm('⚠️ Load default configuration? This will replace config.yaml with config.default.yaml')) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
showMessage('message-config', 'processing', '⏳ Loading config.default.yaml...');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetch('/config/reset', {
|
|
156
|
+
method: 'POST'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
|
|
161
|
+
if (data.success) {
|
|
162
|
+
showMessage('message-config', 'success', '✅ ' + data.message);
|
|
163
|
+
setTimeout(() => location.reload(true), 1000);
|
|
164
|
+
} else {
|
|
165
|
+
showMessage('message-config', 'error', '❌ ' + data.erreur);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
showMessage('message-config', 'error', '❌ Error: ' + error.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Processing
|
|
173
|
+
|
|
174
|
+
// Start processing
|
|
175
|
+
async function lancerTraitement() {
|
|
176
|
+
const statusDiv = document.getElementById('status-traitement');
|
|
177
|
+
const btn = document.getElementById('btn-traiter');
|
|
178
|
+
|
|
179
|
+
btn.disabled = true;
|
|
180
|
+
statusDiv.innerHTML = '<div class="status processing">⏳ Processing in progress...</div>';
|
|
181
|
+
|
|
182
|
+
// Sauvegarder la configuration avant de lancer le traitement
|
|
183
|
+
const configSaved = await sauvegarderConfig();
|
|
184
|
+
|
|
185
|
+
if (!configSaved) {
|
|
186
|
+
statusDiv.innerHTML = `
|
|
187
|
+
<div class="status error">
|
|
188
|
+
<h3>❌ Configuration Error</h3>
|
|
189
|
+
<p>The configuration could not be saved. Please correct the errors before starting the analysis.</p>
|
|
190
|
+
<p style="margin-top: 10px; color: #666; font-size: 0.9em;">
|
|
191
|
+
Check the error message in the Configuration section above.
|
|
192
|
+
</p>
|
|
193
|
+
</div>
|
|
194
|
+
`;
|
|
195
|
+
btn.disabled = false;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch('/traiter', {
|
|
201
|
+
method: 'POST'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const data = await response.json();
|
|
205
|
+
|
|
206
|
+
if (data.success) {
|
|
207
|
+
let messageSucces = '<h3>✅ Analysis completed successfully!</h3>';
|
|
208
|
+
|
|
209
|
+
// Display completed steps
|
|
210
|
+
if (data.resultat.etapes) {
|
|
211
|
+
messageSucces += '<div style="margin: 20px 0; text-align: left;">';
|
|
212
|
+
messageSucces += '<h4>📋 Completed Steps:</h4>';
|
|
213
|
+
messageSucces += '<ol>';
|
|
214
|
+
|
|
215
|
+
data.resultat.etapes.forEach(etape => {
|
|
216
|
+
let icone = '✅';
|
|
217
|
+
let couleur = '#28a745';
|
|
218
|
+
|
|
219
|
+
if (etape.statut === 'en_cours') {
|
|
220
|
+
icone = '⏳';
|
|
221
|
+
couleur = '#ffc107';
|
|
222
|
+
} else if (etape.statut === 'erreur') {
|
|
223
|
+
icone = '❌';
|
|
224
|
+
couleur = '#dc3545';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
messageSucces += `
|
|
228
|
+
<li style="color: ${couleur};">
|
|
229
|
+
${icone} <strong>${etape.nom}:</strong>
|
|
230
|
+
${etape.message ? `<span style="color: #666; font-size: 0.9em;">${etape.message}</span>` : ''}
|
|
231
|
+
</li>
|
|
232
|
+
`;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
messageSucces += '</ol></div>';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Display generated files
|
|
239
|
+
if (data.resultat.rapport_path || data.resultat.rapport_name || data.resultat.output_directory) {
|
|
240
|
+
messageSucces += '<div style="margin: 20px 0; text-align: left;">';
|
|
241
|
+
messageSucces += '<h4>📄 Output Files:</h4>';
|
|
242
|
+
messageSucces += '<ul style="margin: 0; padding-left: 20px;">';
|
|
243
|
+
|
|
244
|
+
if (data.resultat.rapport_path && data.resultat.rapport_name) {
|
|
245
|
+
messageSucces += `
|
|
246
|
+
<li>
|
|
247
|
+
<strong>Report:</strong>
|
|
248
|
+
<a href="${data.resultat.rapport_path}" target="_blank" rel="noopener noreferrer">
|
|
249
|
+
${data.resultat.rapport_name}
|
|
250
|
+
</a>
|
|
251
|
+
</li>
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
if (data.resultat.output_directory) {
|
|
255
|
+
messageSucces += `<li><strong>Output Directory:</strong> ${data.resultat.output_directory}</li>`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
messageSucces += '</ul></div>';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Display statistics
|
|
262
|
+
if (data.resultat.nombre_datasets_analyses || data.resultat.nombre_datasets_invalid) {
|
|
263
|
+
messageSucces += '<div style="margin: 20px 0; text-align: left;">';
|
|
264
|
+
messageSucces += '<h4>📊 Dataset Statistics:</h4>';
|
|
265
|
+
messageSucces += '<ul style="margin: 0; padding-left: 20px;">';
|
|
266
|
+
|
|
267
|
+
if (data.resultat.nombre_datasets_analyses) {
|
|
268
|
+
messageSucces += `<li><strong>Analyzed:</strong> ${data.resultat.nombre_datasets_analyses}</li>`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const invalid_ds = data.resultat.nombre_datasets_invalid;
|
|
272
|
+
if (invalid_ds) {
|
|
273
|
+
if (invalid_ds.invalid_dataset) {
|
|
274
|
+
messageSucces += `<li><strong>With at least one issue:</strong> ${invalid_ds.invalid_dataset}</li>`;
|
|
275
|
+
}
|
|
276
|
+
if (invalid_ds.invalid_ascii) {
|
|
277
|
+
messageSucces += `<li><strong>ASCII issue:</strong> ${invalid_ds.invalid_ascii}</li>`;
|
|
278
|
+
}
|
|
279
|
+
if (invalid_ds.invalid_define) {
|
|
280
|
+
messageSucces += `<li><strong>Invalid keys in define.xml:</strong> ${invalid_ds.invalid_define}</li>`;
|
|
281
|
+
}
|
|
282
|
+
if (invalid_ds.invalid_data) {
|
|
283
|
+
messageSucces += `<li><strong>Minimum keys not found:</strong> ${invalid_ds.invalid_data}</li>`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
messageSucces += '</ul></div>';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
statusDiv.innerHTML = `<div class="status">${messageSucces}</div>`;
|
|
291
|
+
|
|
292
|
+
// Auto-refresh results if already loaded
|
|
293
|
+
const listeDiv = document.getElementById('liste-resultats');
|
|
294
|
+
if (listeDiv.style.display !== 'none') {
|
|
295
|
+
rafraichirResultats();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
} else {
|
|
299
|
+
// Display validation errors
|
|
300
|
+
let messageErreur = `<h3>❌ ${data.erreur}</h3>`;
|
|
301
|
+
|
|
302
|
+
if (data.details && data.details.length > 0) {
|
|
303
|
+
messageErreur += '<div style="margin: 20px 0; text-align: left;">';
|
|
304
|
+
messageErreur += '<h4>Validation Errors:</h4>';
|
|
305
|
+
messageErreur += '<ul style="margin-left: 20px; color: #721c24;">';
|
|
306
|
+
data.details.forEach(erreur => {
|
|
307
|
+
messageErreur += `<li style="margin: 5px 0;">${erreur}</li>`;
|
|
308
|
+
});
|
|
309
|
+
messageErreur += '</ul></div>';
|
|
310
|
+
messageErreur += '<p style="margin-top: 15px;">⚠️ Please correct these errors before restarting the analysis</p>';
|
|
311
|
+
} else {
|
|
312
|
+
messageErreur += `<p style="margin-top: 15px;">${data.erreur}</p>`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
statusDiv.innerHTML = `<div class="status error">${messageErreur}</div>`;
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
statusDiv.innerHTML = `
|
|
319
|
+
<div class="status error">
|
|
320
|
+
<h3>❌ Connection Error</h3>
|
|
321
|
+
<p>${error.message}</p>
|
|
322
|
+
<p style="margin-top: 10px; color: #666; font-size: 0.9em;">
|
|
323
|
+
Please check that the server is running correctly (ruby app.rb)
|
|
324
|
+
</p>
|
|
325
|
+
</div>
|
|
326
|
+
`;
|
|
327
|
+
} finally {
|
|
328
|
+
btn.disabled = false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Results Management
|
|
333
|
+
|
|
334
|
+
// Load results for the first time
|
|
335
|
+
async function chargerResultats() {
|
|
336
|
+
const btnLoad = document.getElementById('btn-load-results');
|
|
337
|
+
const btnRefresh = document.getElementById('btn-refresh-results');
|
|
338
|
+
const listeDiv = document.getElementById('liste-resultats');
|
|
339
|
+
|
|
340
|
+
// Hide Load button and show Refresh button
|
|
341
|
+
btnLoad.style.display = 'none';
|
|
342
|
+
btnRefresh.style.display = 'inline-block';
|
|
343
|
+
listeDiv.style.display = 'block';
|
|
344
|
+
|
|
345
|
+
await rafraichirResultats();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Refresh results list
|
|
349
|
+
let isRefreshing = false;
|
|
350
|
+
async function rafraichirResultats() {
|
|
351
|
+
if (isRefreshing) return;
|
|
352
|
+
isRefreshing = true;
|
|
353
|
+
|
|
354
|
+
const listeDiv = document.getElementById('liste-resultats');
|
|
355
|
+
listeDiv.innerHTML = '<p style="color: #666;">⏳ Loading files...</p>';
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const response = await fetch('/resultats');
|
|
359
|
+
const data = await response.json();
|
|
360
|
+
|
|
361
|
+
if (!data.success) {
|
|
362
|
+
listeDiv.innerHTML = '<p style="color: #dc3545;">❌ Error loading results</p>';
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const arbo = data.arborescence;
|
|
367
|
+
|
|
368
|
+
// Check if there are files or folders
|
|
369
|
+
const hasFiles = arbo.fichiers && arbo.fichiers.length > 0;
|
|
370
|
+
const hasFolders = arbo.dossiers && Object.keys(arbo.dossiers).length > 0;
|
|
371
|
+
|
|
372
|
+
if (hasFiles || hasFolders) {
|
|
373
|
+
let html = '<div class="file-tree">';
|
|
374
|
+
html += construireArbre(arbo, '', true);
|
|
375
|
+
html += '</div>';
|
|
376
|
+
|
|
377
|
+
listeDiv.innerHTML = html;
|
|
378
|
+
} else {
|
|
379
|
+
listeDiv.innerHTML = '<p style="color: #666;">No result files yet. Run an analysis to generate files.</p>';
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Error:', error);
|
|
383
|
+
listeDiv.innerHTML = '<p style="color: #dc3545;">❌ Error loading files: ' + error.message + '</p>';
|
|
384
|
+
} finally {
|
|
385
|
+
isRefreshing = false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Build HTML tree recursively
|
|
390
|
+
function construireArbre(noeud, chemin_parent, isRoot = false) {
|
|
391
|
+
let html = '';
|
|
392
|
+
|
|
393
|
+
// Display folders
|
|
394
|
+
if (noeud.dossiers) {
|
|
395
|
+
const dossiers = Object.keys(noeud.dossiers).sort();
|
|
396
|
+
dossiers.forEach(nomDossier => {
|
|
397
|
+
const cheminDossier = chemin_parent ? `${chemin_parent}/${nomDossier}` : nomDossier;
|
|
398
|
+
const sousDossier = noeud.dossiers[nomDossier];
|
|
399
|
+
const id = 'folder-' + cheminDossier.replace(/[\/\s]/g, '-');
|
|
400
|
+
|
|
401
|
+
const nbFichiers = sousDossier.fichiers ? sousDossier.fichiers.length : 0;
|
|
402
|
+
const nbDossiers = sousDossier.dossiers ? Object.keys(sousDossier.dossiers).length : 0;
|
|
403
|
+
|
|
404
|
+
// Check if folder contains CSV files
|
|
405
|
+
const hasCsvFiles = sousDossier.fichiers && sousDossier.fichiers.some(f => f.extension === '.csv');
|
|
406
|
+
|
|
407
|
+
// Check if this is a first-level folder (isRoot is true and no parent path)
|
|
408
|
+
const isFirstLevel = isRoot && !chemin_parent;
|
|
409
|
+
|
|
410
|
+
html += `
|
|
411
|
+
<div class="folder-item">
|
|
412
|
+
<div class="folder-header" onclick="toggleFolder('${id}')" role="button" tabindex="0" aria-expanded="false" aria-controls="${id}">
|
|
413
|
+
<span class="folder-icon" id="${id}-icon" aria-hidden="true">📁</span>
|
|
414
|
+
<strong>${escapeHtml(nomDossier)}</strong>
|
|
415
|
+
<span style="color: #666; font-size: 0.9em; margin-left: 10px;">
|
|
416
|
+
(${nbFichiers} file${nbFichiers !== 1 ? 's' : ''}, ${nbDossiers} folder${nbDossiers !== 1 ? 's' : ''})
|
|
417
|
+
</span>
|
|
418
|
+
<div class="folder-actions">
|
|
419
|
+
${isFirstLevel ? `
|
|
420
|
+
<button onclick="event.stopPropagation(); telechargerZipDossier('${escapeHtml(cheminDossier)}')"
|
|
421
|
+
class="btn-primary"
|
|
422
|
+
aria-label="Download folder as ZIP">
|
|
423
|
+
📦 Download as ZIP
|
|
424
|
+
</button>
|
|
425
|
+
` : ''}
|
|
426
|
+
${hasCsvFiles ? `
|
|
427
|
+
<button onclick="event.stopPropagation(); telechargerDossierExcel('${escapeHtml(cheminDossier)}')"
|
|
428
|
+
class="btn-primary"
|
|
429
|
+
aria-label="Download all CSV files as Excel">
|
|
430
|
+
📊 Download as Excel
|
|
431
|
+
</button>
|
|
432
|
+
` : ''}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="folder-content" id="${id}" style="display: none; margin-left: 20px;" role="region">
|
|
436
|
+
${construireArbre(sousDossier, cheminDossier, false)}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Display files
|
|
444
|
+
if (noeud.fichiers && noeud.fichiers.length > 0) {
|
|
445
|
+
noeud.fichiers.forEach(fichier => {
|
|
446
|
+
const icone = getIconePourExtension(fichier.extension);
|
|
447
|
+
const tailleMo = (fichier.taille / 1024).toFixed(2);
|
|
448
|
+
|
|
449
|
+
html += `
|
|
450
|
+
<div class="file-item">
|
|
451
|
+
<div class="file-info">
|
|
452
|
+
<div class="file-name">${icone} ${escapeHtml(fichier.nom)}</div>
|
|
453
|
+
<div class="file-meta">
|
|
454
|
+
${tailleMo} KB • ${fichier.date}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div>
|
|
458
|
+
${fichier.extension === '.html' ?
|
|
459
|
+
`<button onclick="ouvrirFichier('${escapeHtml(fichier.chemin)}')" class="btn-secondary" style="margin-right: 5px;" aria-label="Open ${escapeHtml(fichier.nom)}">👁️ Open</button>
|
|
460
|
+
<button onclick="telechargerHtmlAsWord('${escapeHtml(fichier.chemin)}')" class="btn-secondary" style="margin-right: 5px;" aria-label="Download as Word">📄 Word</button>` :
|
|
461
|
+
''}
|
|
462
|
+
<button onclick="telecharger('${escapeHtml(fichier.chemin)}')" class="btn-primary" aria-label="Download ${escapeHtml(fichier.nom)}">⬇️ Download</button>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
`;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return html;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Toggle folder display
|
|
473
|
+
function toggleFolder(id) {
|
|
474
|
+
const folder = document.getElementById(id);
|
|
475
|
+
const icon = document.getElementById(id + '-icon');
|
|
476
|
+
const header = folder.previousElementSibling;
|
|
477
|
+
|
|
478
|
+
if (folder && icon) {
|
|
479
|
+
const isExpanded = folder.style.display !== 'none';
|
|
480
|
+
folder.style.display = isExpanded ? 'none' : 'block';
|
|
481
|
+
icon.textContent = isExpanded ? '📁' : '📂';
|
|
482
|
+
if (header) {
|
|
483
|
+
header.setAttribute('aria-expanded', !isExpanded);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Get icon based on extension
|
|
489
|
+
function getIconePourExtension(ext) {
|
|
490
|
+
const icons = {
|
|
491
|
+
'.html': '🌐',
|
|
492
|
+
'.csv': '📊',
|
|
493
|
+
'.xlsx': '📗',
|
|
494
|
+
'.xls': '📗',
|
|
495
|
+
'.pdf': '📕',
|
|
496
|
+
'.txt': '📄',
|
|
497
|
+
'.md': '📄',
|
|
498
|
+
'.yaml': '⚙️',
|
|
499
|
+
'.yml': '⚙️'
|
|
500
|
+
};
|
|
501
|
+
return icons[ext.toLowerCase()] || '📄';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Download a file
|
|
505
|
+
function telecharger(chemin) {
|
|
506
|
+
window.location.href = '/telecharger/' + encodeURIComponent(chemin);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Open HTML file in new tab
|
|
510
|
+
function ouvrirFichier(chemin) {
|
|
511
|
+
window.open('/telecharger/' + encodeURIComponent(chemin), '_blank', 'noopener,noreferrer');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Utility Functions
|
|
515
|
+
|
|
516
|
+
// Show message with auto-dismiss
|
|
517
|
+
function showMessage(elementId, type, message, duration = 3000) {
|
|
518
|
+
const msgDiv = document.getElementById(elementId);
|
|
519
|
+
msgDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
|
520
|
+
|
|
521
|
+
if (duration > 0) {
|
|
522
|
+
setTimeout(() => msgDiv.innerHTML = '', duration);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Escape HTML to prevent XSS
|
|
527
|
+
function escapeHtml(text) {
|
|
528
|
+
const div = document.createElement('div');
|
|
529
|
+
div.textContent = text;
|
|
530
|
+
return div.innerHTML;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Keyboard accessibility for folder toggles
|
|
534
|
+
document.addEventListener('keydown', (e) => {
|
|
535
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
536
|
+
if (e.target.classList.contains('folder-header')) {
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
e.target.click();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Download folder as ZIP
|
|
544
|
+
function telechargerZipDossier(cheminDossier) {
|
|
545
|
+
window.location.href = '/telecharger_zip_dossier/' + encodeURIComponent(cheminDossier);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Download all CSV files in a folder as Excel
|
|
549
|
+
function telechargerDossierExcel(cheminDossier) {
|
|
550
|
+
window.location.href = '/telecharger_dossier_excel/' + encodeURIComponent(cheminDossier);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Download HTML file as Word document
|
|
554
|
+
async function telechargerHtmlAsWord(chemin) {
|
|
555
|
+
const response = await fetch('/telecharger_html_word/' + encodeURIComponent(chemin));
|
|
556
|
+
const data = await response.json();
|
|
557
|
+
if (data.success) {
|
|
558
|
+
const cheminDocx = chemin.replace(/\.html$/, '.docx');
|
|
559
|
+
window.location.href = '/telecharger/' + encodeURIComponent(cheminDocx);
|
|
560
|
+
rafraichirResultats();
|
|
561
|
+
} else {
|
|
562
|
+
alert('❌ ' + data.erreur);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Download HTML file as PDF
|
|
567
|
+
function telechargerHtmlAsPdf(chemin) {
|
|
568
|
+
window.location.href = '/telecharger_html_pdf/' + encodeURIComponent(chemin);
|
|
569
|
+
}
|