import * as XLSX from 'xlsx'; import { getDb } from './db'; import { formations as formationsTable, sequences as sequencesTable, datesFormation } from '../drizzle/schema'; export interface FormationImport { nom: string; description: string; publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre'; } export interface SequenceImport { formationNom: string; nom: string; lieu: string; publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre'; capaciteMax: number; dateBlocage: string; statut: 'ouverte' | 'bloquee' | 'terminee'; dates: Array<{ debut: string; fin: string }>; } export interface ImportResult { success: boolean; formationsCreated: number; sequencesCreated: number; errors: string[]; warnings: string[]; } /** * Parse un fichier Excel et retourne les données structurées */ export function parseExcelFile(buffer: Buffer): { formations: FormationImport[]; sequences: SequenceImport[] } { const workbook = XLSX.read(buffer, { type: 'buffer' }); const formations: FormationImport[] = []; const sequences: SequenceImport[] = []; // Parser la feuille "Formations" if (workbook.SheetNames.includes('Formations')) { const sheet = workbook.Sheets['Formations']; const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); // Ignorer la première ligne (en-têtes) et les lignes vides for (let i = 1; i < data.length; i++) { const row = data[i]; if (!row || row.length === 0 || !row[0]) continue; // Arrêter si on atteint les instructions if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break; formations.push({ nom: String(row[0] || '').trim(), description: String(row[1] || '').trim(), publicCible: normalizePublicCible(String(row[2] || '').trim()), }); } } // Parser la feuille "Séquences" if (workbook.SheetNames.includes('Séquences')) { const sheet = workbook.Sheets['Séquences']; const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); for (let i = 1; i < data.length; i++) { const row = data[i]; if (!row || row.length === 0 || !row[0]) continue; // Arrêter si on atteint les instructions if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break; const dates: Array<{ debut: string; fin: string }> = []; // Parser les 4 dates possibles (colonnes 7-14) for (let d = 0; d < 4; d++) { const debutIdx = 7 + (d * 2); const finIdx = 8 + (d * 2); if (row[debutIdx] && row[finIdx]) { dates.push({ debut: parseExcelDate(row[debutIdx]), fin: parseExcelDate(row[finIdx]), }); } } sequences.push({ formationNom: String(row[0] || '').trim(), nom: String(row[1] || '').trim(), lieu: String(row[2] || '').trim(), publicCible: normalizePublicCible(String(row[3] || '').trim()), capaciteMax: parseInt(String(row[4] || '12')), dateBlocage: parseExcelDate(row[5]), statut: normalizeStatut(String(row[6] || '').trim()), dates, }); } } return { formations, sequences }; } /** * Valide les données importées */ export function validateImportData( formations: FormationImport[], sequences: SequenceImport[] ): { valid: boolean; errors: string[]; warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; // Valider les formations formations.forEach((formation, index) => { if (!formation.nom) { errors.push(`Formation ligne ${index + 2}: Le nom est obligatoire`); } if (!formation.description) { errors.push(`Formation ligne ${index + 2}: La description est obligatoire`); } if (!['directeur', 'chef_service', 'tous', 'autre'].includes(formation.publicCible)) { errors.push(`Formation ligne ${index + 2}: Public cible invalide (${formation.publicCible})`); } }); // Créer un set des noms de formations pour validation des séquences const formationNames = new Set(formations.map(f => f.nom.toLowerCase())); // Valider les séquences sequences.forEach((sequence, index) => { if (!sequence.formationNom) { errors.push(`Séquence ligne ${index + 2}: Le nom de la formation est obligatoire`); } else if (!formationNames.has(sequence.formationNom.toLowerCase())) { errors.push(`Séquence ligne ${index + 2}: Formation "${sequence.formationNom}" non trouvée dans la feuille Formations`); } if (!sequence.nom) { errors.push(`Séquence ligne ${index + 2}: Le nom est obligatoire`); } if (!sequence.lieu) { errors.push(`Séquence ligne ${index + 2}: Le lieu est obligatoire`); } if (!['directeur', 'chef_service', 'tous', 'autre'].includes(sequence.publicCible)) { errors.push(`Séquence ligne ${index + 2}: Public cible invalide`); } if (!sequence.dateBlocage) { errors.push(`Séquence ligne ${index + 2}: La date de blocage est obligatoire`); } if (!['ouverte', 'bloquee', 'terminee'].includes(sequence.statut)) { errors.push(`Séquence ligne ${index + 2}: Statut invalide`); } if (sequence.dates.length === 0) { errors.push(`Séquence ligne ${index + 2}: Au moins une date est obligatoire`); } if (sequence.dates.length > 4) { warnings.push(`Séquence ligne ${index + 2}: Maximum 4 dates autorisées, les dates supplémentaires seront ignorées`); } }); return { valid: errors.length === 0, errors, warnings, }; } /** * Importe les données dans la base de données */ export async function importToDatabase( formations: FormationImport[], sequences: SequenceImport[] ): Promise { const db = await getDb(); if (!db) { return { success: false, formationsCreated: 0, sequencesCreated: 0, errors: ['Connexion à la base de données impossible'], warnings: [], }; } const errors: string[] = []; const warnings: string[] = []; let formationsCreated = 0; let sequencesCreated = 0; try { // Map pour stocker les IDs des formations créées const formationIdMap = new Map(); // Importer les formations for (const formation of formations) { try { const result = await db.insert(formationsTable).values({ nom: formation.nom, description: formation.description, publicCible: formation.publicCible, lienUnique: generateUniqueLien(), }); const formationId = Number(result.insertId); formationIdMap.set(formation.nom.toLowerCase(), formationId); formationsCreated++; } catch (error: any) { errors.push(`Erreur lors de la création de la formation "${formation.nom}": ${error.message}`); } } // Importer les séquences for (const sequence of sequences) { try { const formationId = formationIdMap.get(sequence.formationNom.toLowerCase()); if (!formationId) { errors.push(`Formation "${sequence.formationNom}" non trouvée pour la séquence "${sequence.nom}"`); continue; } const result = await db.insert(sequencesTable).values({ formationId, nom: sequence.nom, lieu: sequence.lieu, publicCible: sequence.publicCible, capaciteMax: sequence.capaciteMax, dateBlocage: new Date(sequence.dateBlocage), statut: sequence.statut, }); const sequenceId = Number(result.insertId); // Insérer les dates de formation for (const date of sequence.dates.slice(0, 4)) { await db.insert(datesFormation).values({ sequenceId, dateDebut: new Date(date.debut), dateFin: new Date(date.fin), }); } sequencesCreated++; } catch (error: any) { errors.push(`Erreur lors de la création de la séquence "${sequence.nom}": ${error.message}`); } } return { success: errors.length === 0, formationsCreated, sequencesCreated, errors, warnings, }; } catch (error: any) { return { success: false, formationsCreated, sequencesCreated, errors: [`Erreur générale: ${error.message}`], warnings, }; } } /** * Normalise le public cible */ function normalizePublicCible(value: string): 'directeur' | 'chef_service' | 'tous' | 'autre' { const normalized = value.toLowerCase().trim(); if (normalized === 'directeur') return 'directeur'; if (normalized === 'chef_service' || normalized === 'chef de service') return 'chef_service'; if (normalized === 'tous') return 'tous'; return 'autre'; } /** * Normalise le statut */ function normalizeStatut(value: string): 'ouverte' | 'bloquee' | 'terminee' { const normalized = value.toLowerCase().trim(); if (normalized === 'ouverte') return 'ouverte'; if (normalized === 'bloquee' || normalized === 'bloquée') return 'bloquee'; if (normalized === 'terminee' || normalized === 'terminée') return 'terminee'; return 'ouverte'; } /** * Parse une date Excel (peut être un nombre de série Excel ou une chaîne) */ function parseExcelDate(value: any): string { if (!value) return ''; // Si c'est un nombre (date Excel) if (typeof value === 'number') { const date = XLSX.SSF.parse_date_code(value); return `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')} ${String(date.H || 0).padStart(2, '0')}:${String(date.M || 0).padStart(2, '0')}`; } // Si c'est déjà une chaîne return String(value).trim(); } /** * Génère un lien unique pour une formation */ function generateUniqueLien(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); }