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 = `
${title}
`; visibleFields.forEach(field => { displayedFields.add(field); const value = companyProfileData[field]; const fieldId = `profile-${this.sanitizeFieldId(field)}`; const conversation = conversations[fieldId]; const commentStatus = this.getConversationStatus(conversation); const commentCount = conversation?.thread?.length || 0; const isLongText = (typeof value === 'string' && value.length > 100) || ['Périmètre de l\'audit FR', 'Audit scope EN', 'Process et activités', 'Procédés observés (Audit)', 'Vérification des AC N-1 (Suivi)'].includes(field); const displayValue = isLongText ? `
${value || 'N/A'}
` : `${value || 'N/A'}`; sectionHtml += ``; }); sectionHtml += `
InformationValeurCommentaires
${field} ${displayValue}
${commentCount}
`; return sectionHtml; }; // 1. Rendu des catégories définies Object.entries(categories).forEach(([categoryName, fields]) => { html += renderTableSection(categoryName, fields); }); // 2. Rendu des champs restants (Catch-All) const allKeys = Object.keys(companyProfileData); const remainingKeys = allKeys.filter(k => !displayedFields.has(k)).sort(); if (remainingKeys.length > 0) { html += renderTableSection('Autres Informations / Méta-données', remainingKeys); } container.innerHTML = html; if (this.filterProfileTable) this.filterProfileTable(); // Re-apply existing filters if any } renderChecklistTable() { const tbody = document.getElementById('checklistTableBody'); if (!tbody) return; // Use the new filtered data source instead of the raw one const checklistData = this.state.getFilteredChecklistData(); const conversations = this.state.get().conversations; if (!checklistData || checklistData.length === 0) { tbody.innerHTML = 'Aucun élément ne correspond au filtre actif.'; return; } let html = ''; checklistData.forEach(item => { const isNC = ['B', 'C', 'D'].includes(item.score); const constatFieldId = `ckl-${item.uuid}`; const paFieldId = `pa-${item.uuid}`; const constatConv = conversations[constatFieldId] || conversations[`req-${item.uuid}`]; // Fallback old format const paConv = conversations[paFieldId]; const constatStatus = this.getConversationStatus(constatConv); const constatCount = constatConv?.thread?.length || 0; const paStatus = this.getConversationStatus(paConv); const paCount = paConv?.thread?.length || 0; html += ` ${item.requirementNumber} ${item.score} ${item.explanation || ''} ${item.detailedExplanation || ''}
${constatCount}
${paCount}
`; }); tbody.innerHTML = html; this.setupTableFilters(); } renderNonConformitiesTable() { const tbody = document.getElementById('nonConformitiesTableBody'); if (!tbody) return; const checklistData = this.state.get().checklistData; const conversations = this.state.get().conversations; if (!checklistData || checklistData.length === 0) { tbody.innerHTML = 'Aucune donnée de NC/NA.'; return; } const nonConformItems = checklistData.filter(item => ['B', 'C', 'D', 'NA'].includes(item.score)); if (nonConformItems.length === 0) { tbody.innerHTML = '🎉 Aucune NC/NA trouvée !'; return; } let html = ''; nonConformItems.forEach(item => { const fieldId = `req-${item.uuid}`; const conversation = conversations[fieldId]; const commentStatus = this.getConversationStatus(conversation); const commentCount = conversation?.thread?.length || 0; html += ` ${item.requirementNumber} ${item.score} ${item.explanation || '-'} ${item.detailedExplanation || '-'}
${commentCount}
`; }); tbody.innerHTML = html; this.updateNonConformitiesStats(); } renderAuditorTaskList() { const tbody = document.getElementById('auditorTasksBody'); const emptyState = document.getElementById('auditorEmptyState'); const tableContainer = document.getElementById('auditorTasksContainer'); if (!tbody) return; const conversations = this.state.get().conversations; // Filter logic const filter = this.state.get().activeFilters.auditorTasks?.filter || 'pending'; // Update Buttons ['pending', 'resolved', 'all'].forEach(f => { const btn = document.getElementById(`auditor-filter-${f}`); if (btn) { if (f === filter) { btn.classList.add('btn-primary'); btn.classList.remove('btn-secondary'); } else { btn.classList.add('btn-secondary'); btn.classList.remove('btn-primary'); } } }); let tasks = []; let pendingCount = 0; let resolvedCount = 0; Object.entries(conversations).forEach(([fieldId, conversation]) => { const thread = conversation.thread; if (!thread || thread.length === 0) return; const lastMessage = thread[thread.length - 1]; const isResolved = conversation.status === 'resolved'; // "Pending": Reviewer spoke last, and not resolved. Auditor needs to act. const isPending = !isResolved && lastMessage.author === 'reviewer'; // "Replied/Resolved": Auditor spoke last (waiting for reviewer) OR explicitly resolved const isReplied = !isResolved && lastMessage.author === 'auditor'; if (isPending) pendingCount++; if (isResolved || isReplied) resolvedCount++; let include = false; if (filter === 'pending' && isPending) include = true; if (filter === 'resolved' && (isResolved || isReplied)) include = true; if (filter === 'all' && (isPending || isResolved || isReplied)) include = true; if (include) { const fieldInfo = this.getFieldInfo(fieldId); let statusLabel = ''; let statusClass = ''; if (isPending) { statusLabel = 'À traiter'; statusClass = 'pending'; } else if (isReplied) { statusLabel = 'En attente Reviewer'; statusClass = 'read'; // Reuse 'read' style for simplicity or add custom } else if (isResolved) { statusLabel = 'Résolu / Clôturé'; statusClass = 'resolved'; } tasks.push({ fieldId: fieldId, type: fieldInfo.type, name: fieldInfo.name, lastMessage: lastMessage.content, date: lastMessage.date, statusLabel, statusClass, isResolved: isResolved || isReplied }); } }); // Update Counters (Independent of view filter) this.uiManager.updateElementText('auditorTodoCounter', pendingCount > 0 ? pendingCount : ''); this.uiManager.updateElementText('auditorPendingTotal', pendingCount); this.uiManager.updateElementText('auditorResolvedTotal', resolvedCount); // Sort Tasks tasks.sort((a, b) => { // Prioritize Pending items at the top if (a.statusClass === 'pending' && b.statusClass !== 'pending') return -1; if (a.statusClass !== 'pending' && b.statusClass === 'pending') return 1; if (a.statusClass === 'pending') { // Pending: Oldest first (to handle queue) return new Date(a.date) - new Date(b.date); } else { // Resolved/Replied: Newest first (history) return new Date(b.date) - new Date(a.date); } }); // Display Logic if (tasks.length === 0) { if (tableContainer) tableContainer.classList.add('hidden'); if (emptyState) { // Only show the "Good Job" empty state if we are filtering for PENDING and there are no pending tasks. if (filter === 'pending') { emptyState.classList.remove('hidden'); emptyState.innerHTML = `

