MMOON commited on
Commit
f150763
·
verified ·
1 Parent(s): a5a87c0

Upload 4 files

Browse files
Files changed (4) hide show
  1. data-processor.js +405 -93
  2. file-handler.js +684 -22
  3. index.html +16 -3
  4. ui-manager.js +32 -8
data-processor.js CHANGED
@@ -5,6 +5,132 @@ class DataProcessor {
5
  this.state.subscribe(this.onStateChange.bind(this));
6
  }
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  setUIManager(uiManager) {
9
  this.uiManager = uiManager;
10
  }
@@ -645,25 +771,43 @@ class DataProcessor {
645
 
646
  let html = '';
647
  checklistData.forEach(item => {
648
- const fieldId = `req-${item.uuid}`;
649
- const conversation = conversations[fieldId];
650
- const commentStatus = this.getConversationStatus(conversation);
651
- const commentCount = conversation?.thread?.length || 0;
652
 
653
- html += `<tr class="table-row-clickable" data-field-id="${fieldId}" data-chapter="${item.chapter}" data-score="${item.score}" data-comment-status="${commentStatus}" onclick="openCommentModal(this)">
 
 
 
 
 
 
 
 
654
  <td class="font-medium">${item.requirementNumber}</td>
655
  <td><span class="score-badge score-${item.score}">${item.score}</span></td>
656
- <td class="max-w-xs">${item.explanation || ''}</td>
657
- <td class="max-w-xs">${item.detailedExplanation || ''}</td>
658
- <td class="comment-status-cell">
 
659
  <div class="comment-indicators">
660
- <span class="comment-count-badge" style="display: ${commentCount > 0 ? 'inline-flex' : 'none'}">${commentCount}</span>
661
- <span class="status-indicator ${commentStatus}"></span>
662
- <button class="quick-comment-btn" onclick="event.stopPropagation(); openCommentModal(this.closest('tr'))">
663
  <i class="fas fa-comment"></i>
664
  </button>
665
  </div>
666
  </td>
 
 
 
 
 
 
 
 
 
 
667
  </tr>`;
668
  });
669
 
@@ -851,8 +995,10 @@ class DataProcessor {
851
  let typeLabel = '';
852
  if (task.type === 'profile') {
853
  typeLabel = '<span class="score-badge" style="background-color:#3b82f6">Profil</span>';
854
- } else if (task.type === 'nc') {
855
- typeLabel = '<span class="score-badge" style="background-color:#ef4444">Non-Conformité</span>';
 
 
856
  } else {
857
  typeLabel = '<span class="score-badge" style="background-color:#8b5cf6">Checklist</span>';
858
  }
@@ -897,46 +1043,66 @@ class DataProcessor {
897
  };
898
  }
899
  } else if (fieldId.startsWith('dossier-')) {
900
- const dossierFields = {
901
- 'mandat': 'Mandat',
902
- 'plan-daudit': 'Plan d\'audit',
903
- 'bilan-de-clture': 'Bilan de clôture',
904
- 'elments-obligatoires': 'Eléments obligatoires',
905
- 'notes-daudit': 'Notes d\'audit',
906
- 'synthese-daudit': 'Synthèse d\'audit',
907
- 'annexes-diverses': 'Annexes diverses'
908
- };
909
- const sanitizedPart = fieldId.replace('dossier-', '');
910
- const name = dossierFields[sanitizedPart] || 'Document du dossier';
 
 
 
 
 
 
 
 
 
 
911
  return {
912
  name: `[DOSSIER] ${name}`,
913
- content: `Point de revue pour le document : ${name}`,
914
  type: 'dossier'
915
  };
916
- } else if (fieldId.startsWith('req-') || fieldId.startsWith('nc-')) {
917
- const uuid = fieldId.replace(/^(req-|nc-)/, '');
 
918
  const item = checklistData.find(r => r.uuid === uuid);
 
919
  if (item) {
920
- let contentHTML = `<strong>Score:</strong> <span class="score-badge score-${item.score}">${item.score}</span><br><br>`;
921
- contentHTML += `<strong>Constat (Explanation):</strong><br><div class="p-2 bg-gray-50 dark:bg-gray-800 rounded mb-2">${item.explanation || '-'}</div>`;
922
- contentHTML += `<strong>Explication détaillée:</strong><br><div class="p-2 bg-gray-50 dark:bg-gray-800 rounded mb-2">${item.detailedExplanation || '-'}</div>`;
923
 
924
- if (['B', 'C', 'D'].includes(item.score) || item.correction || item.correctiveAction || item.evidence) {
 
 
925
  contentHTML += `<strong>Correction (Action Immédiate):</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.correction || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
926
-
927
  contentHTML += `<strong>Preuves de correction:</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.evidence || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
928
-
929
  contentHTML += `<strong>Action Corrective (Plan d'action):</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.correctiveAction || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  }
931
-
932
- // Determine precise type
933
- const isNC = ['B', 'C', 'D'].includes(item.score);
934
-
935
- return {
936
- name: `Exigence ${item.requirementNumber}`,
937
- content: contentHTML,
938
- type: isNC ? 'nc' : 'requirement'
939
- };
940
  }
941
  }
942
 
@@ -1111,20 +1277,29 @@ class DataProcessor {
1111
  }
1112
  });
1113
  } else if (tabId === 'dossier') {
1114
- const dossierFields = [
1115
- 'Plan d\'audit', 'Mandat de l\'audit', 'Contrat/Offre',
1116
- 'Qualification de l\'auditeur', 'Documents de l\'entreprise', 'Annexes diverses'
1117
- ];
1118
- dossierFields.forEach(field => {
1119
- const fieldId = `dossier-${this.sanitizeFieldId(field)}`;
1120
- const conversation = conversations[fieldId];
1121
- if (conversation && conversation.thread?.length > 0) {
1122
  total++;
1123
- const status = this.getConversationStatus(conversation);
1124
- if (status === 'resolved') resolved++;
1125
- else if (status === 'pending') pending++;
1126
- }
 
 
 
 
 
 
 
 
 
 
 
 
1127
  });
 
 
 
1128
  }
1129
 
1130
  console.log(`--- Finished refreshing for ${tabId}. Total: ${total} ---`);
@@ -1465,56 +1640,193 @@ class DataProcessor {
1465
  const container = document.getElementById('dossierTableContainer');
1466
  if (!container) return;
1467
 
1468
- const dossierFields = [
1469
- 'Mandat',
1470
- 'Plan d\'audit',
1471
- 'Bilan de clôture',
1472
- 'Eléments obligatoires',
1473
- 'Notes d\'audit',
1474
- 'Synthèse d\'audit',
1475
- 'Annexes diverses'
1476
- ];
1477
 
1478
- const conversations = this.state.get().conversations;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1479
 
1480
  let html = `
1481
- <div class="table-container">
1482
- <table class="data-table">
1483
- <thead>
1484
- <tr>
1485
- <th style="width: 40%">Document / Thématique</th>
1486
- <th style="width: 40%">Statut / Instructions</th>
1487
- <th style="width: 20%">Revue & Commentaires</th>
1488
- </tr>
1489
- </thead>
1490
- <tbody>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1491
  `;
1492
 
1493
- dossierFields.forEach(field => {
1494
- const fieldId = `dossier-${this.sanitizeFieldId(field)}`;
1495
- const conversation = conversations[fieldId];
1496
- const commentStatus = this.getConversationStatus(conversation);
1497
- const commentCount = conversation?.thread?.length || 0;
 
 
 
 
 
 
 
 
 
 
 
 
1498
 
1499
  html += `
1500
- <tr class="table-row-clickable" data-field-id="${fieldId}" data-comment-status="${commentStatus}" onclick="openCommentModal(this)">
1501
- <td class="font-medium">${field}</td>
1502
- <td class="text-sm text-gray-500">Cliquez pour ajouter une question ou un commentaire sur ce document.</td>
1503
- <td class="comment-status-cell">
1504
- <div class="comment-indicators">
1505
- <span class="comment-count-badge" style="display: ${commentCount > 0 ? 'inline-flex' : 'none'}">${commentCount}</span>
1506
- <span class="status-indicator ${commentStatus}"></span>
1507
- <button class="quick-comment-btn" onclick="event.stopPropagation(); openCommentModal(this.closest('tr'))">
1508
- <i class="fas fa-comment"></i>
1509
- </button>
1510
  </div>
1511
- </td>
1512
- </tr>
 
 
 
 
 
 
 
1513
  `;
1514
- });
1515
 
1516
- html += `</tbody></table></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
  container.innerHTML = html;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1518
  }
1519
 
1520
  updateCertificationDecision() {
 
5
  this.state.subscribe(this.onStateChange.bind(this));
6
  }
7
 
8
+ static REVIEW_CHECKLIST_STRUCTURE = {
9
+ "1_documents_et_dossier": {
10
+ "titre": "Réception et complétude des documents",
11
+ "items": [
12
+ { "id": "doc_001", "nom": "Mandat d'audit" },
13
+ { "id": "doc_002", "nom": "Plan d'audit" },
14
+ { "id": "doc_003", "nom": "Bilan de clôture" },
15
+ { "id": "doc_004", "nom": "Éléments obligatoires complétés" },
16
+ { "id": "doc_005", "nom": "Synthèse" },
17
+ { "id": "doc_006", "nom": "Rapport AXP" },
18
+ { "id": "doc_007", "nom": "Plan d'actions complété" },
19
+ { "id": "doc_008", "nom": "Preuves de correction" },
20
+ { "id": "doc_009", "nom": "Notes d'audit" }
21
+ ]
22
+ },
23
+ "2_audits_a_distance": {
24
+ "titre": "Spécificités audits à distance",
25
+ "sous_titres": "Applicable pour : audits IFS Broker à distance + audits siège à distance",
26
+ "items": [
27
+ { "id": "dist_001", "nom": "Captures d'écran début d'audit", "description": "Participants, date et heure visibles" },
28
+ { "id": "dist_002", "nom": "Captures d'écran fin d'audit", "description": "Participants, date et heure visibles" },
29
+ { "id": "dist_003", "nom": "Enregistrement historique de conservation", "description": "Si applicable selon l'outil utilisé" },
30
+ { "id": "dist_004", "nom": "Preuve test de connexion", "description": "Capture d'écran + date/résultat complétés dans le mandat" },
31
+ { "id": "dist_005", "nom": "Analyse de risques CRO", "description": "Complétée par le Coordinateur Responsable de l'Organisme" }
32
+ ]
33
+ },
34
+ "3_coherence_durees": {
35
+ "titre": "Cohérence des durées d'audit",
36
+ "items": [
37
+ { "id": "dur_001", "nom": "Cohérence mandat ↔ plan d'audit", "description": "Durée identique entre les deux documents" },
38
+ { "id": "dur_002", "nom": "Cohérence mandat ↔ synthèse", "description": "Durée mandat = durée réellement passée (synthèse)" },
39
+ { "id": "dur_003", "nom": "Cohérence rapport ↔ calculateur IFS", "description": "Joindre l'outil de calcul", "justification": "" },
40
+ { "id": "dur_004", "nom": "Temps passé en usine", "description": "Temps effectif en site ou justification documentée", "justification": "" }
41
+ ]
42
+ },
43
+ "4_statut_rapport": {
44
+ "titre": "Statut et complétude du rapport",
45
+ "items": [
46
+ { "id": "rap_001", "nom": "Version du rapport", "description": "Statut 'non périmé' et version finalisée" },
47
+ { "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"] }
48
+ ]
49
+ },
50
+ "5_donnees_entreprise": {
51
+ "titre": "Informations de l'entreprise - Profil",
52
+ "items": [
53
+ { "id": "prof_001", "nom": "Nombre maximum d'employés", "description": "Au pic de l'activité" },
54
+ { "id": "prof_002", "nom": "Surface totale du site", "description": "Production + stockage (m²)" },
55
+ { "id": "prof_003", "nom": "Activité saisonnière", "description": "Renseignée si applicable" },
56
+ { "id": "prof_004", "nom": "Produits totalement sous-traités", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
57
+ { "id": "prof_005", "nom": "Produits de négoce", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
58
+ { "id": "prof_006", "nom": "Procédés partiellement sous-traités", "description": "Nom entreprise, localisation, certification IFS, COID si applicable" },
59
+ { "id": "prof_007", "nom": "Usage du logo IFS", "description": "Conforme à la réglementation" }
60
+ ]
61
+ },
62
+ "6_donnees_audit": {
63
+ "titre": "Données de l'audit / Évaluation",
64
+ "items": [
65
+ { "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" },
66
+ { "id": "eval_002", "nom": "Option d'audit", "description": "Annoncée ou non annoncée" },
67
+ { "id": "eval_003", "nom": "Certification IFS précédente", "description": "Date et fin de validité du certificat antérieur" },
68
+ { "id": "eval_004", "nom": "Personne en charge de la revue", "description": "Identifiée et documentée" },
69
+ { "id": "eval_005", "nom": "Horaires d'audit", "description": "Sans les pauses + justification en cas de dépassement/réduction", "justification": "" },
70
+ { "id": "eval_006", "nom": "Auditeurs et participants", "description": "Direction, RQ, traducteur et observateur éventuels (optionnel hors Direction/RQ)" }
71
+ ]
72
+ },
73
+ "7_secteurs_scope": {
74
+ "titre": "Secteurs / Scope Data",
75
+ "items": [
76
+ { "id": "scope_001", "nom": "Description complète des procédés", "description": "Vue complète + secteurs technologiques" },
77
+ { "id": "scope_002", "nom": "Périmètre d'audit", "description": "Libellé + traduction anglaise" },
78
+ { "id": "scope_003", "nom": "Exclusions", "description": "Documentées pour le certificat avec justification dans 'informations additionnelles'" },
79
+ { "id": "scope_004", "nom": "Outils calcul durée", "description": "Secteurs technologiques sélectionnés pour chaque secteur produit" },
80
+ { "id": "scope_005", "nom": "Sous-catégories de produits", "description": "Exhaustivité et conformité vérifiées" }
81
+ ]
82
+ },
83
+ "8_audits_multisites": {
84
+ "titre": "Organisation de l'audit en multi-sites",
85
+ "items": [
86
+ { "id": "multi_001", "nom": "Plan d'audit adapté", "description": "Identification des chapitres audités une seule fois pour tous les sites" },
87
+ { "id": "multi_002", "nom": "Description des autres sites", "description": "Nom, localisation, statut certification IFS, COID si applicable" },
88
+ { "id": "multi_003", "nom": "Organisation audit multisites", "description": "Activités communes auditées une fois + services centraux (date/lieu)" },
89
+ { "id": "multi_004", "nom": "Répercution écarts siège", "description": "Plan d'actions et rapport complétés" },
90
+ { "id": "multi_005", "nom": "Onglet multisites synthèse", "description": "Cas des audits multi-sites complété" }
91
+ ]
92
+ },
93
+ "9_pertinence_deviations": {
94
+ "titre": "Pertinence des déviations",
95
+ "items": [
96
+ { "id": "dev_001", "nom": "Justification des déviations", "description": "Notes et justification documentées" },
97
+ { "id": "dev_002", "nom": "Suivi actions correctives précédentes", "description": "Suivi des NC/déviations de l'audit antérieur" }
98
+ ]
99
+ },
100
+ "10_plan_actions": {
101
+ "titre": "Plan d'actions",
102
+ "items": [
103
+ { "id": "pa_001", "nom": "Pertinence corrections", "description": "Actions correctives proposées par l'entreprise pertinentes et efficaces" },
104
+ { "id": "pa_002", "nom": "Délais corrections", "description": "Antérieurs à envoi PA à OC ou certification du dossier" },
105
+ { "id": "pa_003", "nom": "Délais actions correctives", "description": "Avant ouverture prochaine fenêtre audit selon gravité NC/déviation" },
106
+ { "id": "pa_004", "nom": "Statuts mise en place", "description": "Corrections et actions correctives renseignées" },
107
+ { "id": "pa_005", "nom": "Validation auditeur", "description": "Statut 'OK-Libéré-Validé-Approuvé', nom auditeur, date validation" }
108
+ ]
109
+ },
110
+ "11_preuves_corrections": {
111
+ "titre": "Preuves de corrections",
112
+ "items": [
113
+ { "id": "prev_001", "nom": "Pertinence des preuves", "description": "Preuves documentées et pertinentes pour chaque correction" }
114
+ ]
115
+ },
116
+ "12_checklist": {
117
+ "titre": "Check-list audit",
118
+ "items": [
119
+ { "id": "ckl_001", "nom": "Champs obligatoires complétés", "description": "Tous les champs obligatoires renseignés" },
120
+ { "id": "ckl_002", "nom": "NA justifiés", "description": "Non Applicable justifiés et documentés" }
121
+ ]
122
+ },
123
+ "13_traductions_anglais": {
124
+ "titre": "Traduction en anglais",
125
+ "items": [
126
+ { "id": "tra_001", "nom": "Traduction rapport", "description": "Tout traduit sauf les NA" },
127
+ { "id": "tra_002", "nom": "Champs obligatoires", "description": "Seule version anglaise requise pour export rapport" },
128
+ { "id": "tra_003", "nom": "Déviations et NC", "description": "Langue audit + traduction anglaise" },
129
+ { "id": "tra_004", "nom": "Plan d'actions", "description": "Traduction déviations, NC, corrections, actions correctives" }
130
+ ]
131
+ }
132
+ };
133
+
134
  setUIManager(uiManager) {
135
  this.uiManager = uiManager;
136
  }
 
771
 
772
  let html = '';
773
  checklistData.forEach(item => {
774
+ const isNC = ['B', 'C', 'D'].includes(item.score);
775
+ const constatFieldId = `ckl-${item.uuid}`;
776
+ const paFieldId = `pa-${item.uuid}`;
 
777
 
778
+ const constatConv = conversations[constatFieldId] || conversations[`req-${item.uuid}`]; // Fallback old format
779
+ const paConv = conversations[paFieldId];
780
+
781
+ const constatStatus = this.getConversationStatus(constatConv);
782
+ const constatCount = constatConv?.thread?.length || 0;
783
+ const paStatus = this.getConversationStatus(paConv);
784
+ const paCount = paConv?.thread?.length || 0;
785
+
786
+ html += `<tr class="table-row-clickable" data-chapter="${item.chapter}" data-score="${item.score}">
787
  <td class="font-medium">${item.requirementNumber}</td>
788
  <td><span class="score-badge score-${item.score}">${item.score}</span></td>
789
+ <td class="max-w-xs text-xs">${item.explanation || ''}</td>
790
+ <td class="max-w-xs text-xs">${item.detailedExplanation || ''}</td>
791
+
792
+ <td class="comment-status-cell" onclick="event.stopPropagation(); window.openCommentModalFromCell(this, '${constatFieldId}')">
793
  <div class="comment-indicators">
794
+ <span class="comment-count-badge" style="display: ${constatCount > 0 ? 'inline-flex' : 'none'}">${constatCount}</span>
795
+ <span class="status-indicator ${constatStatus}"></span>
796
+ <button class="quick-comment-btn">
797
  <i class="fas fa-comment"></i>
798
  </button>
799
  </div>
800
  </td>
801
+
802
+ <td class="comment-status-cell ${isNC ? '' : 'opacity-20 pointer-events-none'}" onclick="event.stopPropagation(); if(${isNC}) window.openCommentModalFromCell(this, '${paFieldId}')">
803
+ <div class="comment-indicators">
804
+ <span class="comment-count-badge" style="display: ${paCount > 0 ? 'inline-flex' : 'none'}">${paCount}</span>
805
+ <span class="status-indicator ${paStatus}"></span>
806
+ <button class="quick-comment-btn">
807
+ <i class="fas fa-tools"></i>
808
+ </button>
809
+ </div>
810
+ </td>
811
  </tr>`;
812
  });
813
 
 
995
  let typeLabel = '';
996
  if (task.type === 'profile') {
997
  typeLabel = '<span class="score-badge" style="background-color:#3b82f6">Profil</span>';
998
+ } else if (task.type === 'nc-pa' || task.fieldId.startsWith('pa-')) {
999
+ typeLabel = '<span class="score-badge" style="background-color:#ef4444">Plan d\'actions</span>';
1000
+ } else if (task.type === 'ckl' || task.fieldId.startsWith('ckl-')) {
1001
+ typeLabel = '<span class="score-badge" style="background-color:#f59e0b">Constat d\'audit</span>';
1002
  } else {
1003
  typeLabel = '<span class="score-badge" style="background-color:#8b5cf6">Checklist</span>';
1004
  }
 
1043
  };
1044
  }
1045
  } else if (fieldId.startsWith('dossier-')) {
1046
+ const rawId = fieldId.replace('dossier-', '');
1047
+ let name = 'Document du dossier';
1048
+ let description = '';
1049
+
1050
+ // Search in the static structure
1051
+ for (const key in DataProcessor.REVIEW_CHECKLIST_STRUCTURE) {
1052
+ const category = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[key];
1053
+ const item = category.items.find(i => i.id === rawId);
1054
+ if (item) {
1055
+ name = item.nom;
1056
+ description = item.description || '';
1057
+ break;
1058
+ }
1059
+ }
1060
+
1061
+ let contentHTML = `<strong>${name}</strong><br>`;
1062
+ if (description) {
1063
+ contentHTML += `<p class="text-sm text-gray-500 mb-2">${description}</p>`;
1064
+ }
1065
+ contentHTML += `<br>Point de contrôle de la revue du dossier.`;
1066
+
1067
  return {
1068
  name: `[DOSSIER] ${name}`,
1069
+ content: contentHTML,
1070
  type: 'dossier'
1071
  };
1072
+ } else if (fieldId.startsWith('ckl-') || fieldId.startsWith('pa-') || fieldId.startsWith('req-') || fieldId.startsWith('nc-')) {
1073
+ const prefix = fieldId.startsWith('ckl-') ? 'ckl-' : (fieldId.startsWith('pa-') ? 'pa-' : (fieldId.startsWith('req-') ? 'req-' : 'nc-'));
1074
+ const uuid = fieldId.replace(prefix, '');
1075
  const item = checklistData.find(r => r.uuid === uuid);
1076
+
1077
  if (item) {
1078
+ let contentHTML = `<strong>Exigence ${item.requirementNumber}</strong> - `;
1079
+ contentHTML += `<span class="score-badge score-${item.score}">${item.score}</span><br><br>`;
 
1080
 
1081
+ if (prefix === 'pa-') {
1082
+ contentHTML += `<div style="border-left: 4px solid var(--color-danger); padding-left: 10px; margin-bottom: 15px;">
1083
+ <h4 style="margin: 0 0 10px 0; color: var(--color-danger);">PLAN D'ACTIONS</h4>`;
1084
  contentHTML += `<strong>Correction (Action Immédiate):</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.correction || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
 
1085
  contentHTML += `<strong>Preuves de correction:</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.evidence || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
 
1086
  contentHTML += `<strong>Action Corrective (Plan d'action):</strong><br><div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded mb-2">${item.correctiveAction || '<span class="text-gray-400 italic">Non renseigné</span>'}</div>`;
1087
+ contentHTML += `</div>`;
1088
+ return {
1089
+ name: `[P.A.] Exigence ${item.requirementNumber}`,
1090
+ content: contentHTML,
1091
+ type: 'nc-pa'
1092
+ };
1093
+ } else {
1094
+ // CKL or REQ or NC fallback -> Constat focus
1095
+ contentHTML += `<div style="border-left: 4px solid var(--color-theme-500); padding-left: 10px;">
1096
+ <h4 style="margin: 0 0 10px 0; color: var(--color-theme-600);">CONSTAT D'AUDIT</h4>`;
1097
+ contentHTML += `<strong>Constat (Explanation):</strong><br><div class="p-2 bg-gray-50 dark:bg-gray-800 rounded mb-2">${item.explanation || '-'}</div>`;
1098
+ contentHTML += `<strong>Explication détaillée:</strong><br><div class="p-2 bg-gray-50 dark:bg-gray-800 rounded mb-2">${item.detailedExplanation || '-'}</div>`;
1099
+ contentHTML += `</div>`;
1100
+ return {
1101
+ name: `[CONSTAT] Exigence ${item.requirementNumber}`,
1102
+ content: contentHTML,
1103
+ type: prefix === 'ckl-' ? 'ckl' : 'requirement'
1104
+ };
1105
  }
 
 
 
 
 
 
 
 
 
1106
  }
1107
  }
