/** * CSV Export Utility * Client-side CSV generation and download with Italian locale support * Uses Blob API for efficient file generation */ function csvExport() { const _t = (key) => (window.CSV_I18N && window.CSV_I18N[key]) || key; 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', _t('subtask_id'), _t('subtask_name'), _t('measured_value'), _t('unit'), _t('nominal_value'), _t('tolerance_plus'), _t('tolerance_minus'), _t('deviation'), _t('result'), _t('lot_number'), _t('serial_number'), _t('input_method'), _t('measurement_date'), _t('operator') ]; 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 = _t('task_summary_title') + '\n\n'; csv += `${_t('task_id')}${this.delimiter}${task.id}\n`; csv += `${_t('task_name')}${this.delimiter}${this.escapeCsvValue(task.name || '')}\n`; csv += `${_t('recipe')}${this.delimiter}${this.escapeCsvValue(task.recipe_name || '')}\n`; csv += `${_t('operator')}${this.delimiter}${this.escapeCsvValue(task.operator_name || '')}\n`; csv += `${_t('start_date')}${this.delimiter}${this.formatDateTime(task.started_at)}\n`; csv += `${_t('end_date')}${this.delimiter}${this.formatDateTime(task.completed_at)}\n`; csv += `${_t('status')}${this.delimiter}${task.status || ''}\n`; csv += `${_t('lot')}${this.delimiter}${this.escapeCsvValue(task.lot_number || '')}\n`; csv += `${_t('serial')}${this.delimiter}${this.escapeCsvValue(task.serial_number || '')}\n`; // Statistics csv += `\n${_t('statistics')}\n`; const stats = this.calculateStats(task.measurements); csv += `${_t('total_measurements')}${this.delimiter}${stats.total}\n`; csv += `${_t('passed')}${this.delimiter}${stats.passed}\n`; csv += `${_t('failed')}${this.delimiter}${stats.failed}\n`; csv += `${_t('pass_rate')}${this.delimiter}${this.formatNumber(stats.passRate)}%\n`; // Measurements detail csv += `\n${_t('measurement_details')}\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; } } }; }