Tout est à jour !

Vous avez répondu à toutes les questions du reviewer.

`; } else { emptyState.classList.remove('hidden'); emptyState.innerHTML = `

Aucun élément

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 += ` ${typeLabel} ${task.name} ${shortMsg} ${task.statusLabel} `; }); tbody.innerHTML = html; } } getFieldInfo(fieldId) { const companyProfileData = this.state.get().companyProfileData; const checklistData = this.state.get().checklistData; if (fieldId.startsWith('profile-')) { const sanitizedPart = fieldId.replace('profile-', ''); let originalFieldName = ''; for (const fieldName in companyProfileData) { if (this.sanitizeFieldId(fieldName) === sanitizedPart) { originalFieldName = fieldName; break; } } if (originalFieldName) { return { name: originalFieldName, content: companyProfileData[originalFieldName] || 'N/A', type: 'profile' }; } } else if (fieldId.startsWith('dossier-')) { const rawId = fieldId.replace('dossier-', ''); let name = 'Document du dossier'; let description = ''; // Search in the static structure for (const key in DataProcessor.REVIEW_CHECKLIST_STRUCTURE) { const category = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[key]; const item = category.items.find(i => i.id === rawId); if (item) { name = item.nom; description = item.description || ''; break; } } let contentHTML = `${name}
`; if (description) { contentHTML += `

${description}

`; } contentHTML += `
Point de contrôle de la revue du dossier.`; return { name: `[DOSSIER] ${name}`, content: contentHTML, type: 'dossier' }; } else if (fieldId.startsWith('ckl-') || fieldId.startsWith('pa-') || fieldId.startsWith('req-') || fieldId.startsWith('nc-')) { const prefix = fieldId.startsWith('ckl-') ? 'ckl-' : (fieldId.startsWith('pa-') ? 'pa-' : (fieldId.startsWith('req-') ? 'req-' : 'nc-')); const uuid = fieldId.replace(prefix, ''); const item = checklistData.find(r => r.uuid === uuid); if (item) { let contentHTML = `Exigence ${item.requirementNumber} - `; contentHTML += `${item.score}

`; if (prefix === 'pa-') { contentHTML += `

PLAN D'ACTIONS

`; contentHTML += `Correction (Action Immédiate):
${item.correction || 'Non renseigné'}
`; contentHTML += `Preuves de correction:
${item.evidence || 'Non renseigné'}
`; contentHTML += `Action Corrective (Plan d'action):
${item.correctiveAction || 'Non renseigné'}
`; contentHTML += `
`; return { name: `[P.A.] Exigence ${item.requirementNumber}`, content: contentHTML, type: 'nc-pa' }; } else { // CKL or REQ or NC fallback -> Constat focus contentHTML += `

CONSTAT D'AUDIT

`; contentHTML += `Constat (Explanation):
${item.explanation || '-'}
`; contentHTML += `Explication détaillée:
${item.detailedExplanation || '-'}
`; contentHTML += `
`; return { name: `[CONSTAT] Exigence ${item.requirementNumber}`, content: contentHTML, type: prefix === 'ckl-' ? 'ckl' : 'requirement' }; } } } return { name: 'Champ inconnu', content: 'Contenu non disponible', type: 'unknown' }; } addCommentToConversation(fieldId, comment) { const currentConversations = this.state.get().conversations; const oldConversation = currentConversations[fieldId]; // Create a new thread with the new comment and update statuses immutably. const newThreadWithComment = oldConversation ? [...oldConversation.thread, comment] : [comment]; const updatedThread = newThreadWithComment.map((msg, index) => { if (index < newThreadWithComment.length - 1 && msg.status === 'pending') { return { ...msg, status: 'read' }; } return msg; }); // Create new history array, safely handling cases where history might not exist. const newHistory = oldConversation?.history ? [...oldConversation.history] : []; if (!oldConversation) { newHistory.push({ type: 'conversation_created', date: new Date().toISOString(), actor: comment.author, details: 'Conversation initiée' }); } newHistory.push({ type: 'comment_added', date: comment.date, actor: comment.author, details: `Commentaire ajouté par ${comment.author}` }); const newConversation = { ...(oldConversation || {}), thread: updatedThread, lastActivity: comment.date, status: oldConversation?.status === 'resolved' ? 'active' : (oldConversation?.status || 'active'), priority: oldConversation?.priority || 'normal', history: newHistory }; const newConversations = { ...currentConversations, [fieldId]: newConversation }; this.state.setState({ conversations: newConversations, hasUnsavedChanges: true }); } getConversationStatus(conversation) { if (!conversation || !conversation.thread || conversation.thread.length === 0) { return 'none'; } if (conversation.status === 'resolved') { return 'resolved'; } const visibleMessages = conversation.thread.filter(m => !m.isDeleted); if (visibleMessages.length === 0) { return 'none'; } const lastMessage = visibleMessages[visibleMessages.length - 1]; const currentUser = this.state.get().currentMode; if (lastMessage.author !== currentUser) { return 'pending'; } return 'read'; } getStatusLabel(status) { const labels = { 'pending': 'En attente', 'read': 'Lu', 'resolved': 'Résolu', 'none': 'Aucun' }; return labels[status] || status; } addHistoryEntry(fieldId, type, actor, details) { const conversations = { ...this.state.get().conversations }; if (conversations[fieldId]) { conversations[fieldId].history.push({ type, date: new Date().toISOString(), actor, details }); conversations[fieldId].lastActivity = new Date().toISOString(); this.state.setState({ conversations, hasUnsavedChanges: true }); } } getCommentsText(fieldId) { const conversation = this.state.get().conversations[fieldId]; if (!conversation || !conversation.thread.length) return '-'; return conversation.thread.map(comment => `[${comment.author}] ${comment.content}` ).join(' | '); } refreshAllCounters() { this.refreshCountersForTab('profil'); this.refreshCountersForTab('checklist'); this.refreshCountersForTab('nonconformites'); // Also refresh auditor tasks if relevant if (this.state.get().currentMode === 'auditor') { this.renderAuditorTaskList(); } this.updateProgressStats(); } refreshCountersForTab(tabId) { console.log(`--- Refreshing counters for tab: ${tabId} ---`); // Skip for auditor tasks tab as it's handled separately if (tabId === 'auditor-tasks') return; let pending = 0, resolved = 0, total = 0; const conversations = this.state.get().conversations; const checklistData = this.state.get().checklistData; const companyProfileData = this.state.get().companyProfileData; if (tabId === 'profil') { Object.keys(companyProfileData).forEach(field => { const fieldId = `profile-${this.sanitizeFieldId(field)}`; const conversation = conversations[fieldId]; if (conversation && conversation.thread?.length > 0) { total++; const status = this.getConversationStatus(conversation); if (status === 'resolved') resolved++; else if (status === 'pending') pending++; } }); } else if (tabId === 'checklist') { checklistData.forEach(item => { const fieldId = `req-${item.uuid}`; const conversation = conversations[fieldId]; if (conversation && conversation.thread?.length > 0) { total++; const status = this.getConversationStatus(conversation); if (status === 'resolved') resolved++; else if (status === 'pending') pending++; } }); } else if (tabId === 'nonconformites') { const nonConformItems = checklistData.filter(item => ['B', 'C', 'D', 'NA'].includes(item.score)); nonConformItems.forEach((item, index) => { const fieldId = `req-${item.uuid}`; const conversation = conversations[fieldId]; if (conversation && conversation.thread?.length > 0) { total++; const status = this.getConversationStatus(conversation); if (status === 'resolved') resolved++; else if (status === 'pending') pending++; } }); } else if (tabId === 'dossier') { Object.values(DataProcessor.REVIEW_CHECKLIST_STRUCTURE).forEach(cat => { cat.items.forEach(item => { total++; const status = this.state.get().dossierReviewState?.[item.id]; const conversation = conversations[`dossier-${item.id}`]; // Total counts conversations (consistent with other tabs) // But for Dossier, we often want to know what's left to process const hasComment = conversation && conversation.thread?.length > 0; if (!status) pending++; // Note: resolved is tricky here as it's about comments. // Let's stick to comment status for resolved/pending logic if (hasComment) { const convStatus = this.getConversationStatus(conversation); if (convStatus === 'resolved') resolved++; // If we already counted it as pending because of NC, we don't double count for comment } }); }); // Overwrite total to be the number of comments for the sidebar counter consistency const dossierCommentCount = Object.keys(conversations).filter(k => k.startsWith('dossier-') && conversations[k].thread?.length > 0).length; total = dossierCommentCount; } console.log(`--- Finished refreshing for ${tabId}. Total: ${total} ---`); this.updateElementText(`${tabId}PendingCount`, pending); this.updateElementText(`${tabId}ResolvedCount`, resolved); this.updateElementText(`${tabId}TotalCount`, total); this.updateElementText(`${tabId}CommentCounter`, total > 0 ? total : ''); } updateProgressStats() { const totalComments = this.getTotalCommentsCount(); this.updateElementText('commentsCount', totalComments); const progressPercentage = this.calculateProgressPercentage(); this.updateElementText('progressPercentage', progressPercentage + '%'); } updateNonConformitiesStats() { const checklistData = this.state.get().checklistData; const conversations = this.state.get().conversations; if (!checklistData) return; const ncNaItems = checklistData.filter(i => ['B', 'C', 'D', 'NA'].includes(i.score)); this.updateElementText('totalNC', ncNaItems.length); this.updateElementText('scoreB', checklistData.filter(i => i.score === 'B').length); this.updateElementText('scoreC', checklistData.filter(i => i.score === 'C').length); this.updateElementText('scoreD', checklistData.filter(i => i.score === 'D').length); this.updateElementText('naCount', checklistData.filter(i => i.score === 'NA').length); const ncComments = ncNaItems.filter(item => { const fieldId = `nc-${item.uuid}`; return conversations[fieldId]?.thread?.length > 0; }).length; this.updateElementText('commentsNCCount', ncComments); } getTotalCommentsCount() { const conversations = this.state.get().conversations; return Object.keys(conversations).filter(key => conversations[key]?.thread?.length > 0 ).length; } calculateProgressPercentage() { const companyProfileData = this.state.get().companyProfileData; const checklistData = this.state.get().checklistData; const totalFields = Object.keys(companyProfileData).length + checklistData.length; const fieldsWithComments = this.getTotalCommentsCount(); return totalFields > 0 ? Math.round((fieldsWithComments / totalFields) * 100) : 0; } setupTableFilters() { const createFilterHandler = (tab, filterName) => { return (e) => { const value = e.target.value; const activeFilters = { ...this.state.get().activeFilters }; activeFilters[tab][filterName] = value; this.state.setState({ activeFilters }); }; }; const setupListener = (elementId, eventType, tab, filterName) => { const element = document.getElementById(elementId); if (element) { // To prevent multiple listeners, we can store the handler on the element const handler = debounce(createFilterHandler(tab, filterName), 300); if (element._filterHandler) { element.removeEventListener(eventType, element._filterHandler); } element.addEventListener(eventType, handler); element._filterHandler = handler; } }; // Profile Filters setupListener('profileCommentStatusFilter', 'change', 'profil', 'status'); setupListener('profileSearchInput', 'keyup', 'profil', 'search'); // Checklist Filters setupListener('chapterFilter', 'change', 'checklist', 'chapter'); setupListener('scoreFilter', 'change', 'checklist', 'score'); setupListener('commentStatusFilter', 'change', 'checklist', 'status'); setupListener('searchInput', 'keyup', 'checklist', 'search'); // Non-Conformities Filters setupListener('ncTypeFilter', 'change', 'nonconformites', 'type'); setupListener('ncChapterFilter', 'change', 'nonconformites', 'chapter'); setupListener('correctionFilter', 'change', 'nonconformites', 'correction'); setupListener('ncSearchInput', 'keyup', 'nonconformites', 'search'); // These are now handled by state changes, but we keep the window binding for the UI buttons window.showAll = () => this.showAll(); window.showOnlyWithComments = () => this.showOnlyWithComments(); // QUICK FILTER BUTTONS (Added) const setupButtonFilter = (btnId, actions) => { const btn = document.getElementById(btnId); if (btn) { // Remove potential existing listeners if needed, or just add one. // Since this might be called multiple times, we should probably handle it, but simple add is fine for now if not re-run often. // Ideally use the same _handler pattern if we want to be safe. const handler = (e) => { e.preventDefault(); const activeFilters = { ...this.state.get().activeFilters }; actions(activeFilters.checklist); this.state.setState({ activeFilters }); }; if (btn._clickHandler) btn.removeEventListener('click', btn._clickHandler); btn.addEventListener('click', handler); btn._clickHandler = handler; } }; setupButtonFilter('filter-nc', (filters) => { filters.score = 'B,C,D'; filters.chapter = ''; // We might want to keep search, but let's prioritize showing NCs clearly. }); setupButtonFilter('filter-na', (filters) => { filters.score = 'NA'; filters.chapter = ''; }); setupButtonFilter('filter-comments-only', (filters) => { filters.status = 'with_comments'; }); setupButtonFilter('filter-all', (filters) => { filters.chapter = ''; filters.score = ''; filters.status = ''; filters.search = ''; }); window.setAuditorTaskFilter = (filterType) => { const activeFilters = { ...this.state.get().activeFilters }; if (activeFilters.auditorTasks) { activeFilters.auditorTasks.filter = filterType; this.state.setState({ activeFilters }); } }; window.updateCertificationDecision = () => this.updateCertificationDecision(); } filterChecklist() { const filters = this.state.get().activeFilters.checklist; const { chapter, score, status: commentStatus, search } = filters; // Update UI to reflect state const chapterFilterEl = document.getElementById('chapterFilter'); if (chapterFilterEl) chapterFilterEl.value = chapter; const scoreFilterEl = document.getElementById('scoreFilter'); if (scoreFilterEl) scoreFilterEl.value = score; const commentStatusFilterEl = document.getElementById('commentStatusFilter'); if (commentStatusFilterEl) commentStatusFilterEl.value = commentStatus; const searchInputEl = document.getElementById('searchInput'); if (searchInputEl) searchInputEl.value = search; const rows = document.querySelectorAll('#checklistTableBody tr'); if (!rows || rows.length === 0) return; rows.forEach(row => { if (row.cells.length <= 1) return; // Ignore placeholder rows let show = true; const rowChapter = row.dataset.chapter; const rowScore = row.dataset.score; const rowCommentStatus = row.dataset.commentStatus; const rowTextContent = row.textContent.toLowerCase(); if (chapter && rowChapter !== chapter) { show = false; } if (score) { const scores = score.split(','); if (!scores.includes(rowScore)) { show = false; } } if (commentStatus) { if (commentStatus === 'with_comments') { if (rowCommentStatus === 'none') show = false; } else { if (rowCommentStatus !== commentStatus) show = false; } } if (search && !rowTextContent.includes(search)) { show = false; } row.style.display = show ? '' : 'none'; }); } filterNonConformities() { const filters = this.state.get().activeFilters.nonconformites; const { type, chapter, correction, search } = filters; // Update UI to reflect state const ncTypeFilterEl = document.getElementById('ncTypeFilter'); if (ncTypeFilterEl) ncTypeFilterEl.value = type; const ncChapterFilterEl = document.getElementById('ncChapterFilter'); if (ncChapterFilterEl) ncChapterFilterEl.value = chapter; const correctionFilterEl = document.getElementById('correctionFilter'); if (correctionFilterEl) correctionFilterEl.value = correction; const ncSearchInputEl = document.getElementById('ncSearchInput'); if (ncSearchInputEl) ncSearchInputEl.value = search; const rows = document.querySelectorAll('#nonConformitiesTableBody tr'); if (!rows || rows.length === 0) return; rows.forEach(row => { if (row.cells.length <= 1) return; let show = true; const rowType = row.dataset.score; // Assuming type filter is based on score const rowChapter = row.dataset.chapter; const rowCommentStatus = row.dataset.commentStatus; const rowTextContent = row.textContent.toLowerCase(); if (type) { const scores = type.split(','); if (!scores.includes(rowType)) { show = false; } } if (chapter && rowChapter !== chapter) { show = false; } if (correction) { const hasComments = rowCommentStatus !== 'none'; if (correction === 'with' && !hasComments) { show = false; } if (correction === 'without' && hasComments) { show = false; } if (correction === 'pending' && rowCommentStatus !== 'pending') { show = false; } } if (search && !rowTextContent.includes(search)) { show = false; } row.style.display = show ? '' : 'none'; }); } filterProfileTable() { const filters = this.state.get().activeFilters.profil; const { status, search } = filters; // Update UI to reflect state const statusElement = document.getElementById('profileCommentStatusFilter'); if (statusElement) statusElement.value = status; const searchElement = document.getElementById('profileSearchInput'); if (searchElement) searchElement.value = search; const categories = document.querySelectorAll('#companyProfileTable .category-header'); if (!categories || categories.length === 0) return; categories.forEach(header => { const tableContainer = header.nextElementSibling; if (!tableContainer || !tableContainer.classList.contains('table-container')) return; const rows = tableContainer.querySelectorAll('tbody tr'); let visibleRowsInCategory = 0; rows.forEach(row => { let show = true; const rowCommentStatus = row.dataset.commentStatus; const hasComments = rowCommentStatus !== 'none'; const rowTextContent = row.textContent.toLowerCase(); if (status) { if (status === 'with_comments' && !hasComments) { show = false; } if (status === 'none' && hasComments) { show = false; } if (status === 'pending' && rowCommentStatus !== 'pending') { show = false; } if (status === 'resolved' && rowCommentStatus !== 'resolved') { show = false; } } if (search && !rowTextContent.includes(search)) { show = false; } row.style.display = show ? '' : 'none'; if (show) { visibleRowsInCategory++; } }); // Now hide the whole category if no rows are visible const shouldShowCategory = visibleRowsInCategory > 0; header.style.display = shouldShowCategory ? '' : 'none'; tableContainer.style.display = shouldShowCategory ? '' : 'none'; }); } renderDossierTable() { const container = document.getElementById('dossierTableContainer'); if (!container) return; // Ensure UI state if (!this.dossierUiState) { this.dossierUiState = { filter: 'all', openCategories: new Set() }; } const state = this.state.get(); const conversations = state.conversations || {}; const reviewState = state.dossierReviewState || {}; const currentFilter = this.dossierUiState.filter; // Statistics let allItems = []; Object.values(DataProcessor.REVIEW_CHECKLIST_STRUCTURE).forEach(cat => { cat.items.forEach(item => allItems.push({ ...item, categoryKey: cat.titre })); }); const counts = { all: allItems.length, todo: allItems.filter(i => !reviewState[i.id]).length, problem: allItems.filter(i => reviewState[i.id] === 'nok').length, comments: allItems.filter(i => conversations[`dossier-${i.id}`]?.thread?.length > 0).length }; // Local Window Helpers window.setDossierFilter = (f) => { this.dossierUiState.filter = f; this.renderDossierTable(); }; window.toggleDossierCategory = (k) => { if (this.dossierUiState.openCategories.has(k)) this.dossierUiState.openCategories.delete(k); else this.dossierUiState.openCategories.add(k); this.renderDossierTable(); }; const themeColor = 'var(--color-theme-600, #3b82f6)'; const successColor = 'var(--color-success, #10b981)'; const dangerColor = 'var(--color-danger, #ef4444)'; const grayColor = 'var(--color-gray-500, #64748b)'; let html = `
`; for (const [key, category] of Object.entries(DataProcessor.REVIEW_CHECKLIST_STRUCTURE)) { const items = category.items; const completed = items.filter(i => reviewState[i.id]).length; const isAllDone = completed === items.length; const visibleItems = items.filter(item => { const s = reviewState[item.id]; const hasC = conversations[`dossier-${item.id}`]?.thread?.length > 0; if (currentFilter === 'todo') return !s; if (currentFilter === 'problem') return s === 'nok'; if (currentFilter === 'comments') return hasC; return true; }); if (visibleItems.length === 0 && currentFilter !== 'all') continue; const isOpen = this.dossierUiState.openCategories.has(key) || currentFilter !== 'all'; html += `

${category.titre}

${category.sous_titres || ''}

TOUT N/A TOUT VALIDE ${completed} / ${items.length}
`; visibleItems.forEach(item => { const fieldId = `dossier-${item.id}`; const conv = conversations[fieldId]; const threadLen = conv?.thread?.length || 0; const status = reviewState[item.id]; html += `
${item.nom}
${item.description || ''}
`; }); if (visibleItems.length === 0) { html += `
Aucun point dans cette sélection.
`; } html += `
`; } html += `
`; container.innerHTML = html; // Ensure global functions if (!window.markCategoryNA) { window.markCategoryNA = (k) => { const cat = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[k]; if (!cat) return; const cur = this.state.get().dossierReviewState || {}; const upd = {}; cat.items.forEach(i => { if (!cur[i.id]) upd[i.id] = 'na'; }); this.state.setState({ dossierReviewState: { ...cur, ...upd } }); this.renderDossierTable(); }; } if (!window.markCategoryValid) { window.markCategoryValid = (k) => { const cat = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[k]; if (!cat) return; const cur = this.state.get().dossierReviewState || {}; const upd = {}; cat.items.forEach(i => { if (!cur[i.id]) upd[i.id] = 'ok'; }); this.state.setState({ dossierReviewState: { ...cur, ...upd } }); this.renderDossierTable(); }; } if (!window.toggleDossierStatus) { window.toggleDossierStatus = (id, s) => { const cur = this.state.get().dossierReviewState || {}; const next = cur[id] === s ? null : s; this.state.setState({ dossierReviewState: { ...cur, [id]: next } }); this.renderDossierTable(); }; } } updateCertificationDecision() { // Collect data from the form const date = document.getElementById('decisionDate')?.value; const maker = document.getElementById('decisionMaker')?.value; const result = document.getElementById('certificationResult')?.value; const comments = document.getElementById('decisionComment')?.value; // Update state const certificationDecisionData = { date, maker, result, comments, lastUpdated: new Date().toISOString() }; this.state.setState({ certificationDecisionData, hasUnsavedChanges: true }); console.log('💾 Certification decision saved to state:', certificationDecisionData); } renderCertificationDecision() { const data = this.state.get().certificationDecisionData || {}; const dateEl = document.getElementById('decisionDate'); const makerEl = document.getElementById('decisionMaker'); const resultEl = document.getElementById('certificationResult'); const commentEl = document.getElementById('decisionComment'); if (dateEl) dateEl.value = data.date || ''; if (makerEl) makerEl.value = data.maker || ''; if (resultEl) resultEl.value = data.result || ''; if (commentEl) commentEl.value = data.comments || ''; } sanitizeFieldId(str) { if (!str) return ''; // Convert to string, lowercase, and replace spaces with hyphens let sanitized = str.toString().toLowerCase().replace(/\s+/g, '-'); // Remove all characters that are not alphanumeric, a hyphen, or an underscore sanitized = sanitized.replace(/[^\w\-]/g, ''); // Replace multiple hyphens with a single one sanitized = sanitized.replace(/\-\-+/g, '-'); // Trim hyphens from the start and end sanitized = sanitized.replace(/^-+/, '').replace(/-+$/, ''); return sanitized; } }