1108
 
 
1277
  }
1278
  });
1279
  } else if (tabId === 'dossier') {
1280
+ Object.values(DataProcessor.REVIEW_CHECKLIST_STRUCTURE).forEach(cat => {
1281
+ cat.items.forEach(item => {
 
 
 
 
 
 
1282
  total++;
1283
+ const status = this.state.get().dossierReviewState?.[item.id];
1284
+ const conversation = conversations[`dossier-${item.id}`];
1285
+
1286
+ // Total counts conversations (consistent with other tabs)
1287
+ // But for Dossier, we often want to know what's left to process
1288
+ const hasComment = conversation && conversation.thread?.length > 0;
1289
+
1290
+ if (!status) pending++;
1291
+ // Note: resolved is tricky here as it's about comments.
1292
+ // Let's stick to comment status for resolved/pending logic
1293
+ if (hasComment) {
1294
+ const convStatus = this.getConversationStatus(conversation);
1295
+ if (convStatus === 'resolved') resolved++;
1296
+ // If we already counted it as pending because of NC, we don't double count for comment
1297
+ }
1298
+ });
1299
  });
1300
+ // Overwrite total to be the number of comments for the sidebar counter consistency
1301
+ const dossierCommentCount = Object.keys(conversations).filter(k => k.startsWith('dossier-') && conversations[k].thread?.length > 0).length;
1302
+ total = dossierCommentCount;
1303
  }
