Files
TieMeasureFlow/client/static/js/csv-export.js
T
Adriano a386986c17 feat: FASE 3 - Flusso MeasurementTec (selezione ricetta, esecuzione misure, riepilogo)
Implementazione completa del flusso operativo per il ruolo MeasurementTec:

Blueprint measure.py:
- select_recipe: selezione ricetta con ricerca e barcode
- task_list: lista task con conteggi subtask e allegati
- task_execute: esecuzione misure con numpad, calibro USB, feedback real-time
- task_complete: riepilogo con statistiche pass/fail e export CSV
- API AJAX: lookup-barcode, save-traceability, save-measurement
- Autorizzazione role_required("MeasurementTec") su tutte le route

Componenti riutilizzabili:
- numpad.html/js/css: tastierino numerico touch-friendly con keyboard support
- caliper_status.html + caliper.js: integrazione calibro USB via Web Serial API
- barcode_scanner.html + barcode.js: scansione barcode con html5-qrcode
- measurement_feedback.html: feedback visivo pass/warning/fail in tempo reale
- next_measurement.html: indicatore prossima misurazione
- annotation-viewer.js: visualizzatore canvas con marker su disegni tecnici
- csv-export.js: export CSV con locale italiano (delimitatore ;, decimale ,)

Sicurezza:
- Decoratore role_required(*roles) per autorizzazione basata su ruoli
- CSRF token su tutti i POST AJAX
- |tojson per prevenire XSS su annotations_json
- Validazione input lato client e server

i18n: 23+ nuove chiavi tradotte IT/EN per tutti i template FASE 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 08:40:58 +01:00

269 lines
7.9 KiB
JavaScript

