293 lines
9.9 KiB
TypeScript
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;
|
|
}
|