1304
 
1305
  console.log(`--- Finished refreshing for ${tabId}. Total: ${total} ---`);
 
1640
  const container = document.getElementById('dossierTableContainer');
1641
  if (!container) return;
1642
 
1643
+ // Ensure UI state
1644
+ if (!this.dossierUiState) {
1645
+ this.dossierUiState = { filter: 'all', openCategories: new Set() };
1646
+ }
 
 
 
 
 
1647
 
1648
+ const state = this.state.get();
1649
+ const conversations = state.conversations || {};
1650
+ const reviewState = state.dossierReviewState || {};
1651
+ const currentFilter = this.dossierUiState.filter;
1652
+
1653
+ // Statistics
1654
+ let allItems = [];
1655
+ Object.values(DataProcessor.REVIEW_CHECKLIST_STRUCTURE).forEach(cat => {
1656
+ cat.items.forEach(item => allItems.push({ ...item, categoryKey: cat.titre }));
1657
+ });
1658
+
1659
+ const counts = {
1660
+ all: allItems.length,
1661
+ todo: allItems.filter(i => !reviewState[i.id]).length,
1662
+ problem: allItems.filter(i => reviewState[i.id] === 'nok').length,
1663
+ comments: allItems.filter(i => conversations[`dossier-${i.id}`]?.thread?.length > 0).length
1664
+ };
1665
+
1666
+ // Local Window Helpers
1667
+ window.setDossierFilter = (f) => { this.dossierUiState.filter = f; this.renderDossierTable(); };
1668
+ window.toggleDossierCategory = (k) => {
1669
+ if (this.dossierUiState.openCategories.has(k)) this.dossierUiState.openCategories.delete(k);
1670
+ else this.dossierUiState.openCategories.add(k);
1671
+ this.renderDossierTable();
1672
+ };
1673
+
1674
+ const themeColor = 'var(--color-theme-600, #3b82f6)';
1675
+ const successColor = 'var(--color-success, #10b981)';
1676
+ const dangerColor = 'var(--color-danger, #ef4444)';
1677
+ const grayColor = 'var(--color-gray-500, #64748b)';
1678
 
1679
  let html = `
1680
+ <style>
1681
+ .dossier-modern-tabs { display: flex; gap: 24px; border-bottom: 1px solid var(--border-primary); margin-bottom: 24px; padding-bottom: 2px; }
1682
+ .dossier-tab-btn { background: none; border: none; padding: 12px 4px; font-size: 14px; font-weight: 600; color: var(--text-tertiary); cursor: pointer; position: relative; transition: all 0.2s; text-transform: uppercase; letter-spacing: 0.5px; }
1683
+ .dossier-tab-btn.active { color: ${themeColor}; }
1684
+ .dossier-tab-btn.active::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 3px; background: ${themeColor}; border-radius: 3px; }
1685
+ .dossier-card { background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: 12px; margin-bottom: 20px; box-shadow: var(--shadow-sm); overflow: hidden; }
1686
+ .dossier-cat-header { padding: 20px 24px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; transition: background 0.2s; }
1687
+ .dossier-cat-header:hover { background: var(--color-gray-50); }
1688
+ .dossier-item-row { display: grid; grid-template-columns: 1fr auto auto; gap: 24px; padding: 16px 24px; align-items: center; border-top: 1px solid var(--border-primary); }
1689
+ .dossier-validation-group { display: flex; gap: 8px; }
1690
+ .dossier-v-btn { height: 36px; padding: 0 16px; border-radius: 18px; border: 1.5px solid var(--border-primary); background: transparent; color: var(--text-secondary); font-size: 11px; font-weight: 700; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 6px; }
1691
+ .dossier-v-btn:hover { background: var(--color-gray-100); border-color: var(--color-gray-400); }
1692
+ .btn-ok.active { background: ${successColor}; border-color: ${successColor}; color: white; box-shadow: 0 4px 12px -2px rgba(16, 185, 129, 0.4); }
1693
+ .btn-nok.active { background: ${dangerColor}; border-color: ${dangerColor}; color: white; box-shadow: 0 4px 12px -2px rgba(239, 68, 68, 0.4); }
1694
+ .btn-na.active { background: ${grayColor}; border-color: ${grayColor}; color: white; }
1695
+ .comment-btn-modern { width: 40px; height: 40px; border-radius: 10px; background: var(--color-gray-100); border: none; color: var(--color-gray-500); cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; transition: all 0.2s; }
1696
+ .comment-btn-modern:hover { background: var(--color-theme-50); color: ${themeColor}; }
1697
+ .comment-btn-modern.has-comments { background: var(--color-theme-100); color: ${themeColor}; border: 1px solid var(--color-theme-200); }
1698
+ .v-badge { position: absolute; top: -6px; right: -6px; background: ${themeColor}; color: white; font-size: 9px; font-weight: 800; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
1699
+ .status-dot { width: 8px; height: 8px; border-radius: 4px; background: var(--border-primary); }
1700
+ .status-dot.complete { background: ${successColor}; }
1701
+ </style>
1702
+
1703
+ <div class="dossier-modern-tabs">
1704
+ <button onclick="window.setDossierFilter('all')" class="dossier-tab-btn ${currentFilter === 'all' ? 'active' : ''}">Tout (${counts.all})</button>
1705
+ <button onclick="window.setDossierFilter('todo')" class="dossier-tab-btn ${currentFilter === 'todo' ? 'active' : ''}">À traiter (${counts.todo})</button>
1706
+ <button onclick="window.setDossierFilter('problem')" class="dossier-tab-btn ${currentFilter === 'problem' ? 'active' : ''}">Points NOK (${counts.problem})</button>
1707
+ <button onclick="window.setDossierFilter('comments')" class="dossier-tab-btn ${currentFilter === 'comments' ? 'active' : ''}">Commentaires (${counts.comments})</button>
1708
+ </div>
1709
+
1710
+ <div class="dossier-content-area">
1711
  `;
1712
 
