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:
Manus
2026-01-14 03:08:54 -05:00
parent 8ecb856191
commit b53801c463
94 changed files with 29353 additions and 2042 deletions

View 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);
}