import * as XLSX from 'xlsx'; /** * Utilitaires centralisés pour le traitement des fichiers */ export interface ParsedFileData { data: any[]; headers: string[]; error?: string; } export function parseCSV(file: File): Promise { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target?.result as string; const lines = text.split('\n').filter(line => line.trim()); if (lines.length < 2) { resolve({ data: [], headers: [], error: 'Le fichier doit contenir au moins un en-tête et une ligne de données.' }); return; } // Trouver la ligne d'en-têtes (ignorer les lignes de titre et vides) let headerLineIndex = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Si la ligne contient des virgules et ressemble à des en-têtes if (line.includes(',') && !line.toLowerCase().includes('modèle') && !line.toLowerCase().includes('liste')) { headerLineIndex = i; break; } } const headers = lines[headerLineIndex].split(',').map(h => h.trim().replace(/"/g, '')); const dataLines = lines.slice(headerLineIndex + 1); const data = dataLines .filter(line => line.trim()) // Ignorer les lignes vides .map(line => { const values = line.split(',').map(v => v.trim().replace(/"/g, '')); const row: any = {}; headers.forEach((header, index) => { row[header] = values[index] || ''; }); return row; }) .filter(row => { // Ignorer les lignes où tous les champs sont vides return Object.values(row).some(value => value && value.toString().trim()); }); resolve({ data, headers }); } catch (error) { resolve({ data: [], headers: [], error: 'Erreur lors de la lecture du fichier CSV.' }); } }; reader.readAsText(file); }); } export function parseExcel(file: File): Promise { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { try { const fileData = new Uint8Array(e.target?.result as ArrayBuffer); const workbook = XLSX.read(fileData, { type: 'array' }); // Prendre la première feuille const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; // Convertir en JSON const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); if (jsonData.length < 2) { resolve({ data: [], headers: [], error: 'Le fichier doit contenir au moins un en-tête et une ligne de données.' }); return; } // Trouver la ligne d'en-têtes (ignorer les lignes de titre et vides) let headerLineIndex = 0; for (let i = 0; i < jsonData.length; i++) { const row = jsonData[i] as any[]; if (row && row.length > 0) { const firstCell = row[0]; // Si la première cellule ressemble à un en-tête et pas à un titre if (firstCell && typeof firstCell === 'string' && !firstCell.toLowerCase().includes('modèle') && !firstCell.toLowerCase().includes('liste') && !firstCell.toLowerCase().includes('propositions')) { headerLineIndex = i; break; } } } const headers = (jsonData[headerLineIndex] as string[]).filter(h => h && h.toString().trim()); const rows = jsonData.slice(headerLineIndex + 1) as any[][]; const parsedData = rows .filter(row => row && row.length > 0) // Ignorer les lignes vides .map(row => { const rowData: any = {}; headers.forEach((header, index) => { rowData[header] = row[index] || ''; }); return rowData; }) .filter(rowData => { // Ignorer les lignes où tous les champs sont vides return Object.values(rowData).some(value => value && value.toString().trim()); }); resolve({ data: parsedData, headers }); } catch (error) { resolve({ data: [], headers: [], error: 'Erreur lors de la lecture du fichier Excel.' }); } }; reader.readAsArrayBuffer(file); }); } export function getExpectedColumns(type: 'propositions' | 'participants'): string[] { if (type === 'propositions') { return ['Titre', 'Description', 'Prénom', 'Nom', 'Email']; } else { return ['Prénom', 'Nom', 'Email']; } } /** * Normalise les noms de colonnes pour améliorer la compatibilité */ export function normalizeColumnName(columnName: string): string { if (!columnName) return ''; const normalized = columnName.toLowerCase().trim(); // Mappings pour les colonnes communes const mappings: { [key: string]: string } = { 'email': 'Email', 'e-mail': 'Email', 'mail': 'Email', 'courriel': 'Email', 'prénom': 'Prénom', 'prenom': 'Prénom', 'firstname': 'Prénom', 'first_name': 'Prénom', 'nom': 'Nom', 'lastname': 'Nom', 'last_name': 'Nom', 'titre': 'Titre', 'title': 'Titre', 'description': 'Description', 'desc': 'Description' }; return mappings[normalized] || columnName; } /** * Normalise les données parsées pour correspondre aux colonnes attendues */ export function normalizeParsedData(data: any[], type: 'propositions' | 'participants'): any[] { const expectedColumns = getExpectedColumns(type); return data.map(row => { const normalizedRow: any = {}; // Normaliser chaque colonne Object.keys(row).forEach(key => { const normalizedKey = normalizeColumnName(key); normalizedRow[normalizedKey] = row[key]; }); return normalizedRow; }); } export async function downloadTemplate(type: 'propositions' | 'participants'): Promise { const columns = getExpectedColumns(type); // Importer dynamiquement la fonction pour éviter les dépendances circulaires const { getExportFileFormat, generateExportFile, downloadExportFile } = await import('./export-utils'); const format = await getExportFileFormat(); // Créer la matrice de données avec les en-têtes const matrix: string[][] = []; // Pour les formats Excel/ODS, ajouter un titre if (format !== 'csv') { matrix.push([`Modèle d'import - ${type === 'propositions' ? 'Propositions' : 'Participants'}`]); matrix.push([]); // Ligne vide } matrix.push(columns); // En-têtes des colonnes // Ajouter quelques lignes d'exemple if (type === 'propositions') { matrix.push(['Exemple de proposition', 'Description de la proposition', 'Jean', 'Dupont', 'jean.dupont@example.com']); matrix.push(['Autre proposition', 'Autre description', 'Marie', 'Martin', 'marie.martin@example.com']); } else { matrix.push(['Jean', 'Dupont', 'jean.dupont@example.com']); matrix.push(['Marie', 'Martin', 'marie.martin@example.com']); } // Générer le fichier dans le format configuré const data = generateExportFile(matrix, format); // Créer le nom de fichier avec l'extension appropriée const filename = `template_${type}.${format}`; // Télécharger le fichier downloadExportFile(data, filename, format); } export function validateFileType(file: File): { isValid: boolean; error?: string } { const isCSV = file.type === 'text/csv' || (file.name && file.name.toLowerCase().endsWith('.csv')); const isExcel = file.type === 'application/vnd.oasis.opendocument.spreadsheet' || file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.type === 'application/vnd.ms-excel' || (file.name && (file.name.toLowerCase().endsWith('.ods') || file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls'))); const isPDF = file.type === 'application/pdf' || (file.name && file.name.toLowerCase().endsWith('.pdf')); if (!isCSV && !isExcel && !isPDF) { return { isValid: false, error: 'Veuillez sélectionner un fichier valide (CSV, ODS, XLSX, XLS ou PDF).' }; } return { isValid: true }; } /** * Formate une taille de fichier en bytes vers une représentation lisible */ export function formatFileSize(bytes: number): string { if (bytes < 0) return '0 B'; if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Extrait l'extension d'un nom de fichier */ export function getFileExtension(filename: string): string { if (!filename || filename.indexOf('.') === -1) return ''; const parts = filename.split('.'); return parts[parts.length - 1]; } /** * Nettoie un nom de fichier en supprimant les caractères spéciaux */ export function sanitizeFileName(filename: string): string { if (!filename) return ''; // Supprimer les espaces en début et fin let sanitized = filename.trim(); // Remplacer les caractères spéciaux par des tirets sanitized = sanitized.replace(/[^a-zA-Z0-9.-]/g, '-'); // Supprimer les tirets multiples sanitized = sanitized.replace(/-+/g, '-'); // Supprimer les tirets en début et fin sanitized = sanitized.replace(/^-+|-+$/g, ''); // Limiter la longueur à 255 caractères if (sanitized.length > 255) { const extension = getFileExtension(sanitized); const nameWithoutExt = sanitized.substring(0, sanitized.lastIndexOf('.')); const maxNameLength = 255 - extension.length - 1; // -1 pour le point sanitized = nameWithoutExt.substring(0, maxNameLength) + '.' + extension; } return sanitized; }