1713
+ for (const [key, category] of Object.entries(DataProcessor.REVIEW_CHECKLIST_STRUCTURE)) {
1714
+ const items = category.items;
1715
+ const completed = items.filter(i => reviewState[i.id]).length;
1716
+ const isAllDone = completed === items.length;
1717
+
1718
+ const visibleItems = items.filter(item => {
1719
+ const s = reviewState[item.id];
1720
+ const hasC = conversations[`dossier-${item.id}`]?.thread?.length > 0;
1721
+ if (currentFilter === 'todo') return !s;
1722
+ if (currentFilter === 'problem') return s === 'nok';
1723
+ if (currentFilter === 'comments') return hasC;
1724
+ return true;
1725
+ });
1726
+
1727
+ if (visibleItems.length === 0 && currentFilter !== 'all') continue;
1728
+
1729
+ const isOpen = this.dossierUiState.openCategories.has(key) || currentFilter !== 'all';
1730
 
1731
  html += `
1732
+ <div class="dossier-card" style="border-left: 5px solid ${isAllDone ? successColor : 'var(--border-primary)'}">
1733
+ <div class="dossier-cat-header" onclick="window.toggleDossierCategory('${key}')">
1734
+ <div style="display: flex; align-items: center; gap: 16px;">
1735
+ <div class="status-dot ${isAllDone ? 'complete' : ''}"></div>
1736
+ <div>
1737
+ <h3 style="margin: 0; font-size: 16px; font-weight: 700; color: var(--text-primary);">${category.titre}</h3>
1738
+ <p style="margin: 4px 0 0 0; font-size: 12px; color: var(--text-tertiary);">${category.sous_titres || ''}</p>
1739
+ </div>
 
 
1740
  </div>
1741
+ <div style="display: flex; align-items: center; gap: 15px;">
1742
+ <span style="font-size: 10px; font-weight: 800; color: var(--color-theme-600); cursor: pointer;" onclick="event.stopPropagation(); window.markCategoryNA('${key}')">TOUT N/A</span>
1743
+ <span style="font-size: 10px; font-weight: 800; color: ${successColor}; cursor: pointer;" onclick="event.stopPropagation(); window.markCategoryValid('${key}')">TOUT VALIDE</span>
1744
+ <span style="font-size: 13px; font-weight: 700; color: ${isAllDone ? successColor : 'var(--text-tertiary)'}">${completed} / ${items.length}</span>
1745
+ <i class="fas fa-chevron-down" style="color: var(--color-gray-400); transition: transform 0.3s; transform: rotate(${isOpen ? '180deg' : '0deg'})"></i>
1746
+ </div>
1747
+ </div>
1748
+
1749
+ <div class="dossier-cat-content" style="display: ${isOpen ? 'block' : 'none'}">
1750
  `;
 
1751
 
1752
+ visibleItems.forEach(item => {
1753
+ const fieldId = `dossier-${item.id}`;
1754
+ const conv = conversations[fieldId];
1755
+ const threadLen = conv?.thread?.length || 0;
1756
+ const status = reviewState[item.id];
1757
+
1758
+ html += `
1759
+ <div class="dossier-item-row">
1760
+ <div class="item-info">
1761
+ <div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">${item.nom}</div>
1762
+ <div style="font-size: 11px; color: var(--text-tertiary); line-height: 1.4;">${item.description || ''}</div>
1763
+ </div>
1764
+
1765
+ <div class="dossier-validation-group">
1766
+ <button onclick="window.toggleDossierStatus('${item.id}', 'ok')" class="dossier-v-btn btn-ok ${status === 'ok' ? 'active' : ''}">
1767
+ <i class="fas fa-check-circle"></i> VALIDE
1768
+ </button>
1769
+ <button onclick="window.toggleDossierStatus('${item.id}', 'nok')" class="dossier-v-btn btn-nok ${status === 'nok' ? 'active' : ''}">
1770
+ <i class="fas fa-exclamation-circle"></i> ÉCART
1771
+ </button>
1772
+ <button onclick="window.toggleDossierStatus('${item.id}', 'na')" class="dossier-v-btn btn-na ${status === 'na' ? 'active' : ''}">
1773
+ N/A
1774
+ </button>
1775
+ </div>
1776
+
1777
+ <div class="item-actions">
1778
+ <button class="comment-btn-modern ${threadLen > 0 ? 'has-comments' : ''}" onclick="const r=this.closest('.dossier-item-row'); r.setAttribute('data-field-id', '${fieldId}'); openCommentModal(r)">
1779
+ <i class="fas fa-comment-dots" style="font-size: 18px;"></i>
1780
+ ${threadLen > 0 ? `<div class="v-badge">${threadLen}</div>` : ''}
1781
+ </button>
1782
+ </div>
1783
+ </div>
1784
+ `;
1785
+ });
1786
+
1787
+ if (visibleItems.length === 0) {
1788
+ html += `<div style="padding: 30px; text-align: center; color: var(--text-tertiary); font-style: italic; font-size: 13px;">Aucun point dans cette sélection.</div>`;
1789
+ }
1790
+
1791
+ html += `</div></div>`;
1792
+ }
1793
+
1794
+ html += `</div>`;
1795
  container.innerHTML = html;
1796
+
1797
+ // Ensure global functions
1798
+ if (!window.markCategoryNA) {
1799
+ window.markCategoryNA = (k) => {
1800
+ const cat = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[k];
1801
+ if (!cat) return;
1802
+ const cur = this.state.get().dossierReviewState || {};
1803
+ const upd = {};
1804
+ cat.items.forEach(i => { if (!cur[i.id]) upd[i.id] = 'na'; });
1805
+ this.state.setState({ dossierReviewState: { ...cur, ...upd } });
1806
+ this.renderDossierTable();
1807
+ };
1808
+ }
1809
+
1810
+ if (!window.markCategoryValid) {
1811
+ window.markCategoryValid = (k) => {
1812
+ const cat = DataProcessor.REVIEW_CHECKLIST_STRUCTURE[k];
1813
+ if (!cat) return;
1814
+ const cur = this.state.get().dossierReviewState || {};
1815
+ const upd = {};
1816
+ cat.items.forEach(i => { if (!cur[i.id]) upd[i.id] = 'ok'; });
1817
+ this.state.setState({ dossierReviewState: { ...cur, ...upd } });
1818
+ this.renderDossierTable();
1819
+ };
1820
+ }
1821
+
1822
+ if (!window.toggleDossierStatus) {
1823
+ window.toggleDossierStatus = (id, s) => {
1824
+ const cur = this.state.get().dossierReviewState || {};
1825
+ const next = cur[id] === s ? null : s;
1826
+ this.state.setState({ dossierReviewState: { ...cur, [id]: next } });
1827
+ this.renderDossierTable();
1828
+ };
1829
+ }
1830
  }
1831
 
