Files
mes-budgets-participatifs/src/lib/file-utils.ts
2025-09-21 20:47:14 +02:00

293 lines
9.9 KiB
TypeScript

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<ParsedFileData> {
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<ParsedFileData> {
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<void> {
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;
}