Checkpoint: Corrections multiples :
- Renommage questionnaire.nom → questionnaire.titre dans QuestionnaireReponse.tsx, AdminQuestionnaireEdit.tsx, questionnaireScheduler.ts, questionnaireExport.ts - Correction des requêtes SQL dans questionnaireDb.ts (suppression de la condition complete inexistante) - Ajout des colonnes valeurMin et valeurMax dans la table questions - Renommage de la colonne nom en titre dans la table questionnaires
This commit is contained in:
331
deployment-package/source_server/importExcel.ts
Normal file
331
deployment-package/source_server/importExcel.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
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<any>(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<any>(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<ImportResult> {
|
||||
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<string, number>();
|
||||
|
||||
// Importer les formations
|
||||
console.log(`[Import] Début import de ${formations.length} formations`);
|
||||
for (const formation of formations) {
|
||||
try {
|
||||
console.log(`[Import] Création formation: ${formation.nom}`);
|
||||
const [result] = await db.insert(formationsTable).values({
|
||||
nom: formation.nom,
|
||||
description: formation.description,
|
||||
lienUnique: generateUniqueLien(),
|
||||
}).$returningId();
|
||||
|
||||
const formationId = result.id;
|
||||
formationIdMap.set(formation.nom.toLowerCase(), formationId);
|
||||
console.log(`[Import] Formation créée avec ID ${formationId}, ajoutée à la map avec clé: "${formation.nom.toLowerCase()}"`);
|
||||
formationsCreated++;
|
||||
} catch (error: any) {
|
||||
console.error(`[Import] Erreur formation "${formation.nom}":`, error);
|
||||
errors.push(`Erreur lors de la création de la formation "${formation.nom}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
console.log(`[Import] Formations créées: ${formationsCreated}, Map size: ${formationIdMap.size}`);
|
||||
console.log(`[Import] Clés dans la map:`, Array.from(formationIdMap.keys()));
|
||||
|
||||
// Si des erreurs sont survenues lors de la création des formations, arrêter l'import
|
||||
if (errors.length > 0) {
|
||||
console.error(`[Import] Arrêt de l'import car ${errors.length} erreur(s) lors de la création des formations`);
|
||||
return {
|
||||
success: false,
|
||||
formationsCreated,
|
||||
sequencesCreated: 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
// Importer les séquences
|
||||
console.log(`[Import] Début import de ${sequences.length} séquences`);
|
||||
for (const sequence of sequences) {
|
||||
try {
|
||||
const searchKey = sequence.formationNom.toLowerCase();
|
||||
console.log(`[Import] Recherche formation pour séquence "${sequence.nom}" avec clé: "${searchKey}"`);
|
||||
const formationId = formationIdMap.get(searchKey);
|
||||
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,
|
||||
}).$returningId();
|
||||
|
||||
const sequenceId = result.id;
|
||||
|
||||
// Insérer les dates de formation
|
||||
for (let i = 0; i < Math.min(sequence.dates.length, 4); i++) {
|
||||
const date = sequence.dates[i];
|
||||
await db.insert(datesFormation).values({
|
||||
sequenceId,
|
||||
dateDebut: new Date(date.debut),
|
||||
dateFin: new Date(date.fin),
|
||||
ordre: i + 1,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user