1832
  updateCertificationDecision() {
file-handler.js CHANGED
@@ -102,6 +102,7 @@ class FileHandler {
102
  requirementNumberMapping: {},
103
  packageVersion: 1,
104
  certificationDecisionData: {},
 
105
  currentSession: {
106
  id: `IFS-${Date.now()}`,
107
  name: 'Nouvel Audit',
@@ -139,7 +140,8 @@ class FileHandler {
139
  conversations: conversations,
140
  requirementNumberMapping: workData.requirementNumberMapping || {},
141
  packageVersion: workData.packageVersion || 1,
142
- certificationDecisionData: workData.certificationDecisionData || {}
 
143
  });
144
 
145
  if (workData.currentMode && workData.currentMode !== this.state.get().currentMode) {
@@ -184,20 +186,25 @@ class FileHandler {
184
  }
185
 
186
  migrateConversationKeys(conversations) {
 
187
  const newConversations = { ...conversations };
188
  let keysChanged = false;
 
189
  for (const key in newConversations) {
190
- if (key.startsWith('nc-')) {
191
- const newKey = key.replace('nc-', 'req-');
192
- if (!newConversations[newKey]) { // Avoid overwriting if a req- key already exists
 
 
193
  newConversations[newKey] = newConversations[key];
194
  delete newConversations[key];
195
  keysChanged = true;
196
  }
197
  }
198
  }
 
199
  if (keysChanged) {
200
- console.log('Migrated old conversation keys to new format.');
201
  }
202
  return newConversations;
203
  }
@@ -329,6 +336,7 @@ class FileHandler {
329
  requirementNumberMapping: {},
330
  packageVersion: 1,
331
  certificationDecisionData: {},
 
332
  currentSession: {
333
  id: null,
334
  name: 'Nouveau Dossier',
@@ -368,7 +376,9 @@ class FileHandler {
368
  companyProfileData: this.state.get().companyProfileData,
369
  conversations: this.state.get().conversations,
370
  requirementNumberMapping: this.state.get().requirementNumberMapping,
 
371
  certificationDecisionData: this.state.get().certificationDecisionData,
 
372
 
373
  stats: {
374
  totalComments: this.dataProcessor.getTotalCommentsCount(),
@@ -625,14 +635,402 @@ class FileHandler {
625
  XLSX.utils.book_append_sheet(wb, ws, "COMMENTAIRES");
626
  }
627
 
628
- exportPDF() {
629
  if (!this.state.get().auditData) {
630
- this.uiManager.showError('Aucune donnée à exporter en PDF.');
631
  return;
632
  }
633
 
634
  try {
635
- this.uiManager.showError('🚧 Export PDF en cours de développement. Utilisez l\'export Excel pour le moment.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  } catch (error) {
637
  console.error('Error exporting PDF:', error);
638
  this.uiManager.showError('❌ Erreur export PDF : ' + error.message);
@@ -685,6 +1083,13 @@ class FileHandler {
685
  }
686
  };
687
 
 
 
 
 
 
 
 
688
  reader.readAsArrayBuffer(file);
689
  }
690
 
@@ -722,7 +1127,7 @@ class FileHandler {
722
  for (let i = 0; i < Math.min(rows.length, 25); i++) {
723
  const row = rows[i].map(c => String(c || '').toLowerCase().trim());
724
  if (row.includes('numéro') || row.includes('numero') || row.includes('requirementno')) {
725
- dataStartIndex = i + 2; // Skip duplication row
726
  // Update mapping
727
  row.forEach((cell, idx) => {
728
  const val = String(cell).toLowerCase().trim();
@@ -741,7 +1146,7 @@ class FileHandler {
741
  console.log("🚀 Début lecture données ligne:", dataStartIndex + 1);
742
 
743
  let updatedCount = 0;
744
- const currentChecklist = this.state.get().checklistData;
745
  const numToUUID = {};
746
 
747
  currentChecklist.forEach(item => {
@@ -778,45 +1183,302 @@ class FileHandler {
778
  const itemIndex = currentChecklist.findIndex(item => item.uuid === uuid);
779
  if (itemIndex !== -1) {
780
  let modified = false;
 
781
 
782
  if (correctionText.length > 2) {
783
- currentChecklist[itemIndex].correction = correctionText;
784
  modified = true;
785
  }
786
 
787
  if (evidenceText.length > 2) {
788
- currentChecklist[itemIndex].evidence = evidenceText;
789
  modified = true;
790
  }
791
 
792
  if (actionText.length > 2) {
793
- currentChecklist[itemIndex].correctiveAction = actionText;
794
  modified = true;
795
  }
796
 
797
- if (modified) updatedCount++;
 
 
 
798
  }
799
  }
800
  }
801
  }
802
 
803
  if (updatedCount > 0) {
804
- console.log(`✅ ${updatedCount} plans d'actions mis à jour.`);
805
-
806
  this.state.setState({
807
- checklistData: [...currentChecklist],
808
  hasUnsavedChanges: true
809
  });
810
-
811
  this.dataProcessor.renderChecklistTable();
812
  this.dataProcessor.renderNonConformitiesTable();
813
- this.uiManager.showSuccess(`✅ Import réussi ! ${updatedCount} items mis à jour avec corrections/preuves.`);
814
  } else {
815
- this.uiManager.showError("⚠️ Import terminé, mais aucune correspondance trouvée. Vérifiez 'requirementNo'/'Numéro'.");
816
  }
817
 
818
  this.uiManager.showLoading(false);
819
- this.uiManager.showResults(true);
820
- this.dataProcessor.renderCompanyProfile();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  }
822
  }
 
102
  requirementNumberMapping: {},
103
  packageVersion: 1,
104
  certificationDecisionData: {},
105
+ dossierReviewState: {},
106
  currentSession: {
107
  id: `IFS-${Date.now()}`,
108
  name: 'Nouvel Audit',
 
140
  conversations: conversations,
141
  requirementNumberMapping: workData.requirementNumberMapping || {},
142
  packageVersion: workData.packageVersion || 1,
143
+ certificationDecisionData: workData.certificationDecisionData || {},
144
+ dossierReviewState: workData.dossierReviewState || {}
145
  });
146
 
147
  if (workData.currentMode && workData.currentMode !== this.state.get().currentMode) {
 
186
  }
187
 
188
  migrateConversationKeys(conversations) {
189
+ if (!conversations) return {};
190
  const newConversations = { ...conversations };
191
  let keysChanged = false;
192
+
193
  for (const key in newConversations) {
194
+ // Migrer les anciennes clés 'req-' ou 'nc-' vers 'ckl-' (Canal Constat)
195
+ if (key.startsWith('req-') || key.startsWith('nc-')) {
196
+ const uuid = key.startsWith('req-') ? key.replace('req-', '') : key.replace('nc-', '');
197
+ const newKey = `ckl-${uuid}`;
198
+ if (!newConversations[newKey]) {
199
  newConversations[newKey] = newConversations[key];
200
  delete newConversations[key];
201
  keysChanged = true;
202
  }
203
  }
204
  }
205
+
206
  if (keysChanged) {
207
+ console.log('Migrated old conversation keys to the new dual-channel format (ckl-).');
208
  }
209
  return newConversations;
210
  }
 
336
  requirementNumberMapping: {},
337
  packageVersion: 1,
338
  certificationDecisionData: {},
339
+ dossierReviewState: {},
340
  currentSession: {
341
  id: null,
342
  name: 'Nouveau Dossier',
 
376
  companyProfileData: this.state.get().companyProfileData,
377
  conversations: this.state.get().conversations,
378
  requirementNumberMapping: this.state.get().requirementNumberMapping,
379
+ packageVersion: this.state.get().packageVersion,
380
  certificationDecisionData: this.state.get().certificationDecisionData,
381
+ dossierReviewState: this.state.get().dossierReviewState,
382
 
383
  stats: {
384
  totalComments: this.dataProcessor.getTotalCommentsCount(),
 
635
  XLSX.utils.book_append_sheet(wb, ws, "COMMENTAIRES");
636
  }
637
 
638
+ exportActionPlanForSite() {
639
  if (!this.state.get().auditData) {
640
+ this.uiManager.showError('Aucune donnée à exporter.');
641
  return;
642
  }
643
 
644
  try {
645
+ const wb = XLSX.utils.book_new();
646
+ const conversations = this.state.get().conversations;
647
+ const checklistData = this.state.get().checklistData;
648
+
649
+ const paData = [['N° Exigence', 'Score', 'Constat (Rappel d\'audit)', 'Questions Reviewer / Corrections demandées', 'Statut']];
650
+
651
+ Object.entries(conversations).forEach(([fieldId, conv]) => {
652
+ if (fieldId.startsWith('pa-')) {
653
+ const uuid = fieldId.replace('pa-', '');
654
+ const item = checklistData.find(i => i.uuid === uuid);
655
+ if (!item) return;
656
+
657
+ const commentsText = conv.thread.map(m => `[${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'}] ${m.content}`).join('\n---\n');
658
+ paData.push([
659
+ item.requirementNumber,
660
+ item.score,
661
+ item.explanation || '-',
662
+ commentsText,
663
+ this.dataProcessor.getConversationStatus(conv) === 'resolved' ? 'VALIDÉ' : 'EN ATTENTE'
664
+ ]);
665
+ }
666
+ });
667
+
668
+ if (paData.length === 1) {
669
+ this.uiManager.showError("Aucune question sur le plan d'actions (canal spécifique) n'a été identifiée.");
670
+ return;
671
+ }
672
+
673
+ const ws = XLSX.utils.aoa_to_sheet(paData);
674
+ ws['!cols'] = [{ width: 15 }, { width: 10 }, { width: 40 }, { width: 60 }, { width: 15 }];
675
+ XLSX.utils.book_append_sheet(wb, ws, "QUESTIONS SITE P.A.");
676
+
677
+ const companyName = this.state.get().companyProfileData['Nom du site à auditer'] || 'audit';
678
+ const filename = `QUESTIONS_PA_SITE_${this.sanitizeFileName(companyName)}_${this.getDateStamp()}.xlsx`;
679
+ XLSX.writeFile(wb, filename);
680
+
681
+ this.uiManager.showSuccess(`📑 Fichier pour le site généré : ${filename}`);
682
+
683
+ } catch (error) {
684
+ console.error('Error exporting PA for site:', error);
685
+ this.uiManager.showError("Erreur lors de l'exportation : " + error.message);
686
+ }
687
+ }
688
+
689
+ async exportPDF() {
690
+ if (!this.state.get().auditData) {
691
+ this.uiManager.showError('Aucune donnée à exporter.');
692
+ return;
693
+ }
694
+
695
+ try {
696
+ const { jsPDF } = window.jspdf;
697
+ const doc = new jsPDF();
698
+ const pageWidth = doc.internal.pageSize.width;
699
+ const pageHeight = doc.internal.pageSize.height;
700
+
701
+ const state = this.state.get();
702
+ const profile = state.companyProfileData || {};
703
+ const decision = state.certificationDecisionData || {};
704
+ const dossierState = state.dossierReviewState || {};
705
+ const conversations = state.conversations || {};
706
+ const checklistStructure = this.dataProcessor.constructor.REVIEW_CHECKLIST_STRUCTURE;
707
+
708
+ const findVal = (keywords) => {
709
+ const lowerKeywords = keywords.map(k => k.toLowerCase());
710
+ for (const [key, val] of Object.entries(profile)) {
711
+ const lowerKey = key.toLowerCase();
712
+ if (lowerKeywords.some(k => lowerKey.includes(k))) return val;
713
+ }
714
+ return null;
715
+ };
716
+
717
+
718
+
719
+ // --- STATUS CHECK (DRAFT OR FINAL) ---
720
+ let unresolvedDossierPixels = 0;
721
+ Object.values(checklistStructure).forEach(cat => {
722
+ cat.items.forEach(i => {
723
+ if (!dossierState[i.id]) unresolvedDossierPixels++;
724
+ });
725
+ });
726
+ const isDecisionMade = decision.date && decision.result;
727
+ const isDraft = !isDecisionMade || unresolvedDossierPixels > 0;
728
+ const watermarkText = "DOCUMENT PROVISOIRE - NON VALIDÉ";
729
+
730
+ // --- STYLING CONSTANTS ---
731
+ const COLOR_PRIMARY = [15, 23, 42]; // Slate 900
732
+ const COLOR_ACCENT = [59, 130, 246]; // Blue 500
733
+ const COLOR_SUCCESS = [16, 185, 129]; // Emerald 500
734
+ const COLOR_DANGER = [239, 68, 68]; // Red 500
735
+ const COLOR_GRAY = [148, 163, 184]; // Slate 400
736
+
737
+ // --- HELPER FUNCTIONS ---
738
+ const drawHeader = (title) => {
739
+ doc.setFillColor(...COLOR_PRIMARY);
740
+ doc.rect(0, 0, pageWidth, 25, 'F');
741
+ doc.setTextColor(255, 255, 255);
742
+ doc.setFontSize(14);
743
+ doc.setFont('helvetica', 'bold');
744
+ doc.text("IFS NEO REVIEWER", 15, 17);
745
+ doc.setFontSize(10);
746
+ doc.setFont('helvetica', 'normal');
747
+ doc.text(title, pageWidth - 15, 17, { align: 'right' });
748
+
749
+ if (isDraft) {
750
+ doc.setTextColor(200, 200, 200);
751
+ doc.setFontSize(50);
752
+ doc.text("PROVISOIRE", pageWidth / 2, pageHeight / 2, { align: 'center', angle: 45, renderingMode: 'fill' });
753
+ }
754
+ };
755
+
756
+ const drawFooter = (pageNo) => {
757
+ doc.setTextColor(150);
758
+ doc.setFontSize(8);
759
+ const str = `Page ${pageNo}`;
760
+ doc.text(str, pageWidth - 20, pageHeight - 10, { align: 'right' });
761
+ doc.text(`Généré le ${new Date().toLocaleDateString()} - IFS Review Tool`, 20, pageHeight - 10);
762
+ };
763
+
764
+ // ================= PAGE 1: COVER & SYNTHESIS =================
765
+ drawHeader("SYNTHÈSE DE CERTIFICATION");
766
+
767
+ // Company Info Box - Structured & Filtered
768
+ doc.setDrawColor(200);
769
+ doc.setFillColor(255, 255, 255); // White background
770
+
771
+ // 1. Extract specific fields using smart search
772
+ const val = (keys) => findVal(keys) || '-';
773
+
774
+ const siteName = val(['nom du site', 'site name', 'société', 'company']);
775
+ const coidCode = val(['coid']);
776
+ const auditDateVal = val(['date audit', 'dates audit', 'période']);
777
+ const reviewerName = val(['reviewer', 'review par', 'nom du reviewer']) || profile['Reviewer'] || "Non spécifié";
778
+ const auditorName = val(['auditeur', 'auditor', 'nom de l\'auditeur']) || profile['Auditeur'] || "Non spécifié";
779
+ const reviewDate = new Date().toLocaleDateString();
780
+
781
+ const scopeEn = val(['scope en', 'audit scope', 'scope english']) || "N/A";
782
+ const scopeFr = val(['périm��tre', 'scope fr', 'libellé fr']) || "N/A";
783
+
784
+ // 2. Content Layoutextract
785
+ const startY = 40;
786
+ let currentY = startY + 15;
787
+ const leftCol = 20;
788
+ const rightCol = 110;
789
+
790
+ doc.setFontSize(14);
791
+ doc.setTextColor(...COLOR_PRIMARY);
792
+ doc.setFont('helvetica', 'bold');
793
+ doc.text("INFORMATIONS CLÉS", 20, 50);
794
+
795
+ doc.setFontSize(10);
796
+ doc.setTextColor(50);
797
+
798
+ // Row 1: Site & COID
799
+ doc.setFont('helvetica', 'bold'); doc.text("Site / Société:", leftCol, currentY);
800
+ doc.setFont('helvetica', 'normal'); doc.text(String(siteName), leftCol, currentY + 5);
801
+
802
+ doc.setFont('helvetica', 'bold'); doc.text("COID:", rightCol, currentY);
803
+ doc.setFont('helvetica', 'normal'); doc.text(String(coidCode), rightCol, currentY + 5);
804
+ currentY += 15;
805
+
806
+ // Row 2: Dates
807
+ doc.setFont('helvetica', 'bold'); doc.text("Date de l'audit:", leftCol, currentY);
808
+ doc.setFont('helvetica', 'normal'); doc.text(String(auditDateVal), leftCol, currentY + 5);
809
+
810
+ doc.setFont('helvetica', 'bold'); doc.text("Date de la revue:", rightCol, currentY);
811
+ doc.setFont('helvetica', 'normal'); doc.text(reviewDate, rightCol, currentY + 5);
812
+ currentY += 15;
813
+
814
+ // Row 3: People
815
+ doc.setFont('helvetica', 'bold'); doc.text("Auditeur:", leftCol, currentY);
816
+ doc.setFont('helvetica', 'normal'); doc.text(String(auditorName), leftCol, currentY + 5);
817
+
818
+ doc.setFont('helvetica', 'bold'); doc.text("Reviewer:", rightCol, currentY);
819
+ doc.setFont('helvetica', 'normal'); doc.text(String(reviewerName), rightCol, currentY + 5);
820
+ currentY += 20;
821
+
822
+ // Row 4: Scopes (Full Width)
823
+ doc.setFont('helvetica', 'bold'); doc.text("Audit Scope (EN):", leftCol, currentY);
824
+ currentY += 5;
825
+ doc.setFont('helvetica', 'normal');
826
+ const splitScopeEn = doc.splitTextToSize(String(scopeEn), pageWidth - 40);
827
+ doc.text(splitScopeEn, leftCol, currentY);
828
+ currentY += (splitScopeEn.length * 5) + 8;
829
+
830
+ doc.setFont('helvetica', 'bold'); doc.text("Périmètre (FR):", leftCol, currentY);
831
+ currentY += 5;
832
+ doc.setFont('helvetica', 'normal');
833
+ const splitScopeFr = doc.splitTextToSize(String(scopeFr), pageWidth - 40);
834
+ doc.text(splitScopeFr, leftCol, currentY);
835
+ currentY += (splitScopeFr.length * 5) + 15;
836
+
837
+ // Draw Border around the dynamic section
838
+ const boxHeight = currentY - startY;
839
+ doc.setDrawColor(200);
840
+ doc.roundedRect(14, startY, pageWidth - 28, boxHeight, 2, 2, 'S'); // S for Stroke only
841
+
842
+ // Adjust Y for next section (Decision)
843
+
844
+
845
+
846
+
847
+ // Adjust Y for next section
848
+ const nextSectionY = currentY + 15;
849
+
850
+ // Decision Block
851
+ const decisionY = nextSectionY;
852
+ doc.setFontSize(14);
853
+ doc.setTextColor(...COLOR_PRIMARY);
854
+ doc.text("DÉCISION DE CERTIFICATION", 14, decisionY - 5);
855
+
856
+ if (isDecisionMade) {
857
+ const isSuccess = ['foundation', 'higher'].includes(decision.result);
858
+ const boxColor = isSuccess ? [240, 253, 244] : [254, 242, 242]; // Light green or light red
859
+ const borderColor = isSuccess ? COLOR_SUCCESS : COLOR_DANGER;
860
+ const textColor = isSuccess ? [21, 128, 61] : [185, 28, 28];
861
+ const resultText = decision.result === 'higher' ? "NIVEAU SUPÉRIEUR" : (decision.result === 'foundation' ? "NIVEAU DE BASE" : "NON CERTIFIÉ");
862
+
863
+ doc.setDrawColor(...borderColor);
864
+ doc.setFillColor(...boxColor);
865
+ doc.rect(14, decisionY, pageWidth - 28, 40, 'FD');
866
+
867
+ doc.setFontSize(16);
868
+ doc.setTextColor(...textColor);
869
+ doc.setFont('helvetica', 'bold');
870
+ doc.text(resultText, pageWidth / 2, decisionY + 15, { align: 'center' });
871
+
872
+ doc.setFontSize(10);
873
+ doc.setTextColor(50);
874
+ doc.setFont('helvetica', 'normal');
875
+ doc.text(`Décision prise par: ${decision.maker || 'N/A'}`, pageWidth / 2, decisionY + 25, { align: 'center' });
876
+ doc.text(`Date: ${decision.date || 'N/A'}`, pageWidth / 2, decisionY + 32, { align: 'center' });
877
+ } else {
878
+ doc.setDrawColor(200);
879
+ doc.setFillColor(245, 245, 245);
880
+ doc.rect(14, decisionY, pageWidth - 28, 30, 'FD');
881
+ doc.setFontSize(11);
882
+ doc.setTextColor(100);
883
+ doc.text("Aucune décision de certification n'a encore été enregistrée.", pageWidth / 2, decisionY + 18, { align: 'center' });
884
+ }
885
+
886
+ // Synthesis Comment
887
+ if (decision.comments) {
888
+ const synthesisY = decision.result ? decisionY + 50 : decisionY + 40;
889
+ doc.setFontSize(12);
890
+ doc.setTextColor(...COLOR_PRIMARY);
891
+ doc.text("Synthèse du Reviewer", 14, synthesisY);
892
+
893
+ doc.setFontSize(10);
894
+ doc.setTextColor(60);
895
+ const splitText = doc.splitTextToSize(decision.comments, pageWidth - 30);
896
+ doc.text(splitText, 14, synthesisY + 8);
897
+ }
898
+
899
+ drawFooter(1);
900
+
901
+
902
+ // ================= PAGE 2: DOSSIER REVIEW TABLE =================
903
+ doc.addPage();
904
+ drawHeader("REVUE DU DOSSIER");
905
+
906
+ let dossierRows = [];
907
+ Object.keys(checklistStructure).sort().forEach(key => {
908
+ const cat = checklistStructure[key];
909
+ // Category Header Row - styled differently in autotable
910
+ dossierRows.push([{ content: cat.titre.toUpperCase(), colSpan: 3, styles: { fillColor: [248, 250, 252], fontStyle: 'bold', textColor: [71, 85, 105] } }]);
911
+
912
+ cat.items.forEach(item => {
913
+ const status = dossierState[item.id];
914
+ let statusLabel = "À TRAITER";
915
+ let comments = "";
916
+
917
+ // Get conversation preview
918
+ const fieldId = `dossier-${item.id}`;
919
+ if (conversations[fieldId]?.thread?.length > 0) {
920
+ comments = `${conversations[fieldId].thread.length} message(s)`;
921
+ }
922
+
923
+ if (status === 'ok') statusLabel = "CONFORME";
924
+ if (status === 'nok') statusLabel = "NON CONFORME";
925
+ if (status === 'na') statusLabel = "N/A";
926
+
927
+ dossierRows.push([
928
+ item.nom,
929
+ statusLabel,
930
+ comments
931
+ ]);
932
+ });
933
+ });
934
+
935
+ doc.autoTable({
936
+ startY: 35,
937
+ head: [['Point de contrôle', 'Statut', 'Observations']],
938
+ body: dossierRows,
939
+ theme: 'grid',
940
+ styles: { fontSize: 9, cellPadding: 3, lineColor: [226, 232, 240] },
941
+ headStyles: { fillColor: COLOR_PRIMARY, textColor: 255, fontStyle: 'bold' },
942
+ columnStyles: {
943
+ 0: { cellWidth: 'auto' },
944
+ 1: { cellWidth: 40, fontStyle: 'bold', halign: 'center' },
945
+ 2: { cellWidth: 40, fontStyle: 'italic' }
946
+ },
947
+ didParseCell: function (data) {
948
+ if (data.section === 'body' && data.column.index === 1) {
949
+ const s = data.cell.raw;
950
+ if (s === 'CONFORME') data.cell.styles.textColor = COLOR_SUCCESS;
951
+ if (s === 'NON CONFORME') data.cell.styles.textColor = COLOR_DANGER;
952
+ if (s === 'N/A') data.cell.styles.textColor = COLOR_GRAY;
953
+ if (s === 'À TRAITER') data.cell.styles.textColor = [234, 88, 12]; // Orange
954
+ }
955
+ }
956
+ });
957
+ drawFooter(2);
958
+
959
+
960
+ // ================= PAGE 3: CONVERSATION LOGS =================
961
+ doc.addPage();
962
+ drawHeader("JOURNAL DES ÉCHANGES");
963
+
964
+ let activeThreads = [];
965
+ // Collect all relevant conversations
966
+ Object.keys(conversations).forEach(fieldId => {
967
+ const thread = conversations[fieldId].thread;
968
+ if (thread && thread.length > 0) {
969
+ const info = this.dataProcessor.getFieldInfo(fieldId);
970
+ activeThreads.push({
971
+ name: info.name,
972
+ thread: thread,
973
+ type: info.type
974
+ });
975
+ }
976
+ });
977
+
978
+ if (activeThreads.length === 0) {
979
+ doc.setFontSize(10);
980
+ doc.setTextColor(100);
981
+ doc.text("Aucun échange commentaire/réponse enregistré.", 14, 40);
982
+ } else {
983
+ let yOffset = 40;
984
+
985
+ activeThreads.forEach((item, index) => {
986
+ // Check page break
987
+ if (yOffset > pageHeight - 40) {
988
+ doc.addPage();
989
+ drawHeader("JOURNAL DES ÉCHANGES (Suite)");
990
+ yOffset = 40;
991
+ }
992
+
993
+ doc.setFontSize(11);
994
+ doc.setTextColor(...COLOR_ACCENT);
995
+ doc.setFont('helvetica', 'bold');
996
+ doc.text(`${item.name}`, 14, yOffset);
997
+ yOffset += 7;
998
+
999
+ const msgRows = item.thread.map(msg => [
1000
+ `${msg.author === 'reviewer' ? 'Reviewer' : 'Auditeur'} (${formatDate(msg.date)})`,
1001
+ msg.content
1002
+ ]);
1003
+
1004
+ doc.autoTable({
1005
+ startY: yOffset,
1006
+ body: msgRows,
1007
+ theme: 'plain',
1008
+ styles: { fontSize: 8, cellPadding: 2 },
1009
+ columnStyles: {
1010
+ 0: { cellWidth: 40, fontStyle: 'bold', textColor: [100, 100, 100] },
1011
+ 1: { cellWidth: 'auto' }
1012
+ },
1013
+ didDrawPage: function (data) {
1014
+ // Don't draw header
1015
+ }
1016
+ });
1017
+
1018
+ yOffset = doc.lastAutoTable.finalY + 10;
1019
+ });
1020
+ }
1021
+ drawFooter(3);
1022
+
1023
+ // SAVE
1024
+ // Try to find COID for filename safely
1025
+ let safeCoid = "Draft";
1026
+ const foundCoid = findVal ? findVal(['coid']) : null;
1027
+ if (foundCoid) safeCoid = this.sanitizeFileName(foundCoid);
1028
+ else if (profile['COID']) safeCoid = this.sanitizeFileName(profile['COID']);
1029
+
1030
+ const filename = `Rapport_Certification_${safeCoid}.pdf`;
1031
+ doc.save(filename);
1032
+ this.uiManager.showError(`✅ Export PDF réussi : ${filename}`, 3000); // Using showError for generic toast if nice toast not avail
1033
+
1034
  } catch (error) {
1035
  console.error('Error exporting PDF:', error);
1036
  this.uiManager.showError('❌ Erreur export PDF : ' + error.message);
 
1083
  }
1084
  };
1085
 
1086
+ reader.onerror = (error) => {
1087
+ console.error('File reading error:', error);
1088
+ this.uiManager.showError('Erreur lors de la lecture du fichier');
1089
+ this.uiManager.showLoading(false);
1090
+ event.target.value = null;
1091
+ };
1092
+
1093
  reader.readAsArrayBuffer(file);
1094
  }
1095
 
 
1127
  for (let i = 0; i < Math.min(rows.length, 25); i++) {
1128
  const row = rows[i].map(c => String(c || '').toLowerCase().trim());
1129
  if (row.includes('numéro') || row.includes('numero') || row.includes('requirementno')) {
1130
+ dataStartIndex = i + 1; // Start right after header
1131
  // Update mapping
1132
  row.forEach((cell, idx) => {
1133
  const val = String(cell).toLowerCase().trim();
 
1146
  console.log("🚀 Début lecture données ligne:", dataStartIndex + 1);
1147
 
1148
  let updatedCount = 0;
1149
+ const currentChecklist = [...this.state.get().checklistData]; // Shallow copy of array
1150
  const numToUUID = {};
1151
 
1152
  currentChecklist.forEach(item => {
 
1183
  const itemIndex = currentChecklist.findIndex(item => item.uuid === uuid);
1184
  if (itemIndex !== -1) {
1185
  let modified = false;
1186
+ const item = { ...currentChecklist[itemIndex] }; // Copy object to avoid direct mutation
1187
 
1188
  if (correctionText.length > 2) {
1189
+ item.correction = correctionText;
1190
  modified = true;
1191
  }
1192
 
1193
  if (evidenceText.length > 2) {
1194
+ item.evidence = evidenceText;
1195
  modified = true;
1196
  }
1197
 
1198
  if (actionText.length > 2) {
1199
+ item.correctiveAction = actionText;
1200
  modified = true;
1201
  }
1202
 
1203
+ if (modified) {
1204
+ currentChecklist[itemIndex] = item;
1205
+ updatedCount++;
1206
+ }
1207
  }
1208
  }
1209
  }
1210
  }
1211
 
1212
  if (updatedCount > 0) {
 
 
1213
  this.state.setState({
1214
+ checklistData: currentChecklist,
1215
  hasUnsavedChanges: true
1216
  });
1217
+ this.uiManager.showSuccess(`✅ Plan d'Actions importé : ${updatedCount} exigences mises à jour.`);
1218
  this.dataProcessor.renderChecklistTable();
1219
  this.dataProcessor.renderNonConformitiesTable();
 
1220
  } else {
1221
+ this.uiManager.showError("Aucune donnée valide n'a été importée. Vérifiez le format du fichier.");
1222
  }
1223
 
1224
  this.uiManager.showLoading(false);
1225
+ }
1226
+
1227
+ exportActionPlanForSite() {
1228
+ const state = this.state.get();
1229
+ const conversations = state.conversations || {};
1230
+ const checklistData = state.checklistData || [];
1231
+ const companyProfileData = state.companyProfileData || {};
1232
+
1233
+ // On cherche les conversations qui commencent par 'pa-' (Plan d'Actions)
1234
+ const paEntries = Object.entries(conversations).filter(([key, conv]) =>
1235
+ key.startsWith('pa-') && conv.thread && conv.thread.length > 0
1236
+ );
1237
+
1238
+ if (paEntries.length === 0) {
1239
+ this.uiManager.showError("Aucune question sur le Plan d'Actions (canal spécifique) n'a été trouvée.");
1240
+ return;
1241
+ }
1242
+
1243
+ const header = [
1244
+ "N° Exigence",
1245
+ "Score",
1246
+ "Constat (Audit)",
1247
+ "Échanges / Questions (Reviewer)",
1248
+ "Réponse Site (Correction)",
1249
+ "Preuves",
1250
+ "Action Corrective",
1251
+ "Statut"
1252
+ ];
1253
+
1254
+ const rows = paEntries.map(([fieldId, conv]) => {
1255
+ const uuid = fieldId.replace('pa-', '');
1256
+ const item = checklistData.find(i => i.uuid === uuid);
1257
+
1258
+ // On compile le fil de discussion pour l'affichage
1259
+ const discussion = conv.thread.map(m =>
1260
+ `[${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'} ${new Date(m.date).toLocaleDateString()}] : ${m.content}`
1261
+ ).join('\n\n');
1262
+
1263
+ return [
1264
+ item ? item.requirementNumber : '?',
1265
+ item ? item.score : '?',
1266
+ item ? (item.explanation || '-') : '-',
1267
+ discussion,
1268
+ item ? (item.correction || '') : '',
1269
+ item ? (item.evidence || '') : '',
1270
+ item ? (item.correctiveAction || '') : '',
1271
+ conv.status === 'resolved' ? 'VALIDÉ' : 'EN ATTENTE'
1272
+ ];
1273
+ });
1274
+
1275
+ try {
1276
+ const worksheet = XLSX.utils.aoa_to_sheet([header, ...rows]);
1277
+
1278
+ // Style de base pour les colonnes
1279
+ worksheet['!cols'] = [
1280
+ { width: 12 }, // N°
1281
+ { width: 8 }, // Score
1282
+ { width: 40 }, // Constat
1283
+ { width: 60 }, // Discussion
1284
+ { width: 30 }, // Correction
1285
+ { width: 30 }, // Preuves
1286
+ { width: 30 }, // AC
1287
+ { width: 15 } // Statut
1288
+ ];
1289
+
1290
+ const workbook = XLSX.utils.book_new();
1291
+ XLSX.utils.book_append_sheet(workbook, worksheet, "Questions Plan Actions");
1292
+
1293
+ // Nom de fichier propre
1294
+ const companyName = companyProfileData['Nom du site à auditer'] || 'Site';
1295
+ const coid = companyProfileData['N° COID du portail'] || 'COID';
1296
+ const dateStamp = this.getDateStamp();
1297
+ const filename = `QUESTIONS_PA_${this.sanitizeFileName(coid)}_${this.sanitizeFileName(companyName)}_${dateStamp}.xlsx`;
1298
+
1299
+ XLSX.writeFile(workbook, filename);
1300
+ this.uiManager.showSuccess(`✅ Export réussi : ${filename}`);
1301
+
1302
+ } catch (error) {
1303
+ console.error('Error exporting PA for site:', error);
1304
+ this.uiManager.showError("Erreur lors de l'exportation Excel : " + error.message);
1305
+ }
1306
+ }
1307
+
1308
+ generateActionPlanPrintView() {
1309
+ const state = this.state.get();
1310
+ const conversations = state.conversations || {};
1311
+ const checklistData = state.checklistData || [];
1312
+ const profile = state.companyProfileData || {};
1313
+
1314
+ const paEntries = Object.entries(conversations).filter(([key, conv]) =>
1315
+ key.startsWith('pa-') && conv.thread && conv.thread.length > 0
1316
+ );
1317
+
1318
+ if (paEntries.length === 0) {
1319
+ this.uiManager.showError("Aucune question sur le Plan d'Actions n'a été trouvée.");
1320
+ return;
1321
+ }
1322
+
1323
+ const siteName = profile['Nom du site à auditer'] || 'Site';
1324
+ const coid = profile['N° COID du portail'] || 'N/A';
1325
+
1326
+ let html = `
1327
+ <!DOCTYPE html>
1328
+ <html>
1329
+ <head>
1330
+ <title>Plan d'Actions - ${siteName}</title>
1331
+ <style>
1332
+ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 900px; margin: 40px auto; padding: 20px; }
1333
+ .header { border-bottom: 3px solid #2563eb; padding-bottom: 20px; margin-bottom: 30px; }
1334
+ .header h1 { margin: 0; color: #1e3a8a; }
1335
+ .header p { margin: 5px 0; color: #64748b; font-weight: bold; }
1336
+ .item { border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 30px; page-break-inside: avoid; }
1337
+ .item-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f1f5f9; padding-bottom: 10px; margin-bottom: 15px; }
1338
+ .req-num { font-size: 1.25rem; font-weight: 800; color: #2563eb; }
1339
+ .score-badge { background: #f1f5f9; padding: 4px 12px; border-radius: 4px; font-weight: bold; }
1340
+ .section-title { font-size: 0.85rem; text-transform: uppercase; color: #64748b; font-weight: 700; margin-top: 15px; margin-bottom: 5px; }
1341
+ .content-box { background: #f8fafc; padding: 12px; border-radius: 6px; border-left: 4px solid #cbd5e1; white-space: pre-wrap; margin-bottom: 10px; }
1342
+ .question-box { background: #fff7ed; padding: 15px; border-radius: 6px; border: 1px solid #ffedd5; border-left: 4px solid #f59e0b; margin-top: 20px; }
1343
+ .question-label { color: #c2410c; font-weight: 800; display: block; margin-bottom: 10px; font-size: 1.1rem; }
1344
+ .comment-item { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px dashed #fed7aa; }
1345
+ .comment-item:last-child { border-bottom: none; margin-bottom: 0; }
1346
+ .comment-meta { font-size: 0.75rem; color: #9a3412; font-weight: bold; }
1347
+ .no-print { margin-bottom: 20px; display: flex; gap: 10px; }
1348
+ button { padding: 10px 20px; cursor: pointer; background: #2563eb; color: white; border: none; border-radius: 5px; font-weight: bold; }
1349
+ @media print { .no-print { display: none; } body { margin: 0; padding: 0; } }
1350
+ </style>
1351
+ </head>
1352
+ <body>
1353
+ <div class="no-print">
1354
+ <button onclick="window.print()">Imprimer en PDF / Papier</button>
1355
+ <button onclick="window.close()" style="background: #64748b;">Fermer</button>
1356
+ </div>
1357
+
1358
+ <div class="header">
1359
+ <h1>Questions sur le Plan d'Actions</h1>
1360
+ <p>Site : ${siteName} (COID: ${coid})</p>
1361
+ <p>Date : ${new Date().toLocaleDateString()}</p>
1362
+ </div>
1363
+ `;
1364
+
1365
+ paEntries.forEach(([fieldId, conv]) => {
1366
+ const uuid = fieldId.replace('pa-', '');
1367
+ const item = checklistData.find(i => i.uuid === uuid);
1368
+ if (!item) return;
1369
+
1370
+ html += `
1371
+ <div class="item">
1372
+ <div class="item-header">
1373
+ <span class="req-num">Exigence ${item.requirementNumber}</span>
1374
+ <span class="score-badge">Score: ${item.score}</span>
1375
+ </div>
1376
+
1377
+ <div class="section-title">Constat d'audit (Rappel)</div>
1378
+ <div class="content-box">${item.explanation || '-'}</div>
1379
+
1380
+ <div class="section-title">Correction (saisie)</div>
1381
+ <div class="content-box">${item.correction || 'Non renseigné'}</div>
1382
+
1383
+ <div class="section-title">Action Corrective (saisie)</div>
1384
+ <div class="content-box">${item.correctiveAction || 'Non renseigné'}</div>
1385
+
1386
+ <div class="question-box">
1387
+ <span class="question-label">❓ QUESTION(S) DU REVIEWER :</span>
1388
+ ${conv.thread.map(m => `
1389
+ <div class="comment-item">
1390
+ <div class="comment-meta">${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'} - ${new Date(m.date).toLocaleString()}</div>
1391
+ <div class="comment-content">${m.content}</div>
1392
+ </div>
1393
+ `).join('')}
1394
+ </div>
1395
+ </div>
1396
+ `;
1397
+ });
1398
+
1399
+ html += `
1400
+ <div style="margin-top: 50px; text-align: center; color: #94a3b8; font-size: 0.85rem;">
1401
+ Document généré le ${new Date().toLocaleString()} via IFS Review Tool
1402
+ </div>
1403
+ </body>
1404
+ </html>`;
1405
+
1406
+ const printWindow = window.open('', '_blank');
1407
+ printWindow.document.write(html);
1408
+ printWindow.document.close();
1409
+ }
1410
+
1411
+ generateShortActionPlanPrintView() {
1412
+ const state = this.state.get();
1413
+ const conversations = state.conversations || {};
1414
+ const checklistData = state.checklistData || [];
1415
+ const profile = state.companyProfileData || {};
1416
+
1417
+ const paEntries = Object.entries(conversations).filter(([key, conv]) =>
1418
+ key.startsWith('pa-') && conv.thread && conv.thread.length > 0
1419
+ );
1420
+
1421
+ if (paEntries.length === 0) {
1422
+ this.uiManager.showError("Aucune question sur le Plan d'Actions n'a été trouvée.");
1423
+ return;
1424
+ }
1425
+
1426
+ const siteName = profile['Nom du site à auditer'] || 'Site';
1427
+
1428
+ let html = `
1429
+ <!DOCTYPE html>
1430
+ <html>
1431
+ <head>
1432
+ <title>Liste Questions Reviewer - ${siteName}</title>
1433
+ <style>
1434
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.5; color: #1e293b; max-width: 800px; margin: 40px auto; padding: 20px; }
1435
+ .no-print { margin-bottom: 20px; }
1436
+ h1 { font-size: 1.5rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px; }
1437
+ .q-item { margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #f1f5f9; }
1438
+ .q-header { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
1439
+ .q-num { font-weight: 800; color: #2563eb; font-size: 1.1rem; }
1440
+ .q-score { background: #f1f5f9; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 600; }
1441
+ .q-text { background: #fff7ed; border-left: 4px solid #f59e0b; padding: 10px 15px; font-style: italic; }
1442
+ .q-label { font-size: 0.75rem; text-transform: uppercase; color: #9a3412; font-weight: bold; margin-bottom: 4px; display: block; }
1443
+ button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: white; border: none; border-radius: 4px; }
1444
+ @media print { .no-print { display: none; } }
1445
+ </style>
1446
+ </head>
1447
+ <body>
1448
+ <div class="no-print">
1449
+ <button onclick="window.print()">Imprimer la liste</button>
1450
+ </div>
1451
+ <h1>Questions Reviewer - P.A. (Format court)</h1>
1452
+ <p style="margin-bottom: 30px;"><strong>Site :</strong> ${siteName}</p>
1453
+ `;
1454
+
1455
+ paEntries.forEach(([fieldId, conv]) => {
1456
+ const uuid = fieldId.replace('pa-', '');
1457
+ const item = checklistData.find(i => i.uuid === uuid);
1458
+ if (!item) return;
1459
+
1460
+ // Only get the latest reviewer comment for the "ultra simple" view
1461
+ const reviewerComments = conv.thread.filter(m => m.author === 'reviewer');
1462
+ const lastQuestion = reviewerComments.length > 0 ? reviewerComments[reviewerComments.length - 1].content : "Voir fil de discussion";
1463
+
1464
+ html += `
1465
+ <div class="q-item">
1466
+ <div class="q-header">
1467
+ <span class="q-num">Exigence ${item.requirementNumber}</span>
1468
+ <span class="q-score">Score: ${item.score}</span>
1469
+ </div>
1470
+ <div class="q-text">
1471
+ <span class="q-label">Question du Reviewer :</span>
1472
+ ${lastQuestion}
1473
+ </div>
1474
+ </div>
1475
+ `;
1476
+ });
1477
+
1478
+ html += `</body></html>`;
1479
+
1480
+ const printWindow = window.open('', '_blank');
1481
+ printWindow.document.write(html);
1482
+ printWindow.document.close();
1483
  }
1484
  }
index.html CHANGED
@@ -12,6 +12,7 @@
12
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
 
15
 
16
  <link rel="stylesheet" href="styles.css">
17
  </head>
@@ -282,6 +283,17 @@
282
  onclick="setAuditorTaskFilter('all')">
283
  <i class="fas fa-list"></i> Tout
284
  </button>
 
 
 
 
 
 
 
 
 
 
 
285
  </div>
286
  </div>
287
 
@@ -491,9 +503,10 @@
491
  <tr>
492
  <th style="width: 8%;">N° Exigence</th>
493
  <th style="width: 7%;">Score</th>
494
- <th style="width: 32%;">Explication</th>
495
- <th style="width: 33%;">Explication détaillée</th>
496
- <th style="width: 20%;">Revue : Statut commentaires</th>
 
497
  </tr>
498
  </thead>
499
  <tbody id="checklistTableBody"></tbody>
 
12
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.29/jspdf.plugin.autotable.min.js"></script>
16
 
17
  <link rel="stylesheet" href="styles.css">
18
  </head>
 
283
  onclick="setAuditorTaskFilter('all')">
284
  <i class="fas fa-list"></i> Tout
285
  </button>
286
+ <button class="btn btn-secondary btn-sm" style="margin-left: auto;"
287
+ onclick="printShortActionPlan()">
288
+ <i class="fas fa-list-ul"></i> Liste Simple
289
+ </button>
290
+ <button id="exportPAQuestionsBtn" class="btn btn-primary btn-sm"
291
+ onclick="printActionPlan()">
292
+ <i class="fas fa-print"></i> Fiche P.A.
293
+ </button>
294
+ <button class="btn btn-success btn-sm" onclick="exportActionPlanForSite()">
295
+ <i class="fas fa-file-excel"></i> Excel
296
+ </button>
297
  </div>
298
  </div>
299
 
 
503
  <tr>
504
  <th style="width: 8%;">N° Exigence</th>
505
  <th style="width: 7%;">Score</th>
506
+ <th style="width: 30%;">Explication</th>
507
+ <th style="width: 30%;">Explication détaillée</th>
508
+ <th style="width: 12%;">Revue : CONSTAT</th>
509
+ <th style="width: 13%;">Revue : PLAN D'ACTIONS</th>
510
  </tr>
511
  </thead>
512
  <tbody id="checklistTableBody"></tbody>
ui-manager.js CHANGED
@@ -115,7 +115,8 @@ class UIManager {
115
  this.setupKeyboardShortcuts();
116
 
117
  window.selectMode = (mode) => this.selectMode(mode);
118
- window.openCommentModal = (rowElement) => this.openCommentModal(rowElement);
 
119
  window.closeCommentModal = () => this.closeCommentModal();
120
  window.saveComment = () => this.saveComment();
121
  window.markAsResolved = () => this.markAsResolved();
@@ -146,6 +147,9 @@ class UIManager {
146
  window.openResetModal = () => this.openResetModal();
147
  window.closeResetModal = () => this.closeResetModal();
148
  window.confirmAppReset = () => this.confirmAppReset();
 
 
 
149
 
150
  window.addEventListener('beforeunload', function (event) {
151
  console.log('beforeunload triggered. Unsaved changes:', this.state.get().hasUnsavedChanges);
@@ -565,28 +569,48 @@ class UIManager {
565
  this.dataProcessor.refreshCountersForTab(tabId);
566
  }
567
 
568
- openCommentModal(rowElement) {
569
- if (!rowElement) return;
 
 
 
 
 
 
 
 
 
 
 
570
 
571
- const fieldId = rowElement.dataset.fieldId;
572
  if (!fieldId) return;
573
 
574
  this.currentFieldId = fieldId;
575
-
576
  const fieldInfo = this.dataProcessor.getFieldInfo(fieldId);
577
 
578
- document.getElementById('modalFieldName').textContent = `Commentaires - ${fieldInfo.name}`;
579
- document.getElementById('fieldDisplayContent').innerHTML = fieldInfo.content;
 
580
 
 
 
 
 
581
  this.loadConversationHistory(fieldId);
582
- this.renderHistoryTimeline(fieldId); // Appel pour afficher la timeline
583
 
 
 
 
 
584
  this.setupModalForCurrentMode(fieldId);
585
 
 
586
  this.setupDraftAutoSave(fieldId);
587
 
 
588
  document.getElementById('commentModal').classList.remove('hidden');
589
 
 
590
  setTimeout(() => {
591
  const textarea = document.getElementById('newCommentInput');
592
  if (textarea) textarea.focus();
 
115
  this.setupKeyboardShortcuts();
116
 
117
  window.selectMode = (mode) => this.selectMode(mode);
118
+ window.openCommentModal = (rowElementOrId) => this.openCommentModal(rowElementOrId);
119
+ window.openCommentModalFromCell = (cellElement, fieldId) => this.openCommentModalFromCell(cellElement, fieldId);
120
  window.closeCommentModal = () => this.closeCommentModal();
121
  window.saveComment = () => this.saveComment();
122
  window.markAsResolved = () => this.markAsResolved();
 
147
  window.openResetModal = () => this.openResetModal();
148
  window.closeResetModal = () => this.closeResetModal();
149
  window.confirmAppReset = () => this.confirmAppReset();
150
+ window.exportActionPlanForSite = () => this.fileHandler.exportActionPlanForSite();
151
+ window.printActionPlan = () => this.fileHandler.generateActionPlanPrintView();
152
+ window.printShortActionPlan = () => this.fileHandler.generateShortActionPlanPrintView();
153
 
154
  window.addEventListener('beforeunload', function (event) {
155
  console.log('beforeunload triggered. Unsaved changes:', this.state.get().hasUnsavedChanges);
 
569
  this.dataProcessor.refreshCountersForTab(tabId);
570
  }
571
 
572
+ openCommentModalFromCell(cellElement, fieldId) {
573
+ if (!fieldId) return;
574
+ this.currentFieldId = fieldId;
575
+ this.openCommentModal(fieldId);
576
+ }
577
+
578
+ openCommentModal(rowElementOrId) {
579
+ let fieldId = '';
580
+ if (typeof rowElementOrId === 'string') {
581
+ fieldId = rowElementOrId;
582
+ } else if (rowElementOrId && rowElementOrId.dataset) {
583
+ fieldId = rowElementOrId.dataset.fieldId;
584
+ }
585
 
 
586
  if (!fieldId) return;
587
 
588
  this.currentFieldId = fieldId;
 
589
  const fieldInfo = this.dataProcessor.getFieldInfo(fieldId);
590
 
591
+ // Update Modal Title and Content
592
+ const modalTitle = document.getElementById('modalFieldName');
593
+ const fieldDisplay = document.getElementById('fieldDisplayContent');
594
 
595
+ if (modalTitle) modalTitle.textContent = `Commentaires - ${fieldInfo.name}`;
596
+ if (fieldDisplay) fieldDisplay.innerHTML = fieldInfo.content;
597
+
598
+ // Load Conversation History
599
  this.loadConversationHistory(fieldId);
 
600
 
601
+ // Render History Timeline
602
+ this.renderHistoryTimeline(fieldId);
603
+
604
+ // Mode specific setup (placeholder, read-only, etc.)
605
  this.setupModalForCurrentMode(fieldId);
606
 
607
+ // Start Draft Auto-save for this specific field
608
  this.setupDraftAutoSave(fieldId);
609
 
610
+ // Show Modal
611
  document.getElementById('commentModal').classList.remove('hidden');
612
 
613
+ // Focus on textarea (slightly delayed for visibility)
614
  setTimeout(() => {
615
  const textarea = document.getElementById('newCommentInput');
616
  if (textarea) textarea.focus();