class DataProcessor {
constructor(state, uiManager) {
this.state = state;
this.uiManager = uiManager;
this.state.subscribe(this.onStateChange.bind(this));
}
static REVIEW_CHECKLIST_STRUCTURE = {
"1_documents_et_dossier": {
"titre": "Réception et complétude des documents",
"items": [
{ "id": "doc_001", "nom": "Mandat d'audit" },
{ "id": "doc_002", "nom": "Plan d'audit" },
{ "id": "doc_003", "nom": "Bilan de clôture" },
{ "id": "doc_004", "nom": "Éléments obligatoires complétés" },
{ "id": "doc_005", "nom": "Synthèse" },
{ "id": "doc_006", "nom": "Rapport AXP" },
{ "id": "doc_007", "nom": "Plan d'actions complété" },
{ "id": "doc_008", "nom": "Preuves de correction" },
{ "id": "doc_009", "nom": "Notes d'audit" }
]
},
"2_audits_a_distance": {
"titre": "Spécificités audits à distance",
"sous_titres": "Applicable pour : audits IFS Broker à distance + audits siège à distance",
"items": [
{ "id": "dist_001", "nom": "Captures d'écran début d'audit", "description": "Participants, date et heure visibles" },
{ "id": "dist_002", "nom": "Captures d'écran fin d'audit", "description": "Participants, date et heure visibles" },
{ "id": "dist_003", "nom": "Enregistrement historique de conservation", "description": "Si applicable selon l'outil utilisé" },
{ "id": "dist_004", "nom": "Preuve test de connexion", "description": "Capture d'écran + date/résultat complétés dans le mandat" },
{ "id": "dist_005", "nom": "Analyse de risques CRO", "description": "Complétée par le Coordinateur Responsable de l'Organisme" }
]
},
"3_coherence_durees": {
"titre": "Cohérence des durées d'audit",
"items": [
{ "id": "dur_001", "nom": "Cohérence mandat ↔ plan d'audit", "description": "Durée identique entre les deux documents" },
{ "id": "dur_002", "nom": "Cohérence mandat ↔ synthèse", "description": "Durée mandat = durée réellement passée (synthèse)" },
{ "id": "dur_003", "nom": "Cohérence rapport ↔ calculateur IFS", "description": "Joindre l'outil de calcul", "justification": "" },
{ "id": "dur_004", "nom": "Temps passé en usine", "description": "Temps effectif en site ou justification documentée", "justification": "" }
]
},
"4_statut_rapport": {
"titre": "Statut et complétude du rapport",
"items": [
{ "id": "rap_001", "nom": "Version du rapport", "description": "Statut 'non périmé' et version finalisée" },
{ "id": "rap_002", "nom": "Données manquantes", "description": "Absence de données manquantes (sauf nom/prénom CDO et date décision à compléter)", "exceptions": ["CDO nom/prénom", "Date de décision"] }
]
},
"5_donnees_entreprise": {
"titre": "Informations de l'entreprise - Profil",
"items": [
{ "id": "prof_001", "nom": "Nombre maximum d'employés", "description": "Au pic de l'activité" },
{ "id": "prof_002", "nom": "Surface totale du site", "description": "Production + stockage (m²)" },
{ "id": "prof_003", "nom": "Activité saisonnière", "description": "Renseignée si applicable" },
{ "id": "prof_004", "nom": "Produits totalement sous-traités", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
{ "id": "prof_005", "nom": "Produits de négoce", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
{ "id": "prof_006", "nom": "Procédés partiellement sous-traités", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
{ "id": "prof_007", "nom": "Usage du logo IFS", "description": "Conforme à la réglementation" }
]
},
"6_donnees_audit": {
"titre": "Données de l'audit / Évaluation",
"items": [
{ "id": "eval_001", "nom": "Produits et procédés audités", "description": "Liste des produits et procédés vus lors de l'audit sur site" },
{ "id": "eval_002", "nom": "Option d'audit", "description": "Annoncée ou non annoncée" },
{ "id": "eval_003", "nom": "Certification IFS précédente", "description": "Date et fin de validité du certificat antérieur" },
{ "id": "eval_004", "nom": "Personne en charge de la revue", "description": "Identifiée et documentée" },
{ "id": "eval_005", "nom": "Horaires d'audit", "description": "Sans les pauses + justification en cas de dépassement/réduction", "justification": "" },
{ "id": "eval_006", "nom": "Auditeurs et participants", "description": "Direction, RQ, traducteur et observateur éventuels (optionnel hors Direction/RQ)" }
]
},
"7_secteurs_scope": {
"titre": "Secteurs / Scope Data",
"items": [
{ "id": "scope_001", "nom": "Description complète des procédés", "description": "Vue complète + secteurs technologiques" },
{ "id": "scope_002", "nom": "Périmètre d'audit", "description": "Libellé + traduction anglaise" },
{ "id": "scope_003", "nom": "Exclusions", "description": "Documentées pour le certificat avec justification dans 'informations additionnelles'" },
{ "id": "scope_004", "nom": "Outils calcul durée", "description": "Secteurs technologiques sélectionnés pour chaque secteur produit" },
{ "id": "scope_005", "nom": "Sous-catégories de produits", "description": "Exhaustivité et conformité vérifiées" }
]
},
"8_audits_multisites": {
"titre": "Organisation de l'audit en multi-sites",
"items": [
{ "id": "multi_001", "nom": "Plan d'audit adapté", "description": "Identification des chapitres audités une seule fois pour tous les sites" },
{ "id": "multi_002", "nom": "Description des autres sites", "description": "Nom, localisation, statut certification IFS, COID si applicable" },
{ "id": "multi_003", "nom": "Organisation audit multisites", "description": "Activités communes auditées une fois + services centraux (date/lieu)" },
{ "id": "multi_004", "nom": "Répercution écarts siège", "description": "Plan d'actions et rapport complétés" },
{ "id": "multi_005", "nom": "Onglet multisites synthèse", "description": "Cas des audits multi-sites complété" }
]
},
"9_pertinence_deviations": {
"titre": "Pertinence des déviations",
"items": [
{ "id": "dev_001", "nom": "Justification des déviations", "description": "Notes et justification documentées" },
{ "id": "dev_002", "nom": "Suivi actions correctives précédentes", "description": "Suivi des NC/déviations de l'audit antérieur" }
]
},
"10_plan_actions": {
"titre": "Plan d'actions",
"items": [
{ "id": "pa_001", "nom": "Pertinence corrections", "description": "Actions correctives proposées par l'entreprise pertinentes et efficaces" },
{ "id": "pa_002", "nom": "Délais corrections", "description": "Antérieurs à envoi PA à OC ou certification du dossier" },
{ "id": "pa_003", "nom": "Délais actions correctives", "description": "Avant ouverture prochaine fenêtre audit selon gravité NC/déviation" },
{ "id": "pa_004", "nom": "Statuts mise en place", "description": "Corrections et actions correctives renseignées" },
{ "id": "pa_005", "nom": "Validation auditeur", "description": "Statut 'OK-Libéré-Validé-Approuvé', nom auditeur, date validation" }
]
},
"11_preuves_corrections": {
"titre": "Preuves de corrections",
"items": [
{ "id": "prev_001", "nom": "Pertinence des preuves", "description": "Preuves documentées et pertinentes pour chaque correction" }
]
},
"12_checklist": {
"titre": "Check-list audit",
"items": [
{ "id": "ckl_001", "nom": "Champs obligatoires complétés", "description": "Tous les champs obligatoires renseignés" },
{ "id": "ckl_002", "nom": "NA justifiés", "description": "Non Applicable justifiés et documentés" }
]
},
"13_traductions_anglais": {
"titre": "Traduction en anglais",
"items": [
{ "id": "tra_001", "nom": "Traduction rapport", "description": "Tout traduit sauf les NA" },
{ "id": "tra_002", "nom": "Champs obligatoires", "description": "Seule version anglaise requise pour export rapport" },
{ "id": "tra_003", "nom": "Déviations et NC", "description": "Langue audit + traduction anglaise" },
{ "id": "tra_004", "nom": "Plan d'actions", "description": "Traduction déviations, NC, corrections, actions correctives" }
]
}
};
setUIManager(uiManager) {
this.uiManager = uiManager;
}
onStateChange(newState) {
// Handle state changes here if needed
console.log('DataProcessor State changed:', newState);
if (newState.currentMode === 'auditor') {
this.renderAuditorTaskList();
}
// Re-apply filters whenever the state changes.
// This is called after the UIManager has re-rendered the tables.
this.filterProfileTable();
this.filterChecklist();
this.filterNonConformities();
}
updateElementText(elementId, text) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = text;
} else {
// console.error(`Element with id ${elementId} not found`); // Suppress for now, many elements are dynamic
}
}
processAuditDataLogic(food8) {
console.log('🎯 PROCESSING NEW IFS FILE');
try {
// 1. Extraction Immédiate des Infos de Base et du Profil
// On le fait AVANT la checklist pour afficher les infos même si la checklist échoue
const companyName = food8.questions?.companyName?.answer || 'Société inconnue';
const coid = food8.questions?.companyCoid?.answer || 'COID-XXXX';
// Mise à jour de l'interface
this.uiManager.updateCurrentAuditName(`IFS Reviewer - ${companyName}`);
this.uiManager.updateSessionInfo(coid);
// CHANGEMENT : On passe TOUT l'objet food8 pour extraire aussi les infos hors 'questions'
const companyProfileData = this.extractCompanyProfile(food8);
const auditData = this.state.get().auditData;
const conversations = {};
// Mise à jour du State partiel (Profil)
this.state.setState({
auditData: auditData, // Conserve existing or new? Actually file-handler sets auditData I think? default to existing if not passed?
// Wait, file-handler normally doesn't set auditData state before calling this?
// Actually auditData IS passed in food8 usually? No "food8" IS the auditData.
// We should probably save food8 as auditData in state.
auditData: food8,
companyProfileData: companyProfileData,
conversations: conversations,
currentSession: {
...this.state.get().currentSession,
id: `IFS-${coid}-${new Date().getTime()}`,
name: `Audit ${companyName}`,
data: food8
}
});
// Rendu du Profil Immédiat
this.renderCompanyProfile();
// Calcul des Scores (si disponibles)
if (food8.matrixResult) {
let totalA = 0, totalB = 0, totalC = 0, totalD = 0, totalNA = 0;
food8.matrixResult.forEach(item => {
if (item.type === 'scoreCount') {
switch (item.scoreId) {
case 'A': totalA += item.count; break;
case 'B': totalB += item.count; break;
case 'C': totalC += item.count; break;
case 'D': totalD += item.count; break;
case 'NA': totalNA += item.count; break;
}
}
});
const totalRequirements = totalA + totalB + totalC + totalD + totalNA;
this.uiManager.updateElementText('totalRequirements', totalRequirements);
this.uiManager.updateElementText('conformCount', totalA);
this.uiManager.updateElementText('nonConformCount', totalB + totalC + totalD);
if (food8.result?.overall?.percent) {
this.uiManager.updateElementText('overallScore', food8.result.overall.percent.toFixed(1) + '%');
}
}
// 2. Traitement Asynchrone de la Checklist
this.processChecklistData(food8.checklists).then(processedChecklist => {
const checklistData = processedChecklist.checklistData;
const requirementNumberMapping = processedChecklist.requirementNumberMapping;
this.state.setState({
checklistData: checklistData,
requirementNumberMapping: requirementNumberMapping
});
// Finalisation
this.uiManager.showSuccess(`✅ Dossier chargé : ${companyName}`);
this.uiManager.finalizeDataLoad();
}).catch(error => {
console.error('Warning processing checklist:', error);
this.uiManager.showError(`Attention : Le profil a été chargé, mais la checklist a rencontré une erreur : ${error.message}`);
// On NE reset PAS car le profil est visible
this.uiManager.showLoading(false);
this.uiManager.showResults(true);
});
} catch (error) {
console.error('Critical Error in processAuditDataLogic:', error);
this.uiManager.showError(`Erreur critique lors du chargement : ${error.message}`);
this.uiManager.resetToUploadState();
}
}
extractCompanyProfile(food8) {
if (!food8) return {};
const profile = {};
// Support de l'ancien appel (juste questions) ou du nouveau (objet complet)
const questions = food8.questions || (food8.companyName ? null : food8);
// Si food8 n'a pas de propriété 'questions' mais a des clés de questions, c'est peut-être déjà l'objet questions.
// Mais avec mon refactoring, je passe food8 complet. Donc food8.questions devrait être la cible.
const targetQuestions = food8.questions || {};
// 1. Extraction des QUESTIONS (metadata riche)
const priorities = {
'companyName': 'Nom du site à auditer',
'companyCoid': 'N° COID du portail',
// companyGln géré spécifiquement
'companyStreetNo': 'Rue',
'companyZip': 'Code postal',
'companyCity': 'Nom de la ville',
'companyCountry': 'Pays',
'companyTelephone': 'Téléphone',
'companyEmail': 'Email',
'companyGpsLatitude': 'Latitude',
'companyGpsLongitude': 'Longitude',
'headquartersName': 'Nom du siège social',
'headquartersStreetNo': 'Rue (siège social)',
'headquartersCity': 'Nom de la ville (siège social)',
'headquartersZip': 'Code postal (siège social)',
'headquartersCountry': 'Pays (siège social)',
'headquartersTelephone': 'Téléphone (siège social)',
'productionAreaSize': 'Surface couverte de l\'entreprise (m²)',
'numberOfBuildings': 'Nombre de bâtiments',
'numberOfProductionLines': 'Nombre de lignes de production',
'numberOfFloors': 'Nombre d\'étages',
'numberOfEmployeesForTimeCalculation': 'Nombre maximum d\'employés',
'numberOfEmployeesDescription': 'Commentaires employés',
'companyStructureDecentralisedDescription': 'Structures décentralisées',
'companyStructureMultiLocationProductionDescription': 'Fonctions centralisées',
'workingLanguage': 'Langue parlée et écrite sur le site',
'qmsLanguage': 'Langue du système qualité',
'scopeCertificateScopeDescription_en': 'Audit scope EN',
'scopeAuditScopeDescription': 'Périmètre de l\'audit FR',
'scopeProductGroupsDescription': 'Process et activités',
'seasonalProduction': 'Activité saisonnière ? (O/N)',
'partlyOutsourcedProcesses': 'Partie du procédé sous-traitée ?',
'partlyOutsourcedProcessesDescription': 'Procédés sous-traités',
'fullyOutsourcedProducts': 'Produits totalement sous-traités ?',
'fullyOutsourcedProductsDescription': 'Liste produits sous-traités',
'tradedProductsBrokerActivity': 'Produits de négoce ?',
'tradedProductsBrokerActivityDescription': 'Liste produits de négoce'
};
// A. Traitement des QUESTIONS
Object.entries(targetQuestions).forEach(([key, data]) => {
if (!data || data.answer === undefined || data.answer === '' || data.answer === null) return;
let label = priorities[key];
if (!label) {
label = data.text || key;
}
let value = data.answer;
// --- GESTION DES CHAMPS COMPLEXES SPÉCIFIQUES ---
// 1. GLN (déjà traité)
if (key === 'companyGln' && Array.isArray(value) && value[0]?.rootQuestions?.companyGlnNumber?.answer) {
value = value[0].rootQuestions.companyGlnNumber.answer;
}
// 2. Langues (tableau simple)
else if ((key === 'qmsLanguage' || key === 'workingLanguage' || key === 'auditLanguage') && Array.isArray(value)) {
value = value.join(', ');
}
// 3. Codes Emballeur et Agrément Sanitaire (Tableaux d'objets imbriqués)
else if ((key === 'companyLegalRegistrationNumbers' || key === 'companySanitaryLegalAuthorisationNumbers') && Array.isArray(value)) {
// Structure: [{ rootQuestions: { keyText: { answer: "VALEUR" } } }]
const codes = value.map(item => {
const rootQ = item.rootQuestions;
if (!rootQ) return null;
// On cherche la clé qui contient "Text" ou "Number"
// ex: companyLegalRegistrationNumbersText ou companySanitaryLegalAuthorisationNumbersText
const textKey = Object.keys(rootQ).find(k => k.includes('Text') || k.includes('Number'));
return textKey ? rootQ[textKey].answer : null;
}).filter(Boolean).join(', ');
if (codes) value = codes;
else return;
}
// 4. Claims / Autres listes complexes similaires
else if (key === 'companyClaimsOtherList' && Array.isArray(value)) {
const claims = value.map(item => item.rootQuestions?.companyClaimsOtherListText?.answer).filter(Boolean).join(', ');
if (claims) value = claims;
else return;
}
// 5. Statuts Certification (Sous-traitance) - Tentative d'extraction si complexe
else if ((key.includes('CertificationStatus') || key.includes('Coid')) && Array.isArray(value)) {
// Souvent vide ou complexe. On tente le join si simple, sinon JSON stringify pour debug
if (value.length > 0 && typeof value[0] !== 'object') value = value.join(', ');
else if (value.length > 0) return; // On ignore pour l'instant si trop complexe
else return; // Vide
}
// 6. Tableaux simples génériques
else if (Array.isArray(value)) {
if (value.length > 0 && typeof value[0] !== 'object') {
value = value.join(', ');
} else { return; }
}
// 7. Objets (on ignore sauf si géré plus haut)
else if (typeof value === 'object') {
return;
}
// Mappages de labels manquants spécifiques demandés
if (key === 'companyLegalRegistrationNumbersDescription') label = 'Description Code Emballeur';
if (key === 'companyLegalRegistrationNumbers') label = 'Code Emballeur / Agrément'; // Sera écrasé ou ajouté
if (key === 'companySanitaryLegalAuthorisationNumbers') label = 'N° Agrément Sanitaire';
if (key === 'fullyOutsourcedProducts') label = 'Produits totalement sous-traités ?';
if (key === 'tradedProductsBrokerActivity') label = 'Activité de négoce ?';
if (key === 'productsProducedProcessesRunning') label = 'Procédés observés (Audit)';
if (key === 'exclusions') label = 'Exclusions ?';
if (key === 'exclusionsDescription') label = 'Justification Exclusions';
if (key === 'followupSummary') label = 'Vérification des AC N-1 (Suivi)';
if (key === 'koSummary') label = 'Résumé KO';
if (value) profile[label] = value;
});
// B. Extraction des propriétés RACINES utiles (hors checklists, questions, resultats, objets complexes traités après)
const ignoredRoots = ['questions', 'checklists', 'matrixResult', 'result', 'users', 'auditDetails', 'originalFileName', 'auditors', 'participants', 'productScopesAudit', 'techScopesAudit', 'productScopesCertificate', 'techScopesCertificate', 'auditTimes', 'processingStepsAudit', 'processingStepsCertificate'];
Object.keys(food8).forEach(key => {
if (ignoredRoots.includes(key)) return;
const val = food8[key];
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
const label = key.charAt(0).toUpperCase() + key.slice(1);
profile[label] = val.toString();
}
});
// C. Traitement des Objets Complexes Spécifiques (pour "tout extraire")
// 1. Auditeurs
if (food8.auditors) {
const auditorList = Object.values(food8.auditors).map(a => {
const data = a.auditorData || {};
return `${data.firstName || ''} ${data.lastName || ''} (${data.role || 'Auditor'})`.trim();
}).filter(s => s !== '()').join(', ');
if (auditorList) profile['Auditeurs'] = auditorList;
}
// 2. Participants
if (food8.participants) {
const participantList = Object.values(food8.participants).map(p => {
return `${p.firstName || ''} ${p.lastName || ''} - ${p.position || 'N/A'}`.trim();
}).filter(s => s !== '- N/A').join(', '); // Saut de ligne pour lisibilité si supporté par CSS, sinon virgule
if (participantList) profile['Participants'] = participantList;
// Note: Le CSS actuel du tableau supporte mal les \n, on verra. Sinon utiliser
si le rendu est HTML.
// DataProcessor renderCompanyProfile utilise textContent ou innerHTML? Check renderCompanyProfile.
// renderCompanyProfile dans data-processor.js utilise généralement textContent.
// On va utiliser ", " pour l'instant.
}
// 3. Dates d'audit
if (food8.auditTimes) {
if (food8.auditTimes.startDate) profile['Date début audit'] = food8.auditTimes.startDate;
if (food8.auditTimes.endDate) profile['Date fin audit'] = food8.auditTimes.endDate;
if (food8.auditTimes.auditTimeInMinutes) profile['Durée audit (min)'] = food8.auditTimes.auditTimeInMinutes;
}
// 4. Scopes (Périmètres)
const formatScopes = (scopes, labelPrefix) => {
if (!scopes || !Array.isArray(scopes)) return;
const scopeTexts = scopes.map(s => {
if (s.text) return s.text;
// Si pas de texte, on affiche le type ou UUID court
return s.type === 'productScope' ? `Scope UUID: ...${s.uuid.slice(-6)}` : s.type;
}).filter(Boolean).join(', ');
if (scopeTexts) profile[labelPrefix] = scopeTexts;
};
formatScopes(food8.productScopesAudit, 'Scopes Produits (Audit)');
formatScopes(food8.techScopesAudit, 'Scopes Techniques (Audit)');
console.log(`✅ ${Object.keys(profile).length} champs extraits (Questions + Root + Complexes)`);
return profile;
}
async processChecklistData(checklists) {
if (!checklists?.checklistFood8?.resultScorings) {
return { checklistData: [], requirementNumberMapping: {} };
}
const resultScorings = checklists.checklistFood8.resultScorings;
console.log('🎯 MAPPING UUID → NUMÉROS IFS OFFICIELS');
console.log(`📊 ${Object.keys(resultScorings).length} UUIDs détectés`);
try {
const response = await fetch('https://raw.githubusercontent.com/M00N69/Gemini-Knowledge/refs/heads/main/IFSV8listUUID.csv');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const csvText = await response.text();
console.log('✅ CSV chargé depuis GitHub');
const uuidToInfo = this.validateAndProcessCSV(csvText);
const ifsUUIDs = Object.keys(resultScorings);
const matchedUUIDs = ifsUUIDs.filter(uuid => uuidToInfo[uuid]);
const unmatchedUUIDs = ifsUUIDs.filter(uuid => !uuidToInfo[uuid]);
console.log(`✅ UUIDs MATCHÉS : ${matchedUUIDs.length}/${ifsUUIDs.length} (${((matchedUUIDs.length / ifsUUIDs.length) * 100).toFixed(1)}%)`);
if (unmatchedUUIDs.length > 0) {
console.error(`❌ ${unmatchedUUIDs.length} UUID(s) non mappé(s)`);
const errorMessage = `❌ MAPPING INCOMPLET : ${unmatchedUUIDs.length} exigence(s) non mappée(s) sur ${ifsUUIDs.length}`;
this.uiManager.showError(errorMessage);
this.uiManager.resetToUploadState();
return { checklistData: [], requirementNumberMapping: {} };
}
console.log(`🎯 MAPPING RÉUSSI À 100% : ${matchedUUIDs.length}/${ifsUUIDs.length} exigences`);
const tempData = [];
for (const uuid in resultScorings) {
const scoring = resultScorings[uuid];
const mappedInfo = uuidToInfo[uuid];
tempData.push({
uuid,
requirementNumber: mappedInfo.num,
chapter: mappedInfo.chapitre,
theme: mappedInfo.theme,
sstheme: mappedInfo.sstheme,
scoring
});
}
tempData.sort((a, b) => {
if (a.chapter !== b.chapter) return a.chapter - b.chapter;
const aParts = a.requirementNumber.split('.').map(n => parseInt(n) || 0);
const bParts = b.requirementNumber.split('.').map(n => parseInt(n) || 0);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aVal = aParts[i] || 0;
const bVal = bParts[i] || 0;
if (aVal !== bVal) return aVal - bVal;
}
return 0;
});
let newChecklistData = [];
let newRequirementNumberMapping = {};
tempData.forEach(item => {
newRequirementNumberMapping[item.uuid] = item.requirementNumber;
let fieldAnswersText = '';
if (item.scoring.answers?.fieldAnswers) {
Object.entries(item.scoring.answers.fieldAnswers).forEach(([k, v]) => {
if (v?.answer && v.answer !== 'false' && v.answer !== 'true' && v.answer !== '') {
// Clean up key for display if possible, or just use answer
// If key ends in _en, maybe ignore if we have _fr or base?
// For now, simpler: Just show the answer.
// Often these are additional questions.
fieldAnswersText += `\n> ${v.answer}`;
}
});
}
newChecklistData.push({
uuid: item.uuid,
requirementNumber: item.requirementNumber,
chapter: item.chapter,
theme: item.theme,
sstheme: item.sstheme,
score: item.scoring.score?.label || 'N/D',
scoreValue: item.scoring.score?.value,
explanation: item.scoring.answers?.explanationText || '',
detailedExplanation: (item.scoring.answers?.englishExplanationText || '') + fieldAnswersText,
correction: item.scoring.answers?.correctionText || '',
correctiveAction: item.scoring.answers?.correctiveActionText || '',
needsCorrection: item.scoring.isCorrectionRequired || false
});
});
console.log(`✅ ${newChecklistData.length} exigences traitées avec mapping UUID complet`);
return { checklistData: newChecklistData, requirementNumberMapping: newRequirementNumberMapping };
} catch (error) {
console.error('❌ ERREUR CRITIQUE lors du chargement du CSV de mapping:', error);
const errorMessage = `❌ ERREUR DE CHARGEMENT CSV\n\nImpossible de charger le CSV de mapping des exigences IFS.`;
this.uiManager.showError(errorMessage);
this.uiManager.resetToUploadState();
return { checklistData: [], requirementNumberMapping: {} };
}
}
validateAndProcessCSV(csvText) {
const rows = this.parseCSVWithMultilineSupport(csvText);
if (rows.length < 2) {
throw new Error('CSV doit contenir au moins un header et une ligne de données');
}
const headers = rows[0].map(h => h.replace(/^["']|["']$/g, '').trim());
const requiredColumns = ['UUID', 'Num', 'Chapitre', 'Theme', 'SSTheme'];
const missingColumns = requiredColumns.filter(col => !headers.includes(col));
if (missingColumns.length > 0) {
throw new Error(`Colonnes requises manquantes dans le CSV: ${missingColumns.join(', ')}`);
}
const columnIndices = {
uuid: headers.indexOf('UUID'),
num: headers.indexOf('Num'),
chapitre: headers.indexOf('Chapitre'),
theme: headers.indexOf('Theme'),
sstheme: headers.indexOf('SSTheme')
};
const validMappings = {};
for (let i = 1; i < rows.length; i++) {
const cols = rows[i];
if (!cols || cols.length === 0 || cols.every(col => !col.trim())) continue;
const uuid = cols[columnIndices.uuid]?.trim();
let num = cols[columnIndices.num]?.trim();
const chapitre = cols[columnIndices.chapitre]?.trim();
const theme = cols[columnIndices.theme]?.trim();
const sstheme = cols[columnIndices.sstheme]?.trim();
if (!uuid || uuid.length < 10 || !num) continue;
num = num.replace(/[\r\n]/g, ' ')
.replace(/\s*(KO|NEW|\*)\s*/gi, '')
.replace(/\s+/g, ' ')
.trim();
if (!num) continue;
let chapitreNum;
if (chapitre && chapitre.includes('-')) {
chapitreNum = parseInt(chapitre.split('-')[0]);
} else {
chapitreNum = parseInt(chapitre);
}
if (isNaN(chapitreNum) || chapitreNum < 1 || chapitreNum > 5) continue;
if (validMappings[uuid]) continue;
validMappings[uuid] = {
num: num,
chapitre: chapitreNum,
theme: theme || 'N/A',
sstheme: sstheme || 'N/A'
};
}
return validMappings;
}
parseCSVWithMultilineSupport(csvText) {
const lines = csvText.split('\n');
const result = [];
let currentRow = [];
let currentField = '';
let inQuotes = false;
let i = 0;
while (i < lines.length) {
const line = lines[i];
let j = 0;
while (j < line.length) {
const char = line[j];
const nextChar = line[j + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
j += 2;
continue;
}
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
currentRow.push(currentField.trim());
currentField = '';
} else {
currentField += char;
}
j++;
}
if (inQuotes) {
currentField += '\n';
} else {
currentRow.push(currentField.trim());
result.push(currentRow);
currentRow = [];
currentField = '';
}
i++;
}
if (currentRow.length > 0 || currentField) {
currentRow.push(currentField.trim());
result.push(currentRow);
}
return result;
}
renderCompanyProfile() {
const container = document.getElementById('companyProfileTable');
if (!container) return;
const companyProfileData = this.state.get().companyProfileData;
const conversations = this.state.get().conversations;
if (!companyProfileData || Object.keys(companyProfileData).length === 0) {
container.innerHTML = '
Aucune donnée de profil. Chargez un fichier.
'; return; } const categories = { 'Informations générales': ['Nom du site à auditer', 'N° COID du portail', 'Code GLN', 'Auditeurs', 'Participants', 'Date début audit', 'Date fin audit', 'Durée audit (min)', 'Code Emballeur / Agrément', 'N° Agrément Sanitaire'], 'Adresse du site': ['Rue', 'Code postal', 'Nom de la ville', 'Pays', 'Téléphone', 'Email', 'Latitude', 'Longitude'], 'Siège social': ['Nom du siège social', 'Rue (siège social)', 'Nom de la ville (siège social)', 'Code postal (siège social)', 'Pays (siège social)', 'Téléphone (siège social)'], 'Informations techniques': ['Surface couverte de l\'entreprise (m²)', 'Nombre de bâtiments', 'Nombre de lignes de production', 'Nombre d\'étages', 'Nombre maximum d\'employés', 'Commentaires employés'], 'Structure organisationnelle': ['Structures décentralisées', 'Fonctions centralisées'], 'Langues': ['Langue parlée et écrite sur le site', 'Langue du système qualité', 'Langue audit'], 'Périmètres d\'audit': ['Audit scope EN', 'Scopes Produits (Audit)', 'Scopes Techniques (Audit)', 'Périmètre de l\'audit FR', 'Process et activités'], 'Activités spécifiques': [ 'Activité saisonnière ? (O/N)', 'Partie du procédé sous-traitée ?', 'Procédés sous-traités', 'Produits totalement sous-traités ?', 'Liste produits sous-traités', 'Activité de négoce ?', 'Liste produits de négoce', 'Procédés observés (Audit)' ], 'Exclusions': ['Exclusions ?', 'Justification Exclusions'], 'Résultats et Suivi': ['Résumé KO', 'Vérification des AC N-1 (Suivi)'] }; let html = ''; const displayedFields = new Set(); const renderTableSection = (title, fields) => { const visibleFields = fields.filter(f => companyProfileData.hasOwnProperty(f)); if (visibleFields.length === 0) return ''; let sectionHtml = `| Information | Valeur | Commentaires |
|---|---|---|
| ${field} | ${displayValue} |
${commentCount}
|
Vous avez répondu à toutes les questions du reviewer.
`; } else { emptyState.classList.remove('hidden'); emptyState.innerHTML = `Aucune conversation ne correspond à ce filtre.
`; } } } else { if (tableContainer) tableContainer.classList.remove('hidden'); if (emptyState) emptyState.classList.add('hidden'); let html = ''; tasks.forEach(task => { let shortMsg = task.lastMessage.length > 80 ? task.lastMessage.substring(0, 80) + '...' : task.lastMessage; let typeLabel = ''; if (task.type === 'profile') { typeLabel = 'Profil'; } else if (task.type === 'nc-pa' || task.fieldId.startsWith('pa-')) { typeLabel = 'Plan d\'actions'; } else if (task.type === 'ckl' || task.fieldId.startsWith('ckl-')) { typeLabel = 'Constat d\'audit'; } else { typeLabel = 'Checklist'; } html += `${description}
`; } contentHTML += `${category.sous_titres || ''}