/**
* CSV Export Utility
* Client-side CSV generation and download with Italian locale support
* Uses Blob API for efficient file generation
*/
function csvExport() {
return {
// Locale-specific settings
delimiter: ';', // Italian Excel standard
decimalSeparator: ',', // Italian number format
dateFormat: 'dd/MM/yyyy HH:mm:ss',
/**
* Export measurements to CSV file
* @param {Array} measurements - Array of measurement objects
* @param {String} filename - Optional filename (auto-generated if not provided)
*/
exportMeasurements(measurements, filename = null) {
if (!measurements || !Array.isArray(measurements) || measurements.length === 0) {
console.warn('No measurements to export');
return;
}
// Build CSV content
const csv = this.buildMeasurementCSV(measurements);
// Download file
this.downloadCSV(csv, filename || this.generateFilename('misure'));
},
/**
* Export task execution summary
* @param {Object} task - Task object with measurements
* @param {String} filename - Optional filename
*/
exportTaskSummary(task, filename = null) {
if (!task || !task.measurements || task.measurements.length === 0) {
console.warn('No task data to export');
return;
}
const csv = this.buildTaskSummaryCSV(task);
this.downloadCSV(csv, filename || this.generateFilename(`task_${task.id}`));
},
/**
* Build CSV content for measurements
*/
buildMeasurementCSV(measurements) {
const headers = [
'ID',
'Subtask ID',
'Nome Sottotask',
'Valore Misurato',
'Unità',
'Valore Nominale',
'Tolleranza +',
'Tolleranza -',
'Scarto',
'Esito',
'Numero Lotto',
'Numero Seriale',
'Metodo Input',
'Data Misurazione',
'Operatore'
];
let csv = headers.join(this.delimiter) + '\n';
for (const m of measurements) {
const row = [
m.id || '',
m.subtask_id || '',
this.escapeCsvValue(m.subtask_name || ''),
this.formatNumber(m.value),
this.escapeCsvValue(m.unit || 'mm'),
this.formatNumber(m.nominal),
this.formatNumber(m.tolerance_plus),
this.formatNumber(m.tolerance_minus),
this.formatNumber(m.deviation),
m.pass_fail || '',
this.escapeCsvValue(m.lot_number || ''),
this.escapeCsvValue(m.serial_number || ''),
m.input_method || '',
this.formatDateTime(m.measured_at),
this.escapeCsvValue(m.operator_name || '')
];
csv += row.join(this.delimiter) + '\n';
}
return csv;
},
/**
* Build CSV content for task summary
*/
buildTaskSummaryCSV(task) {
// Header section
let csv = 'RIEPILOGO ESECUZIONE TASK\n\n';
csv += `Task ID${this.delimiter}${task.id}\n`;
csv += `Nome Task${this.delimiter}${this.escapeCsvValue(task.name || '')}\n`;
csv += `Ricetta${this.delimiter}${this.escapeCsvValue(task.recipe_name || '')}\n`;
csv += `Operatore${this.delimiter}${this.escapeCsvValue(task.operator_name || '')}\n`;
csv += `Data Inizio${this.delimiter}${this.formatDateTime(task.started_at)}\n`;
csv += `Data Fine${this.delimiter}${this.formatDateTime(task.completed_at)}\n`;
csv += `Stato${this.delimiter}${task.status || ''}\n`;
csv += `Lotto${this.delimiter}${this.escapeCsvValue(task.lot_number || '')}\n`;
csv += `Seriale${this.delimiter}${this.escapeCsvValue(task.serial_number || '')}\n`;
// Statistics
csv += `\nSTATISTICHE\n`;
const stats = this.calculateStats(task.measurements);
csv += `Totale Misure${this.delimiter}${stats.total}\n`;
csv += `Passate${this.delimiter}${stats.passed}\n`;
csv += `Fallite${this.delimiter}${stats.failed}\n`;
csv += `Percentuale Successo${this.delimiter}${this.formatNumber(stats.passRate)}%\n`;
// Measurements detail
csv += `\nDETTAGLIO MISURE\n`;
csv += this.buildMeasurementCSV(task.measurements);
return csv;
},
/**
* Calculate statistics from measurements
*/
calculateStats(measurements) {
const total = measurements.length;
const passed = measurements.filter(m => (m.pass_fail || '').toLowerCase() === 'pass').length;
const failed = total - passed;
const passRate = total > 0 ? (passed / total * 100) : 0;
return { total, passed, failed, passRate };
},
/**
* Download CSV content as file
*/
downloadCSV(csvContent, filename) {
// Add UTF-8 BOM for Excel compatibility
const BOM = '\uFEFF';
const blob = new Blob([BOM + csvContent], {
type: 'text/csv;charset=utf-8;'
});
// Create download link
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
// Trigger download
document.body.appendChild(link);
link.click();
// Cleanup
document.body.removeChild(link);
URL.revokeObjectURL(url);
},
/**
* Format number with Italian decimal separator
*/
formatNumber(value) {
if (value === null || value === undefined || value === '') {
return '';
}
const num = typeof value === 'number' ? value : parseFloat(value);
if (isNaN(num)) {
return '';
}
// Convert to string and replace decimal separator
return num.toString().replace('.', this.decimalSeparator);
},
/**
* Format datetime for Italian locale
*/
formatDateTime(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
} catch (err) {
console.error('Date format error:', err);
return dateStr;
}
},
/**
* Escape CSV value (handle quotes, delimiters)
*/
escapeCsvValue(value) {
if (value === null || value === undefined) {
return '';
}
const str = String(value);
// If contains delimiter, newline, or quote, wrap in quotes
if (str.includes(this.delimiter) || str.includes('\n') || str.includes('"')) {
// Escape existing quotes by doubling them
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
},
/**
* Generate filename with timestamp
*/
generateFilename(prefix) {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
const timeStr = now.toTimeString().slice(0, 5).replace(':', ''); // HHMM
return `${prefix}_${dateStr}_${timeStr}.csv`;
}
};
}
/**
* CSV Export Button Component
* Reusable Alpine component for export functionality
*/
function csvExportButton() {
return {
exporting: false,
async exportData(data, type = 'measurements') {
this.exporting = true;
try {
const exporter = csvExport();
if (type === 'measurements') {
exporter.exportMeasurements(data);
} else if (type === 'task') {
exporter.exportTaskSummary(data);
}
// Show success feedback
this.$dispatch('export-success', { type });
} catch (err) {
console.error('Export error:', err);
this.$dispatch('export-error', { error: err.message });
} finally {
this.exporting = false;
}
}
};
}