MMOON commited on
Commit
f327e06
·
verified ·
1 Parent(s): 73014cc

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +6 -4
  2. index.html +1616 -18
  3. prompts.txt +0 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Ifsneoreviewer
3
- emoji: 🚀
4
  colorFrom: blue
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: ifsneoreviewer
3
+ emoji: 🐳
4
  colorFrom: blue
5
+ colorTo: pink
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1617 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Outil de Communication Reviewer/Auditeur - IFS Audit</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <!-- JS Libraries for IFS Logic -->
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
12
+ <style>
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
14
+ body {
15
+ font-family: 'Inter', sans-serif;
16
+ }
17
+ .stat-card {
18
+ transition: all 0.3s ease;
19
+ cursor: pointer;
20
+ }
21
+ .stat-card:hover {
22
+ transform: translateY(-3px);
23
+ box-shadow: 0 10px 20px -5px rgba(0,0,0,0.1);
24
+ }
25
+ .sidebar {
26
+ transition: width 0.3s ease, transform 0.3s ease;
27
+ width: 16rem;
28
+ }
29
+ .sidebar-collapsed {
30
+ width: 4rem;
31
+ }
32
+ .sidebar-collapsed .sidebar-text {
33
+ display: none;
34
+ }
35
+ .sidebar-collapsed .justify-between {
36
+ justify-content: center;
37
+ }
38
+ .sidebar-collapsed .fa-chevron-left {
39
+ transform: rotate(180deg);
40
+ }
41
+
42
+ .nav-item:hover {
43
+ background-color: rgba(255, 255, 255, 0.1);
44
+ }
45
+ .progress-bar-custom {
46
+ height: 6px;
47
+ border-radius: 3px;
48
+ background-color: #e2e8f0;
49
+ }
50
+ .progress-fill-custom {
51
+ height: 100%;
52
+ border-radius: 3px;
53
+ transition: width 0.3s ease;
54
+ }
55
+ .tab-content-custom {
56
+ display: none;
57
+ }
58
+ .tab-content-custom.active {
59
+ display: block;
60
+ animation: fadeIn 0.3s ease;
61
+ }
62
+ @keyframes fadeIn {
63
+ from { opacity: 0; }
64
+ to { opacity: 1; }
65
+ }
66
+
67
+ .sticky-header-custom {
68
+ position: sticky;
69
+ top: 0;
70
+ z-index: 10;
71
+ }
72
+
73
+ /* Styles for JS-generated content */
74
+ .stat-card {
75
+ background: white;
76
+ padding: 1.25rem;
77
+ border-radius: 0.5rem;
78
+ text-align: center;
79
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
80
+ transition: transform 0.3s ease;
81
+ }
82
+ .stat-card:hover { transform: translateY(-2px); }
83
+ .stat-number { font-size: 2.2rem; font-weight: 700; color: #2a5298; margin-bottom: 0.5rem; }
84
+ .stat-label { font-size: 0.9rem; color: #6c757d; font-weight: 600; }
85
+
86
+ /* Styles pour les tableaux redimensionnables */
87
+ .data-table-custom {
88
+ width: 100%;
89
+ border-collapse: collapse;
90
+ margin-top: 1.25rem;
91
+ border-radius: 0.75rem;
92
+ overflow: hidden;
93
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
94
+ font-size: 0.9rem;
95
+ table-layout: fixed; /* Important pour le redimensionnement */
96
+ }
97
+
98
+ .data-table-custom th, .data-table-custom td {
99
+ padding: 0.75rem;
100
+ text-align: left;
101
+ border-bottom: 1px solid #e5e7eb;
102
+ border-right: 1px solid #e5e7eb;
103
+ word-wrap: break-word;
104
+ overflow-wrap: break-word;
105
+ position: relative;
106
+ }
107
+
108
+ .data-table-custom th {
109
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
110
+ color: white;
111
+ font-weight: 600;
112
+ position: sticky;
113
+ top: 0;
114
+ user-select: none;
115
+ }
116
+
117
+ /* Poignée de redimensionnement */
118
+ .data-table-custom th::after {
119
+ content: '';
120
+ position: absolute;
121
+ top: 0;
122
+ right: 0;
123
+ width: 5px;
124
+ height: 100%;
125
+ cursor: col-resize;
126
+ background: transparent;
127
+ border-right: 2px solid rgba(255,255,255,0.3);
128
+ }
129
+
130
+ .data-table-custom th:hover::after {
131
+ background: rgba(255,255,255,0.1);
132
+ }
133
+
134
+ .data-table-custom th:last-child::after {
135
+ display: none;
136
+ }
137
+
138
+ .data-table-custom tr:hover {
139
+ background: #f9fafb;
140
+ }
141
+
142
+ .dark .data-table-custom tr:hover {
143
+ background: #374151;
144
+ }
145
+
146
+ .dark .data-table-custom th {
147
+ background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
148
+ }
149
+
150
+ .dark .data-table-custom td {
151
+ border-bottom-color: #4b5563;
152
+ border-right-color: #4b5563;
153
+ }
154
+
155
+ /* Curseur de redimensionnement global */
156
+ .resizing {
157
+ cursor: col-resize !important;
158
+ }
159
+
160
+ .resizing * {
161
+ cursor: col-resize !important;
162
+ }
163
+
164
+ .score-badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-weight: 700; font-size: 0.75rem; color: white; }
165
+ .score-A { background: #28a745; }
166
+ .score-B { background: #ffc107; color: #212529; }
167
+ .score-C { background: #fd7e14; }
168
+ .score-D { background: #dc3545; }
169
+ .score-NA { background: #6c757d; }
170
+
171
+ .category-header-custom {
172
+ background: linear-gradient(135deg, #2a5298 0%, #1e3c72 100%);
173
+ color: white;
174
+ padding: 0.75rem 1rem;
175
+ margin: 1.25rem 0 0.625rem 0;
176
+ border-radius: 0.5rem;
177
+ font-weight: bold;
178
+ font-size: 1.1rem;
179
+ }
180
+ .dark .category-header-custom { background: linear-gradient(135deg, #374151 0%, #1f2937 100%);}
181
+
182
+ .profile-textarea-long, .profile-textarea-short {
183
+ width: 100%; border: 1px solid #d1d5db;
184
+ border-radius: 0.25rem; padding: 0.5rem;
185
+ font-family: inherit; resize: vertical;
186
+ }
187
+ .dark .profile-textarea-long, .dark .profile-textarea-short {
188
+ background-color: #374151;
189
+ border-color: #4b5563;
190
+ color: #e5e7eb;
191
+ }
192
+
193
+ .profile-textarea-long { min-height: 80px; }
194
+ .profile-textarea-short { min-height: 40px; }
195
+
196
+ .value-display-long { max-height: 100px; overflow-y: auto; padding: 0.5rem; background: #f9fafb; border-radius: 0.25rem; word-wrap: break-word; font-size: 0.9rem; line-height: 1.4; }
197
+ .value-display-short { word-wrap: break-word; font-size: 0.9rem; }
198
+ .dark .value-display-long { background: #374151; }
199
+
200
+ .main-content {
201
+ margin-left: 16rem;
202
+ transition: margin-left 0.3s ease;
203
+ }
204
+ .main-content-collapsed {
205
+ margin-left: 4rem;
206
+ }
207
+
208
+ @media (max-width: 768px) {
209
+ .sidebar {
210
+ position: fixed;
211
+ z-index: 40;
212
+ height: 100vh;
213
+ transform: translateX(-100%);
214
+ }
215
+ .sidebar.active {
216
+ transform: translateX(0);
217
+ width: 16rem;
218
+ }
219
+ .sidebar.sidebar-collapsed {
220
+ transform: translateX(-100%);
221
+ }
222
+ .sidebar.sidebar-collapsed.active {
223
+ transform: translateX(0);
224
+ width: 4rem;
225
+ }
226
+ .main-content, .main-content-collapsed {
227
+ margin-left: 0 !important;
228
+ }
229
+ }
230
+ </style>
231
+ </head>
232
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
233
+ <!-- Dark Mode Toggle -->
234
+ <div class="fixed bottom-4 right-4 z-50">
235
+ <button id="darkModeToggle" class="p-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 transition">
236
+ <i class="fas fa-moon"></i>
237
+ </button>
238
+ </div>
239
+
240
+ <!-- Nouvelle section pour le suivi des actions -->
241
+ <div id="actions" class="tab-content-custom">
242
+ <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Suivi des Actions Correctives</h2>
243
+ <div id="actionItemsContainer">
244
+ <!-- Généré dynamiquement -->
245
+ </div>
246
+ <button onclick="exportActionPlan()" class="mt-4 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition">
247
+ <i class="fas fa-file-export mr-2"></i>Exporter Plan d'Actions
248
+ </button>
249
+ </div>
250
+
251
+ <!-- Sidebar -->
252
+ <div class="sidebar bg-blue-800 text-white fixed h-full overflow-y-auto">
253
+ <div class="p-4 flex items-center justify-between border-b border-blue-700">
254
+ <div class="flex items-center space-x-2">
255
+ <i class="fas fa-shield-alt text-xl"></i>
256
+ <span class="font-bold text-lg sidebar-text">IFS Review Tool</span>
257
+ </div>
258
+ <button id="sidebarCollapse" class="text-white hover:text-blue-200 focus:outline-none">
259
+ <i class="fas fa-chevron-left"></i>
260
+ </button>
261
+ </div>
262
+ <div class="p-4">
263
+ <div class="mb-6">
264
+ <h3 class="text-xs uppercase font-semibold text-blue-200 mb-2 sidebar-text">Navigation</h3>
265
+ <ul class="space-y-1">
266
+ <li class="nav-item">
267
+ <a href="#profil-section" class="nav-link block px-3 py-2 rounded-lg flex items-center space-x-3 text-blue-100 hover:text-white" data-tab-target="profil">
268
+ <i class="fas fa-user-tie w-5 text-center"></i>
269
+ <span class="sidebar-text">Profil Entreprise</span>
270
+ </a>
271
+ </li>
272
+ <li class="nav-item">
273
+ <a href="#checklist-section" class="nav-link block px-3 py-2 rounded-lg flex items-center space-x-3 text-blue-100 hover:text-white" data-tab-target="checklist">
274
+ <i class="fas fa-list-check w-5 text-center"></i>
275
+ <span class="sidebar-text">Checklist Complète</span>
276
+ </a>
277
+ </li>
278
+ <li class="nav-item">
279
+ <a href="#nonconformites-section" class="nav-link block px-3 py-2 rounded-lg flex items-center space-x-3 text-blue-100 hover:text-white" data-tab-target="nonconformites">
280
+ <i class="fas fa-exclamation-triangle w-5 text-center"></i>
281
+ <span class="sidebar-text">Non-Conformités</span>
282
+ </a>
283
+ </li>
284
+ </ul>
285
+ </div>
286
+ <div class="mb-6">
287
+ <h3 class="text-xs uppercase font-semibold text-blue-200 mb-2 sidebar-text">Actions Fichier</h3>
288
+ <div class="space-y-2">
289
+ <button id="newAuditBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg flex items-center space-x-3 transition justify-center md:justify-start">
290
+ <i class="fas fa-file-medical w-5 text-center"></i>
291
+ <span class="sidebar-text">Nouveau Dossier</span>
292
+ </button>
293
+ <input type="file" id="fileInputInternal" accept=".ifs,.ifsr" style="display: none;">
294
+ <button id="loadAuditBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg flex items-center space-x-3 transition justify-center md:justify-start">
295
+ <i class="fas fa-upload w-5 text-center"></i>
296
+ <span class="sidebar-text">Charger Fichier</span>
297
+ </button>
298
+ <button id="saveAuditBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg flex items-center space-x-3 transition justify-center md:justify-start">
299
+ <i class="fas fa-save w-5 text-center"></i>
300
+ <span class="sidebar-text">Sauvegarder IFSR</span>
301
+ </button>
302
+ <button id="exportExcelBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg flex items-center space-x-3 transition justify-center md:justify-start">
303
+ <i class="fas fa-file-excel w-5 text-center"></i>
304
+ <span class="sidebar-text">Exporter pour Auditeur</span>
305
+ </button>
306
+ <button id="exportAllDataBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg flex items-center space-x-3 transition justify-center md:justify-start mt-2">
307
+ <i class="fas fa-archive w-5 text-center"></i>
308
+ <span class="sidebar-text">Export Brut Complet</span>
309
+ </button>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- Mobile Sidebar Toggle -->
316
+ <button id="mobileSidebarToggle" class="md:hidden fixed top-4 left-4 z-30 bg-blue-600 text-white p-2 rounded-lg shadow-lg">
317
+ <i class="fas fa-bars"></i>
318
+ </button>
319
+
320
+ <!-- Main Content -->
321
+ <div class="main-content min-h-screen">
322
+ <!-- Header -->
323
+ <header class="bg-white dark:bg-gray-800 shadow-sm sticky-header-custom">
324
+ <div class="container mx-auto px-4 py-3 flex items-center justify-between">
325
+ <div class="flex items-center">
326
+ <button id="mobileSidebarToggleHeader" class="md:hidden text-gray-600 dark:text-gray-300 mr-3">
327
+ <i class="fas fa-bars text-xl"></i>
328
+ </button>
329
+ <h1 class="text-xl md:text-2xl font-bold text-blue-800 dark:text-blue-300">IFS Reviewer - <span id="currentAuditName">Aucun audit chargé</span></h1>
330
+ </div>
331
+ <div class="flex items-center space-x-4">
332
+ <div id="sessionInfoTop" class="text-sm text-gray-600 dark:text-gray-400">
333
+ <span id="storageTypeTop">Stockage: Fichier</span> | <span id="sessionIdTop">COID: ----</span>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </header>
338
+
339
+ <!-- Main Section for IFS Tool -->
340
+ <main class="container mx-auto px-4 py-6">
341
+ <!-- Tab Content Area -->
342
+ <div id="profil" class="tab-content-custom active">
343
+ <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Profil de l'entreprise auditée</h2>
344
+ <div id="uploadZone" class="border-3 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-10 text-center mb-6 cursor-pointer hover:border-blue-500 dark:hover:border-blue-400 bg-white dark:bg-gray-800">
345
+ <div class="text-5xl text-gray-400 dark:text-gray-500 mb-4"><i class="fas fa-file-upload"></i></div>
346
+ <h3 class="text-xl font-semibold mb-2 text-gray-700 dark:text-gray-300">Démarrer votre travail de review</h3>
347
+ <p class="text-gray-600 dark:text-gray-400"><strong>Nouveau dossier :</strong> Glissez-déposez votre fichier <strong>.ifs</strong> (export NEO)</p>
348
+ <p class="text-gray-600 dark:text-gray-400"><strong>Reprendre travail :</strong> Glissez-déposez votre fichier <strong>.ifsr</strong> (travail sauvegardé)</p>
349
+ <p class="mt-4 text-sm text-gray-500 dark:text-gray-400">ou cliquez ici pour sélectionner un fichier</p>
350
+ <input type="file" id="fileInput" accept=".ifs,.ifsr" style="display: none;">
351
+ </div>
352
+
353
+ <div id="loading" class="text-center p-10 hidden">
354
+ <div class="spinner animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div>
355
+ <p class="text-gray-700 dark:text-gray-300">Traitement et sauvegarde du fichier en cours...</p>
356
+ <div class="progress-bar-custom w-full mt-3">
357
+ <div id="progressFill" class="progress-fill-custom bg-blue-500" style="width: 0%"></div>
358
+ </div>
359
+ </div>
360
+
361
+ <div id="profilResults" class="hidden">
362
+ <div id="successAlert" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
363
+ </div>
364
+
365
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
366
+ <div class="stat-card dark:bg-gray-800">
367
+ <div class="stat-number dark:text-blue-400" id="totalRequirements">0</div>
368
+ <div class="stat-label dark:text-gray-400">Exigences totales</div>
369
+ </div>
370
+ <div class="stat-card dark:bg-gray-800">
371
+ <div class="stat-number dark:text-green-400" id="conformCount">0</div>
372
+ <div class="stat-label dark:text-gray-400">Conformités (A)</div>
373
+ </div>
374
+ <div class="stat-card dark:bg-gray-800">
375
+ <div class="stat-number dark:text-red-400" id="nonConformCount">0</div>
376
+ <div class="stat-label dark:text-gray-400">Non-Conformités</div>
377
+ </div>
378
+ <div class="stat-card dark:bg-gray-800">
379
+ <div class="stat-number dark:text-blue-400" id="overallScore">0%</div>
380
+ <div class="stat-label dark:text-gray-400">Score global</div>
381
+ </div>
382
+ <div class="stat-card bg-gradient-to-r from-green-500 to-teal-500 text-white">
383
+ <div class="stat-number" id="progressPercentage">0%</div>
384
+ <div class="stat-label">Travail reviewer</div>
385
+ </div>
386
+ <div class="stat-card bg-gradient-to-r from-blue-500 to-indigo-500 text-white">
387
+ <div class="stat-number" id="commentsCount">0</div>
388
+ <div class="stat-label">Commentaires</div>
389
+ </div>
390
+ </div>
391
+
392
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
393
+ <h3 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Informations de l'entreprise</h3>
394
+ <div id="companyProfileTable">
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <div id="checklist" class="tab-content-custom">
401
+ <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Checklist complète des exigences IFS</h2>
402
+ <div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow mb-6">
403
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
404
+ <div>
405
+ <label for="chapterFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Chapitre IFS</label>
406
+ <select id="chapterFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
407
+ <option value="">Tous</option>
408
+ <option value="1">1. Management</option>
409
+ <option value="2">2. Système management</option>
410
+ <option value="3">3. Ressources</option>
411
+ <option value="4">4. Planification</option>
412
+ <option value="5">5. Mesures et analyses</option>
413
+ <option value="6">6. Food Defense</option>
414
+ </select>
415
+ </div>
416
+ <div>
417
+ <label for="scoreFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Score</label>
418
+ <select id="scoreFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
419
+ <option value="">Tous</option>
420
+ <option value="A">A</option> <option value="B">B</option> <option value="C">C</option> <option value="D">D</option> <option value="NA">NA</option>
421
+ </select>
422
+ </div>
423
+ <div>
424
+ <label for="explanationFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Explications</label>
425
+ <select id="explanationFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
426
+ <option value="empty">Sans explications</option> <option value="with">Avec explications</option> <option value="all">Toutes</option>
427
+ </select>
428
+ </div>
429
+ <div>
430
+ <label for="searchInput" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Recherche</label>
431
+ <input type="text" id="searchInput" placeholder="Mots-clés..." class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
432
+ </div>
433
+ </div>
434
+ <div class="mt-4 flex justify-end">
435
+ <button onclick="showAll()" class="px-4 py-2 bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition">Afficher Tout</button>
436
+ </div>
437
+ </div>
438
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-x-auto">
439
+ <table class="data-table-custom w-full" id="checklistTable">
440
+ <thead><tr><th>N° Exigence</th><th>Score</th><th>Explication</th><th>Explication détaillée</th><th>Commentaires Reviewer</th></tr></thead>
441
+ <tbody id="checklistTableBody"></tbody>
442
+ </table>
443
+ </div>
444
+ </div>
445
+
446
+ <div id="nonconformites" class="tab-content-custom">
447
+ <h2 class="text-2xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Non-Conformités & Non-Applicables</h2>
448
+ <div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow mb-6">
449
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
450
+ <div>
451
+ <label for="ncTypeFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type</label>
452
+ <select id="ncTypeFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
453
+ <option value="">Tous</option> <option value="B,C,D">NC seulement</option> <option value="NA">NA seulement</option>
454
+ <option value="B">B</option> <option value="C">C</option> <option value="D">D</option>
455
+ </select>
456
+ </div>
457
+ <div>
458
+ <label for="ncChapterFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Chapitre</label>
459
+ <select id="ncChapterFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
460
+ <option value="">Tous</option>
461
+ <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6">6</option>
462
+ </select>
463
+ </div>
464
+ <div>
465
+ <label for="correctionFilter" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Commentaires</label>
466
+ <select id="correctionFilter" class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
467
+ <option value="">Tous</option> <option value="with">Avec commentaires</option> <option value="without">Sans commentaires</option>
468
+ </select>
469
+ </div>
470
+ <div>
471
+ <label for="ncSearchInput" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Recherche</label>
472
+ <input type="text" id="ncSearchInput" placeholder="Mots-clés..." class="mt-1 block w-full py-2 px-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
473
+ </div>
474
+ </div>
475
+ </div>
476
+ <div id="nonConformitiesStats" class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
477
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-blue-400" id="totalNC">0</div><div class="stat-label dark:text-gray-400">Total NC + NA</div></div>
478
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-yellow-400" id="scoreB">0</div><div class="stat-label dark:text-gray-400">Score B</div></div>
479
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-orange-400" id="scoreC">0</div><div class="stat-label dark:text-gray-400">Score C</div></div>
480
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-red-400" id="scoreD">0</div><div class="stat-label dark:text-gray-400">Score D</div></div>
481
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-gray-400" id="naCount">0</div><div class="stat-label dark:text-gray-400">Non-Applicables</div></div>
482
+ <div class="stat-card dark:bg-gray-800"><div class="stat-number dark:text-purple-400" id="commentsNCCount">0</div><div class="stat-label dark:text-gray-400">Commentaires NC/NA</div></div>
483
+ </div>
484
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-x-auto">
485
+ <table class="data-table-custom w-full" id="nonConformitiesTable">
486
+ <thead><tr><th>N° Exigence</th><th>Score</th><th>Explication</th><th>Explication détaillée</th><th>Commentaires Reviewer</th></tr></thead>
487
+ <tbody id="nonConformitiesTableBody"></tbody>
488
+ </table>
489
+ </div>
490
+ </div>
491
+ </main>
492
+ </div>
493
+
494
+ <script>
495
+ // --- Variables globales pour le redimensionnement ---
496
+ let isResizing = false;
497
+ let currentTable = null;
498
+ let currentColumn = null;
499
+ let startX = 0;
500
+ let startWidth = 0;
501
+
502
+ // --- Sidebar and Dark Mode logic ---
503
+ const sidebar = document.querySelector('.sidebar');
504
+ const sidebarCollapse = document.getElementById('sidebarCollapse');
505
+ const mobileSidebarToggle = document.getElementById('mobileSidebarToggle');
506
+ const mobileSidebarToggleHeader = document.getElementById('mobileSidebarToggleHeader');
507
+ const mainContent = document.querySelector('.main-content');
508
+ const sidebarTexts = document.querySelectorAll('.sidebar-text');
509
+
510
+ sidebarCollapse.addEventListener('click', () => {
511
+ sidebar.classList.toggle('sidebar-collapsed');
512
+ mainContent.classList.toggle('main-content-collapsed');
513
+ });
514
+
515
+ function toggleMobileSidebar() {
516
+ sidebar.classList.toggle('active');
517
+ }
518
+ mobileSidebarToggle.addEventListener('click', toggleMobileSidebar);
519
+ if(mobileSidebarToggleHeader) mobileSidebarToggleHeader.addEventListener('click', toggleMobileSidebar);
520
+
521
+ document.getElementById('darkModeToggle').addEventListener('click', () => {
522
+ document.documentElement.classList.toggle('dark');
523
+ const icon = document.getElementById('darkModeToggle').querySelector('i');
524
+ icon.classList.toggle('fa-moon');
525
+ icon.classList.toggle('fa-sun');
526
+ localStorage.setItem('darkMode', document.documentElement.classList.contains('dark'));
527
+ });
528
+
529
+ if (localStorage.getItem('darkMode') === 'true' ||
530
+ (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
531
+ document.documentElement.classList.add('dark');
532
+ document.getElementById('darkModeToggle').querySelector('i').classList.replace('fa-moon', 'fa-sun');
533
+ }
534
+
535
+ // --- Fonctions de redimensionnement des colonnes ---
536
+ function initColumnResize() {
537
+ const tables = document.querySelectorAll('.data-table-custom');
538
+
539
+ tables.forEach(table => {
540
+ const headers = table.querySelectorAll('th');
541
+
542
+ headers.forEach((header, index) => {
543
+ if (index < headers.length - 1) { // Pas pour la dernière colonne
544
+ header.addEventListener('mousedown', (e) => {
545
+ const rect = header.getBoundingClientRect();
546
+ if (e.clientX >= rect.right - 10) { // Zone de 10px depuis le bord droit
547
+ startResize(e, table, index);
548
+ }
549
+ });
550
+
551
+ header.addEventListener('mousemove', (e) => {
552
+ const rect = header.getBoundingClientRect();
553
+ if (e.clientX >= rect.right - 10) {
554
+ header.style.cursor = 'col-resize';
555
+ } else {
556
+ header.style.cursor = 'default';
557
+ }
558
+ });
559
+ }
560
+ });
561
+ });
562
+
563
+ // Événements globaux pour le redimensionnement
564
+ document.addEventListener('mousemove', handleResize);
565
+ document.addEventListener('mouseup', stopResize);
566
+ }
567
+
568
+ function startResize(e, table, columnIndex) {
569
+ isResizing = true;
570
+ currentTable = table;
571
+ currentColumn = columnIndex;
572
+ startX = e.clientX;
573
+
574
+ const header = table.querySelectorAll('th')[columnIndex];
575
+ startWidth = header.offsetWidth;
576
+
577
+ document.body.classList.add('resizing');
578
+ e.preventDefault();
579
+ }
580
+
581
+ function handleResize(e) {
582
+ if (!isResizing) return;
583
+
584
+ const diff = e.clientX - startX;
585
+ const newWidth = Math.max(50, startWidth + diff); // Largeur minimum de 50px
586
+
587
+ // Appliquer la nouvelle largeur
588
+ const headers = currentTable.querySelectorAll('th');
589
+ const cells = currentTable.querySelectorAll('td');
590
+
591
+ headers[currentColumn].style.width = newWidth + 'px';
592
+
593
+ // Appliquer à toutes les cellules de cette colonne
594
+ const rows = currentTable.querySelectorAll('tr');
595
+ rows.forEach(row => {
596
+ const cell = row.cells[currentColumn];
597
+ if (cell) {
598
+ cell.style.width = newWidth + 'px';
599
+ }
600
+ });
601
+ }
602
+
603
+ function stopResize() {
604
+ if (isResizing) {
605
+ isResizing = false;
606
+ currentTable = null;
607
+ currentColumn = null;
608
+ document.body.classList.remove('resizing');
609
+ }
610
+ }
611
+
612
+ // --- Tab Navigation ---
613
+ const navLinks = document.querySelectorAll('.nav-link');
614
+ const tabContents = document.querySelectorAll('.tab-content-custom');
615
+
616
+ navLinks.forEach(link => {
617
+ link.addEventListener('click', (e) => {
618
+ e.preventDefault();
619
+ const targetTabId = link.dataset.tabTarget;
620
+
621
+ navLinks.forEach(l => {
622
+ l.classList.remove('bg-blue-700', 'dark:bg-blue-700');
623
+ l.classList.add('hover:text-white');
624
+ });
625
+ link.classList.add('bg-blue-700', 'dark:bg-blue-700');
626
+ link.classList.remove('hover:text-white');
627
+
628
+ tabContents.forEach(content => {
629
+ content.classList.remove('active');
630
+ });
631
+ const targetContent = document.getElementById(targetTabId);
632
+ if (targetContent) {
633
+ targetContent.classList.add('active');
634
+ if (targetTabId === 'checklist' && checklistData.length > 0) renderChecklistTable();
635
+ if (targetTabId === 'nonconformites' && checklistData.length > 0) renderNonConformitiesTable();
636
+ if (targetTabId === 'profil' && Object.keys(companyProfileData).length > 0) renderCompanyProfile();
637
+
638
+ // Réinitialiser le redimensionnement pour le nouveau tableau
639
+ setTimeout(() => {
640
+ initColumnResize();
641
+ }, 100);
642
+ }
643
+ if (sidebar.classList.contains('active')) {
644
+ toggleMobileSidebar();
645
+ }
646
+ });
647
+ });
648
+
649
+ function setDefaultTab() {
650
+ const defaultLink = document.querySelector('.nav-link[data-tab-target="profil"]');
651
+ if (defaultLink) {
652
+ defaultLink.classList.add('bg-blue-700', 'dark:bg-blue-700');
653
+ defaultLink.classList.remove('hover:text-white');
654
+ }
655
+ const defaultContent = document.getElementById('profil');
656
+ if (defaultContent) {
657
+ defaultContent.classList.add('active');
658
+ }
659
+ }
660
+
661
+ // --- IFS Logic ---
662
+ let auditData = null;
663
+ let checklistData = [];
664
+ let companyProfileData = {};
665
+ let comments = {};
666
+ let requirementNumberMapping = {};
667
+ let currentSession = { id: null, name: '', created: null, lastModified: null, data: null };
668
+
669
+ document.getElementById('newAuditBtn').addEventListener('click', createNewSession);
670
+ document.getElementById('saveAuditBtn').addEventListener('click', saveWorkInProgress);
671
+ document.getElementById('loadAuditBtn').addEventListener('click', () => document.getElementById('fileInputInternal').click());
672
+ document.getElementById('fileInputInternal').addEventListener('change', handleFileUpload);
673
+ document.getElementById('exportExcelBtn').addEventListener('click', exportForAuditor);
674
+ document.getElementById('exportAllDataBtn').addEventListener('click', exportAllData);
675
+
676
+ const uploadZone = document.getElementById('uploadZone');
677
+ const fileInput = document.getElementById('fileInput');
678
+ const loadingElement = document.getElementById('loading');
679
+ const profilResultsElement = document.getElementById('profilResults');
680
+ const successAlertElement = document.getElementById('successAlert');
681
+
682
+ if(uploadZone) {
683
+ uploadZone.addEventListener('click', () => document.getElementById('fileInputInternal').click());
684
+ uploadZone.addEventListener('dragover', (e) => {
685
+ e.preventDefault();
686
+ uploadZone.classList.add('border-blue-500', 'dark:border-blue-400', 'bg-gray-100', 'dark:bg-gray-700');
687
+ });
688
+ uploadZone.addEventListener('dragleave', () => {
689
+ uploadZone.classList.remove('border-blue-500', 'dark:border-blue-400', 'bg-gray-100', 'dark:bg-gray-700');
690
+ });
691
+ uploadZone.addEventListener('drop', (e) => {
692
+ e.preventDefault();
693
+ uploadZone.classList.remove('border-blue-500', 'dark:border-blue-400', 'bg-gray-100', 'dark:bg-gray-700');
694
+ const files = e.dataTransfer.files;
695
+ if (files.length > 0) {
696
+ document.getElementById('fileInputInternal').files = files;
697
+ handleFileUpload({ target: { files: files } });
698
+ }
699
+ });
700
+ }
701
+
702
+ function switchToolTab(tabName) {
703
+ console.log('Switching tool tab to:', tabName);
704
+ document.querySelectorAll('.tab-content-custom').forEach(tab => tab.classList.remove('active'));
705
+ document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('bg-blue-700', 'dark:bg-blue-700'));
706
+
707
+ const targetTabElement = document.getElementById(tabName);
708
+ const targetNavLink = document.querySelector(`.nav-link[data-tab-target="${tabName}"]`);
709
+
710
+ if (targetTabElement) targetTabElement.classList.add('active');
711
+ if (targetNavLink) targetNavLink.classList.add('bg-blue-700', 'dark:bg-blue-700');
712
+
713
+ if (auditData) {
714
+ if (tabName === 'profil') renderCompanyProfile();
715
+ else if (tabName === 'checklist') renderChecklistTable();
716
+ else if (tabName === 'nonconformites') renderNonConformitiesTable();
717
+ }
718
+
719
+ setTimeout(() => {
720
+ initColumnResize();
721
+ }, 100);
722
+ }
723
+
724
+ function handleFileUpload(event) {
725
+ console.log('File upload started via internal input');
726
+ const file = event.target.files[0];
727
+ if (!file) return;
728
+
729
+ const fileName = file.name.toLowerCase();
730
+ const isIFSFile = fileName.endsWith('.ifs');
731
+ const isIFSRFile = fileName.endsWith('.ifsr');
732
+
733
+ if (!isIFSFile && !isIFSRFile) {
734
+ alert('❌ Type de fichier non supporté !\n\n✅ Formats acceptés :\n• .ifs (nouveau dossier depuis NEO)\n• .ifsr (travail en cours sauvegardé)');
735
+ return;
736
+ }
737
+
738
+ if (loadingElement) loadingElement.classList.remove('hidden');
739
+ if (uploadZone) uploadZone.classList.add('hidden');
740
+ if (profilResultsElement) profilResultsElement.classList.add('hidden');
741
+ simulateProgress();
742
+
743
+ const reader = new FileReader();
744
+ reader.onload = function(e) {
745
+ try {
746
+ const content = e.target.result;
747
+ const data = JSON.parse(content);
748
+ if (isIFSFile) processNewIFSFile(data);
749
+ else loadWorkInProgress(data);
750
+ } catch (error) {
751
+ console.error('Error during file processing:', error);
752
+ showError(`Erreur lors du traitement du fichier ${isIFSFile ? 'IFS' : 'IFSR'} : ` + error.message);
753
+ if (loadingElement) loadingElement.classList.add('hidden');
754
+ if (uploadZone) uploadZone.classList.remove('hidden');
755
+ if (profilResultsElement) profilResultsElement.classList.add('hidden');
756
+ } finally {
757
+ event.target.value = null;
758
+ }
759
+ };
760
+ reader.onerror = function(error) {
761
+ console.error('File reading error:', error);
762
+ showError('Erreur lors de la lecture du fichier');
763
+ if (loadingElement) loadingElement.classList.add('hidden');
764
+ if (uploadZone) uploadZone.classList.remove('hidden');
765
+ if (profilResultsElement) profilResultsElement.classList.add('hidden');
766
+ event.target.value = null;
767
+ };
768
+ reader.readAsText(file);
769
+ }
770
+
771
+ function simulateProgress() {
772
+ let progress = 0;
773
+ const progressFill = document.getElementById('progressFill');
774
+ if (!progressFill) {
775
+ setTimeout(() => {
776
+ if (loadingElement) loadingElement.classList.add('hidden');
777
+ if (auditData && profilResultsElement) profilResultsElement.classList.remove('hidden');
778
+ }, 1000);
779
+ return;
780
+ }
781
+ const interval = setInterval(() => {
782
+ progress += Math.random() * 15;
783
+ if (progress >= 100) {
784
+ progress = 100;
785
+ clearInterval(interval);
786
+ setTimeout(() => { if (progressFill) progressFill.style.width = '0%'; }, 500);
787
+ }
788
+ if (progressFill) progressFill.style.width = progress + '%';
789
+ }, 200);
790
+ }
791
+
792
+ function processNewIFSFile(data) {
793
+ auditData = data;
794
+ checklistData = []; companyProfileData = {}; comments = {}; requirementNumberMapping = {};
795
+ currentSession = { id: `IFS-${Date.now()}`, name: 'Nouvel Audit', created: new Date(), lastModified: new Date(), data: auditData };
796
+ processAuditDataLogic(auditData);
797
+ }
798
+
799
+ function loadWorkInProgress(workData) {
800
+ try {
801
+ if (!workData.auditData || !workData.version) throw new Error('Format de fichier IFSR invalide');
802
+ auditData = workData.auditData;
803
+ checklistData = workData.checklistData || [];
804
+ companyProfileData = workData.companyProfileData || {};
805
+ comments = workData.comments || {};
806
+ requirementNumberMapping = workData.requirementNumberMapping || {};
807
+ currentSession = {
808
+ id: workData.coid ? `IFS-${workData.coid}-${new Date(workData.savedDate).getTime()}` : `IFS-Loaded-${Date.now()}`,
809
+ name: `Audit ${workData.companyName || 'Inconnu'} (rechargé)`,
810
+ created: workData.savedDate ? new Date(workData.savedDate) : new Date(),
811
+ lastModified: new Date(), data: auditData
812
+ };
813
+
814
+ const companyName = companyProfileData['Nom du site à auditer'] || workData.companyName || 'Société inconnue';
815
+ const coid = companyProfileData['N° COID du portail'] || workData.coid || 'COID-XXXX';
816
+ const totalComments = Object.keys(comments).filter(key => comments[key]?.[0]?.content.trim() !== '').length;
817
+
818
+ updateSessionIdInUI(coid);
819
+ document.getElementById('currentAuditName').textContent = `IFS Reviewer - ${companyName}`;
820
+
821
+ if (auditData?.data?.modules?.food_8?.result?.overall) {
822
+ const overallResult = auditData.data.modules.food_8.result.overall;
823
+ updateElementTextContent('overallScore', overallResult.percent.toFixed(1) + '%');
824
+ }
825
+ updateElementTextContent('totalRequirements', checklistData.length);
826
+ updateElementTextContent('conformCount', checklistData.filter(item => item.score === 'A').length);
827
+ updateElementTextContent('nonConformCount', checklistData.filter(item => ['B', 'C', 'D'].includes(item.score)).length);
828
+
829
+ if(successAlertElement) successAlertElement.textContent = `📂 Travail rechargé : ${companyName} (${totalComments} commentaires sauvegardés)`;
830
+
831
+ finalizeUIUpdateAndRender();
832
+ alert(`✅ Travail chargé: ${companyName}\nCOID: ${coid}\nCommentaires: ${totalComments}`);
833
+ } catch (error) {
834
+ console.error('Error loading work in progress:', error);
835
+ showError('Erreur lors du chargement : ' + error.message);
836
+ resetToUploadState();
837
+ }
838
+ }
839
+
840
+ function saveWorkInProgress() {
841
+ if (!auditData) {
842
+ alert('❌ Aucune donnée à sauvegarder. Chargez un fichier .ifs d\'abord.');
843
+ return;
844
+ }
845
+ try {
846
+ const companyName = companyProfileData['Nom du site à auditer'] || 'Société inconnue';
847
+ const coid = companyProfileData['N° COID du portail'] || 'COID-XXXX';
848
+ const workPackage = {
849
+ version: '1.2', savedDate: new Date().toISOString(), companyName, coid,
850
+ auditData, checklistData, companyProfileData, comments, requirementNumberMapping,
851
+ stats: {
852
+ totalComments: Object.keys(comments).filter(k => comments[k]?.[0]?.content.trim()).length,
853
+ totalRequirements: checklistData.length,
854
+ progressPercentage: parseFloat(document.getElementById('progressPercentage')?.textContent) || 0
855
+ }
856
+ };
857
+ const dataStr = JSON.stringify(workPackage, null, 2);
858
+ const blob = new Blob([dataStr], {type: 'application/json'});
859
+ const link = document.createElement('a');
860
+ link.href = URL.createObjectURL(blob);
861
+ const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
862
+ link.download = `TRAVAIL_${coid}_${companyName.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}.ifsr`;
863
+ link.click(); URL.revokeObjectURL(link.href);
864
+ alert(`✅ Travail sauvegardé : ${link.download}`);
865
+ } catch (error) {
866
+ console.error('Error saving work:', error);
867
+ alert('❌ Erreur sauvegarde : ' + error.message);
868
+ }
869
+ }
870
+
871
+ function processAuditDataLogic(dataFromIFSFile) {
872
+ console.log('Processing audit data logic...');
873
+ if (!dataFromIFSFile?.data?.modules?.food_8) {
874
+ showError('Format de fichier IFS non valide.');
875
+ resetToUploadState();
876
+ return;
877
+ }
878
+ const food8 = dataFromIFSFile.data.modules.food_8;
879
+ const matrixResult = food8.matrixResult;
880
+ const overallResult = food8.result.overall;
881
+ let totalA = 0, totalB = 0, totalC = 0, totalD = 0, totalNA = 0;
882
+ matrixResult.forEach(item => {
883
+ if (item.type === 'scoreCount') {
884
+ switch(item.scoreId) {
885
+ case 'A': totalA += item.count; break;
886
+ case 'B': totalB += item.count; break;
887
+ case 'C': totalC += item.count; break;
888
+ case 'D': totalD += item.count; break;
889
+ case 'NA': totalNA += item.count; break;
890
+ }
891
+ }
892
+ });
893
+ const totalRequirements = totalA + totalB + totalC + totalD + totalNA;
894
+ updateElementTextContent('totalRequirements', totalRequirements);
895
+ updateElementTextContent('conformCount', totalA);
896
+ updateElementTextContent('nonConformCount', totalB + totalC + totalD);
897
+ updateElementTextContent('overallScore', overallResult.percent.toFixed(1) + '%');
898
+ extractCompanyProfile(food8.questions);
899
+ processChecklistData(food8.checklists);
900
+ const companyName = food8.questions.companyName?.answer || 'Société inconnue';
901
+ const coid = food8.questions.companyCoid?.answer || 'COID-XXXX';
902
+ currentSession.id = `IFS-${coid}-${new Date().getTime()}`;
903
+ currentSession.name = `Audit ${companyName}`;
904
+ updateSessionIdInUI(coid);
905
+ document.getElementById('currentAuditName').textContent = `IFS Reviewer - ${companyName}`;
906
+ if(successAlertElement) successAlertElement.textContent = `✅ Nouveau dossier créé : ${companyName} (${totalRequirements} exigences)`;
907
+ finalizeUIUpdateAndRender();
908
+ }
909
+
910
+ function finalizeUIUpdateAndRender() {
911
+ if (loadingElement) loadingElement.classList.add('hidden');
912
+ if (profilResultsElement) profilResultsElement.classList.remove('hidden');
913
+ if (uploadZone) uploadZone.classList.add('hidden');
914
+
915
+ switchToolTab('profil');
916
+
917
+ setTimeout(() => {
918
+ console.log("Rendering components after data load/process...");
919
+ renderCompanyProfile();
920
+ renderChecklistTable();
921
+ renderNonConformitiesTable();
922
+ updateNonConformitiesStats();
923
+ updateProgressStats();
924
+ initColumnResize();
925
+ }, 100);
926
+ }
927
+
928
+ function resetToUploadState() {
929
+ if (loadingElement) loadingElement.classList.add('hidden');
930
+ if (uploadZone) uploadZone.classList.remove('hidden');
931
+ if (profilResultsElement) profilResultsElement.classList.add('hidden');
932
+ document.getElementById('currentAuditName').textContent = "Aucun audit chargé";
933
+ updateSessionIdInUI("----");
934
+ }
935
+
936
+ function extractCompanyProfile(questions) {
937
+ if (!questions) { companyProfileData = {}; return; }
938
+ companyProfileData = {
939
+ 'Nom du site à auditer': questions.companyName?.answer || '',
940
+ 'N° COID du portail': questions.companyCoid?.answer || '',
941
+ 'Code GLN': questions.companyGln?.[0]?.rootQuestions?.companyGlnNumber?.answer || '',
942
+ 'Rue': questions.companyStreetNo?.answer || '',
943
+ 'Code postal': questions.companyZip?.answer || '',
944
+ 'Nom de la ville': questions.companyCity?.answer || '',
945
+ 'Pays': questions.companyCountry?.answer || '',
946
+ 'Téléphone': questions.companyTelephone?.answer || '',
947
+ 'Email': questions.companyEmail?.answer || '',
948
+ 'Latitude': questions.companyGpsLatitude?.answer || '',
949
+ 'Longitude': questions.companyGpsLongitude?.answer || '',
950
+ 'Nom du siège social': questions.headquartersName?.answer || '',
951
+ 'Rue (siège social)': questions.headquartersStreetNo?.answer || '',
952
+ 'Nom de la ville (siège social)': questions.headquartersCity?.answer || '',
953
+ 'Code postal (siège social)': questions.headquartersZip?.answer || '',
954
+ 'Pays (siège social)': questions.headquartersCountry?.answer || '',
955
+ 'Téléphone (siège social)': questions.headquartersTelephone?.answer || '',
956
+ 'Surface couverte de l\'entreprise (m²)': questions.productionAreaSize?.answer || '',
957
+ 'Nombre de bâtiments': questions.numberOfBuildings?.answer || '',
958
+ 'Nombre de lignes de production': questions.numberOfProductionLines?.answer || '',
959
+ 'Nombre d\'étages': questions.numberOfFloors?.answer || '',
960
+ 'Nombre maximum d\'employés dans l\'année, au pic de production': questions.numberOfEmployeesForTimeCalculation?.answer || '',
961
+ 'Commentaires employés': questions.numberOfEmployeesDescription?.answer || '',
962
+ 'Structures décentralisées': questions.companyStructureDecentralisedDescription?.answer || '',
963
+ 'Fonctions centralisées': questions.companyStructureMultiLocationProductionDescription?.answer || '',
964
+ 'Langue parlée et écrite sur le site': questions.workingLanguage?.answer || '',
965
+ 'Langue du système qualité': questions.qmsLanguage?.answer?.[0] || '',
966
+ 'Audit scope EN': questions.scopeCertificateScopeDescription_en?.answer || '',
967
+ 'Périmètre de l\'audit FR': questions.scopeAuditScopeDescription?.answer || '',
968
+ 'Process et activités': questions.scopeProductGroupsDescription?.answer || '',
969
+ 'Activité saisonnière ? (O/N)': questions.seasonalProduction?.answer || '',
970
+ 'Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)': questions.partlyOutsourcedProcesses?.answer || '',
971
+ 'Si oui lister les procédés sous-traités': questions.partlyOutsourcedProcessesDescription?.answer || '',
972
+ 'Avez-vous des produits totalement sous-traités? (OUI/NON)': questions.fullyOutsourcedProducts?.answer || '',
973
+ 'Si oui, lister les produits totalement sous-traités': questions.fullyOutsourcedProductsDescription?.answer || '',
974
+ 'Avez-vous des produits de négoce? (OUI/NON)': questions.tradedProductsBrokerActivity?.answer || '',
975
+ 'Si oui, lister les produits de négoce': questions.tradedProductsBrokerActivityDescription?.answer || '',
976
+ 'Produits à exclure du champ d\'audit (OUI/NON)': questions.exclusions?.answer || '',
977
+ 'Préciser les produits à exclure': questions.exclusionsDescription?.answer || ''
978
+ };
979
+ }
980
+
981
+ function renderCompanyProfile() {
982
+ const container = document.getElementById('companyProfileTable');
983
+ if (!container) return;
984
+ if (!companyProfileData || Object.keys(companyProfileData).length === 0) {
985
+ container.innerHTML = '<p class="text-center p-10 text-gray-500 dark:text-gray-400">Aucune donnée de profil. Chargez un fichier.</p>';
986
+ return;
987
+ }
988
+ const categories = {
989
+ 'Informations générales': ['Nom du site à auditer', 'N° COID du portail', 'Code GLN'],
990
+ 'Adresse du site': ['Rue', 'Code postal', 'Nom de la ville', 'Pays', 'Téléphone', 'Email', 'Latitude', 'Longitude'],
991
+ '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)'],
992
+ '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 dans l\'année, au pic de production', 'Commentaires employés'],
993
+ 'Structure organisationnelle': ['Structures décentralisées', 'Fonctions centralisées'],
994
+ 'Langues': ['Langue parlée et écrite sur le site', 'Langue du système qualité'],
995
+ 'Périmètres d\'audit': ['Audit scope EN', 'Périmètre de l\'audit FR', 'Process et activités'],
996
+ 'Activités spécifiques': ['Activité saisonnière ? (O/N)', 'Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)', 'Si oui lister les procédés sous-traités', 'Avez-vous des produits totalement sous-traités? (OUI/NON)', 'Si oui, lister les produits totalement sous-traités', 'Avez-vous des produits de négoce? (OUI/NON)', 'Si oui, lister les produits de négoce'],
997
+ 'Exclusions': ['Produits à exclure du champ d\'audit (OUI/NON)', 'Préciser les produits à exclure']
998
+ };
999
+ let html = '';
1000
+ Object.entries(categories).forEach(([categoryName, fields]) => {
1001
+ html += `<div class="category-header-custom">${categoryName}</div>
1002
+ <div class="overflow-x-auto"><table class="data-table-custom w-full">
1003
+ <thead><tr><th>Information</th><th>Valeur</th><th>Commentaires Reviewer</th></tr></thead><tbody>`;
1004
+ fields.forEach(field => {
1005
+ if (companyProfileData.hasOwnProperty(field)) {
1006
+ const value = companyProfileData[field];
1007
+ const commentId = `profile-${field.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase()}`;
1008
+ const currentComment = comments[commentId]?.[0]?.content || '';
1009
+ const isLongText = ['Périmètre de l\'audit FR', 'Audit scope EN', 'Process et activités', 'Si oui lister les procédés sous-traités', 'Si oui, lister les produits totalement sous-traités', 'Si oui, lister les produits de négoce', 'Préciser les produits à exclure', 'Commentaires employés', 'Structures décentralisées', 'Fonctions centralisées'].includes(field);
1010
+ const inputField = `<textarea id="${commentId}" placeholder="Commentaire..." class="${isLongText ? 'profile-textarea-long' : 'profile-textarea-short'}" onchange="saveProfileComment('${commentId}')">${currentComment}</textarea>`;
1011
+ const displayValue = isLongText && value && value.length > 100 ? `<div class="value-display-long dark:bg-gray-700">${value || 'N/A'}</div>` : `<span class="value-display-short">${value || 'N/A'}</span>`;
1012
+ html += `<tr><td class="font-medium">${field}</td><td>${displayValue}</td><td>${inputField}</td></tr>`;
1013
+ }
1014
+ });
1015
+ html += `</tbody></table></div>`;
1016
+ });
1017
+ container.innerHTML = html;
1018
+ }
1019
+
1020
+ function saveProfileComment(commentId) {
1021
+ const textarea = document.getElementById(commentId);
1022
+ if (!textarea) return;
1023
+ const content = textarea.value.trim();
1024
+ if (content) comments[commentId] = [{ id: Date.now(), author: 'reviewer', content: content, date: new Date().toISOString() }];
1025
+ else delete comments[commentId];
1026
+ updateProgressStats();
1027
+ saveCurrentSession();
1028
+ }
1029
+
1030
+ function processChecklistData(checklists) {
1031
+ if (!checklists?.checklistFood8?.resultScorings) {
1032
+ checklistData = [];
1033
+ requirementNumberMapping = {};
1034
+ return;
1035
+ }
1036
+ const resultScorings = checklists.checklistFood8.resultScorings;
1037
+ checklistData = [];
1038
+ requirementNumberMapping = {};
1039
+ let chapterCounters = { 1:0, 2:0, 3:0, 4:0, 5:0, 6:0 };
1040
+
1041
+ for (const uuid in resultScorings) {
1042
+ const scoring = resultScorings[uuid];
1043
+ let chapter = scoring.requirement?.chapter?.number;
1044
+ let reqNumberFromData = scoring.requirement?.number;
1045
+ if (typeof chapter !== 'number' || chapter < 1 || chapter > 6) chapter = getChapterFromUUID(uuid);
1046
+ let reqNumber;
1047
+ if (reqNumberFromData) reqNumber = `${chapter}.${reqNumberFromData}`;
1048
+ else {
1049
+ chapterCounters[chapter]++;
1050
+ const mainNum = Math.floor((chapterCounters[chapter] -1) / 10) + 1;
1051
+ const subNum = ((chapterCounters[chapter] - 1) % 10) + 1;
1052
+ reqNumber = `${chapter}.${mainNum}.${subNum}`;
1053
+ }
1054
+ requirementNumberMapping[uuid] = reqNumber;
1055
+ checklistData.push({
1056
+ uuid,
1057
+ requirementNumber: reqNumber,
1058
+ chapter,
1059
+ score: scoring.score?.label || 'N/D',
1060
+ scoreValue: scoring.score?.value,
1061
+ explanation: scoring.answers?.explanationText || '',
1062
+ detailedExplanation: scoring.answers?.englishExplanationText || '',
1063
+ needsCorrection: scoring.isCorrectionRequired || false
1064
+ });
1065
+ }
1066
+
1067
+ checklistData.sort((a, b) => {
1068
+ const parseReqNum = (reqStr) => reqStr.split('.').map(Number);
1069
+ const [aChap, ...aRest] = parseReqNum(a.requirementNumber);
1070
+ const [bChap, ...bRest] = parseReqNum(b.requirementNumber);
1071
+ if (aChap !== bChap) return aChap - bChap;
1072
+ for(let i=0; i < Math.min(aRest.length, bRest.length); i++) {
1073
+ if(aRest[i] !== bRest[i]) return aRest[i] - bRest[i];
1074
+ }
1075
+ return aRest.length - bRest.length;
1076
+ });
1077
+ }
1078
+
1079
+ function renderChecklistTable() {
1080
+ const tbody = document.getElementById('checklistTableBody');
1081
+ if (!tbody) return;
1082
+ if (!checklistData || checklistData.length === 0) {
1083
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center p-10 text-gray-500 dark:text-gray-400">Aucune donnée de checklist.</td></tr>';
1084
+ return;
1085
+ }
1086
+ let html = '';
1087
+ checklistData.forEach(item => {
1088
+ const commentId = `req-${item.uuid}`;
1089
+ const currentComment = comments[commentId]?.[0]?.content || '';
1090
+ html += `<tr data-chapter="${item.chapter}" data-score="${item.score}" data-explanation-empty="${!(item.explanation || item.detailedExplanation)}">
1091
+ <td class="font-medium">${item.requirementNumber}</td>
1092
+ <td><span class="score-badge score-${item.score}">${item.score}</span></td>
1093
+ <td>${item.explanation || ''}</td>
1094
+ <td>${item.detailedExplanation || ''}</td>
1095
+ <td><textarea id="${commentId}" placeholder="Commentaire..." class="profile-textarea-short w-full" onchange="saveRequirementComment('${commentId}')">${currentComment}</textarea></td>
1096
+ </tr>`;
1097
+ });
1098
+ tbody.innerHTML = html;
1099
+ filterChecklist();
1100
+ }
1101
+
1102
+ function renderNonConformitiesTable() {
1103
+ const tbody = document.getElementById('nonConformitiesTableBody');
1104
+ if (!tbody) return;
1105
+ if (!checklistData || checklistData.length === 0) {
1106
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center p-10 text-gray-500 dark:text-gray-400">Aucune donnée de NC/NA.</td></tr>';
1107
+ return;
1108
+ }
1109
+ const nonConformItems = checklistData.filter(item => ['B', 'C', 'D', 'NA'].includes(item.score));
1110
+ if (nonConformItems.length === 0) {
1111
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center p-10 text-green-500">🎉 Aucune NC/NA trouvée !</td></tr>';
1112
+ return;
1113
+ }
1114
+ let html = '';
1115
+ nonConformItems.forEach(item => {
1116
+ const commentId = `nc-${item.uuid}`;
1117
+ const currentComment = comments[commentId]?.[0]?.content || '';
1118
+ html += `<tr data-chapter="${item.chapter}" data-score="${item.score}" data-has-comment="${currentComment ? 'true' : 'false'}">
1119
+ <td class="font-medium">${item.requirementNumber}</td>
1120
+ <td><span class="score-badge score-${item.score}">${item.score}</span></td>
1121
+ <td class="whitespace-pre-wrap max-w-xs">${item.explanation || '-'}</td>
1122
+ <td class="whitespace-pre-wrap max-w-xs">${item.detailedExplanation || '-'}</td>
1123
+ <td><textarea id="${commentId}" placeholder="Commentaire..." class="profile-textarea-short w-full" onchange="saveRequirementComment('${commentId}')">${currentComment}</textarea></td>
1124
+ </tr>`;
1125
+ });
1126
+ tbody.innerHTML = html;
1127
+ filterNonConformities();
1128
+ }
1129
+
1130
+ function saveRequirementComment(commentId) {
1131
+ const textarea = document.getElementById(commentId);
1132
+ if (!textarea) return;
1133
+ const content = textarea.value.trim();
1134
+ if (content) comments[commentId] = [{ id: Date.now(), author: 'reviewer', content, date: new Date().toISOString() }];
1135
+ else delete comments[commentId];
1136
+ updateProgressStats();
1137
+ updateNonConformitiesStats();
1138
+ saveCurrentSession();
1139
+
1140
+ // Mettre à jour l'attribut data-has-comment pour le filtrage
1141
+ const row = textarea.closest('tr');
1142
+ if (row) {
1143
+ row.setAttribute('data-has-comment', content ? 'true' : 'false');
1144
+ }
1145
+ }
1146
+
1147
+ function updateElementTextContent(id, value) {
1148
+ const element = document.getElementById(id);
1149
+ if (element) element.textContent = value;
1150
+ else console.warn(`Element ${id} not found for text update.`);
1151
+ }
1152
+
1153
+ function updateProgressStats() {
1154
+ const totalComments = Object.keys(comments).filter(k=>comments[k]?.[0]?.content.trim()).length;
1155
+ const profileFields = Object.keys(companyProfileData).length;
1156
+ const ncNaFields = checklistData.filter(i => ['B', 'C', 'D', 'NA'].includes(i.score)).length;
1157
+ const totalFields = profileFields + ncNaFields;
1158
+ const progress = totalFields > 0 ? Math.round((totalComments / totalFields) * 100) : 0;
1159
+ updateElementTextContent('progressPercentage', progress + '%');
1160
+ updateElementTextContent('commentsCount', totalComments);
1161
+ return { totalComments, totalFields, progressPercentage: progress };
1162
+ }
1163
+
1164
+ function updateNonConformitiesStats() {
1165
+ if (!checklistData) return;
1166
+ const ncNaItems = checklistData.filter(i => ['B', 'C', 'D', 'NA'].includes(i.score));
1167
+
1168
+ // Analyse par chapitre
1169
+ const ncByChapter = {};
1170
+ checklistData.forEach(item => {
1171
+ if (['B', 'C', 'D'].includes(item.score)) {
1172
+ if (!ncByChapter[item.chapter]) ncByChapter[item.chapter] = 0;
1173
+ ncByChapter[item.chapter]++;
1174
+ }
1175
+ });
1176
+
1177
+ // Priorisation des NC
1178
+ const criticalNc = checklistData.filter(i => i.score === 'D' && !comments[`nc-${i.uuid}`]?.[0]?.content?.trim());
1179
+
1180
+ const ncComments = ncNaItems.filter(item => {
1181
+ const commentId = `nc-${item.uuid}`;
1182
+ return comments[commentId]?.[0]?.content?.trim();
1183
+ }).length;
1184
+
1185
+ // Mise à jour de l'UI avec les nouvelles métriques
1186
+ document.getElementById('ncByChapter').textContent = Object.entries(ncByChapter).map(([chap, count]) => `Ch.${chap}: ${count}`).join(' | ');
1187
+ document.getElementById('uncommentedCritical').textContent = criticalNc.length;
1188
+
1189
+ updateElementTextContent('totalNC', ncNaItems.length);
1190
+ updateElementTextContent('scoreB', checklistData.filter(i => i.score === 'B').length);
1191
+ updateElementTextContent('scoreC', checklistData.filter(i => i.score === 'C').length);
1192
+ updateElementTextContent('scoreD', checklistData.filter(i => i.score === 'D').length);
1193
+ updateElementTextContent('naCount', checklistData.filter(i => i.score === 'NA').length);
1194
+ updateElementTextContent('commentsNCCount', ncComments);
1195
+ updateProgressStats();
1196
+ }
1197
+
1198
+ function getChapterFromUUID(uuid) {
1199
+ const hashCode = uuid.split('').reduce((a, b) => (a = ((a << 5) - a) + b.charCodeAt(0), a & a), 0);
1200
+ return (Math.abs(hashCode) % 6) + 1;
1201
+ }
1202
+
1203
+ function getCommentForField(fieldId) {
1204
+ return comments[fieldId]?.[0]?.content || '';
1205
+ }
1206
+
1207
+ function showError(message) {
1208
+ console.error('App Error:', message);
1209
+ if(successAlertElement) {
1210
+ successAlertElement.classList.remove('text-green-700', 'bg-green-100', 'dark:bg-green-200', 'dark:text-green-800');
1211
+ successAlertElement.classList.add('text-red-700', 'bg-red-100', 'dark:bg-red-200', 'dark:text-red-800');
1212
+ successAlertElement.textContent = `❌ Erreur: ${message}`;
1213
+ successAlertElement.classList.remove('hidden');
1214
+ } else {
1215
+ alert(`❌ Erreur: ${message}`);
1216
+ }
1217
+ }
1218
+
1219
+ function updateSessionIdInUI(coid) {
1220
+ document.getElementById('sessionIdTop').textContent = `COID: ${coid || '----'}`;
1221
+ }
1222
+
1223
+ let filterTimeout;
1224
+ function filterChecklist() {
1225
+ clearTimeout(filterTimeout);
1226
+ document.getElementById('checklistTableBody').classList.add('opacity-50');
1227
+
1228
+ filterTimeout = setTimeout(() => {
1229
+ const chapter = document.getElementById('chapterFilter')?.value;
1230
+ const score = document.getElementById('scoreFilter')?.value;
1231
+ // ... reste du code de filtrage ...
1232
+
1233
+ document.getElementById('checklistTableBody').classList.remove('opacity-50');
1234
+ }, 300);
1235
+ const explanation = document.getElementById('explanationFilter')?.value;
1236
+ const search = document.getElementById('searchInput')?.value.toLowerCase();
1237
+ const rows = document.querySelectorAll('#checklistTableBody tr');
1238
+ if (rows.length === 0 || (rows.length === 1 && rows[0].getElementsByTagName('td')[0].colSpan > 1) ) return;
1239
+ rows.forEach(row => {
1240
+ let show = true;
1241
+ if (chapter && row.dataset.chapter !== chapter) show = false;
1242
+ if (score && row.dataset.score !== score) show = false;
1243
+ if (explanation === 'empty' && row.dataset.explanationEmpty !== 'true') show = false;
1244
+ if (explanation === 'with' && row.dataset.explanationEmpty === 'true') show = false;
1245
+ if (search && !(row.textContent || row.innerText || "").toLowerCase().includes(search)) show = false;
1246
+ row.style.display = show ? '' : 'none';
1247
+ });
1248
+ }
1249
+
1250
+ ['chapterFilter', 'scoreFilter', 'explanationFilter', 'searchInput'].forEach(id => {
1251
+ const el = document.getElementById(id);
1252
+ if(el) el.addEventListener(id === 'searchInput' ? 'keyup' : 'change', filterChecklist);
1253
+ });
1254
+
1255
+ function filterNonConformities() {
1256
+ const type = document.getElementById('ncTypeFilter')?.value;
1257
+ const chapter = document.getElementById('ncChapterFilter')?.value;
1258
+ const correction = document.getElementById('correctionFilter')?.value;
1259
+ const search = document.getElementById('ncSearchInput')?.value.toLowerCase();
1260
+ const rows = document.querySelectorAll('#nonConformitiesTableBody tr');
1261
+ if (rows.length === 0 || (rows.length === 1 && rows[0].getElementsByTagName('td')[0].colSpan > 1) ) return;
1262
+ rows.forEach(row => {
1263
+ let show = true;
1264
+ if (type) {
1265
+ const scores = type.split(',');
1266
+ if (!scores.includes(row.dataset.score)) show = false;
1267
+ }
1268
+ if (chapter && row.dataset.chapter !== chapter) show = false;
1269
+ if (correction === 'with' && row.dataset.hasComment !== 'true') show = false;
1270
+ if (correction === 'without' && row.dataset.hasComment === 'true') show = false;
1271
+ if (search && !(row.textContent || row.innerText || "").toLowerCase().includes(search)) show = false;
1272
+ row.style.display = show ? '' : 'none';
1273
+ });
1274
+ }
1275
+
1276
+ ['ncTypeFilter', 'ncChapterFilter', 'correctionFilter', 'ncSearchInput'].forEach(id => {
1277
+ const el = document.getElementById(id);
1278
+ if(el) el.addEventListener(id === 'ncSearchInput' ? 'keyup' : 'change', filterNonConformities);
1279
+ });
1280
+
1281
+ function showAll() {
1282
+ document.getElementById('chapterFilter').value = '';
1283
+ document.getElementById('scoreFilter').value = '';
1284
+ document.getElementById('explanationFilter').value = 'all';
1285
+ document.getElementById('searchInput').value = '';
1286
+ filterChecklist();
1287
+ }
1288
+
1289
+ function createNewSession() {
1290
+ if (auditData && !confirm("⚠️ Créer un nouveau dossier ? Le travail non sauvegardé sera perdu.")) return;
1291
+ auditData = null; checklistData = []; companyProfileData = {}; comments = {}; requirementNumberMapping = {};
1292
+ currentSession = { id: null, name: 'Nouveau Dossier', created: new Date(), lastModified: new Date(), data: null };
1293
+ resetToUploadState();
1294
+ ['totalRequirements', 'conformCount', 'nonConformCount', 'overallScore', 'progressPercentage', 'commentsCount']
1295
+ .forEach(id => updateElementTextContent(id, id === 'overallScore' || id === 'progressPercentage' ? '0%' : '0'));
1296
+ if(successAlertElement) successAlertElement.textContent = ''; successAlertElement.classList.add('hidden');
1297
+
1298
+ renderCompanyProfile(); renderChecklistTable(); renderNonConformitiesTable();
1299
+ updateNonConformitiesStats();
1300
+ switchToolTab('profil');
1301
+ alert('✅ Nouveau dossier prêt. Chargez un fichier .ifs ou .ifsr.');
1302
+ }
1303
+
1304
+ function saveCurrentSession(showAlert = false) {
1305
+ if (!currentSession.id && showAlert) {
1306
+ alert('💡 Utilisez "Sauvegarder IFSR" pour télécharger un fichier.');
1307
+ }
1308
+ console.log("saveCurrentSession (auto-save simulation).");
1309
+ }
1310
+
1311
+ async function exportForAuditor() {
1312
+ // Ajout d'un indicateur de chargement
1313
+ const btn = document.getElementById('exportExcelBtn');
1314
+ const originalHtml = btn.innerHTML;
1315
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Génération en cours...';
1316
+ btn.disabled = true;
1317
+
1318
+ await new Promise(resolve => setTimeout(resolve, 100)); // Petite pause pour l'UI
1319
+ if (!auditData) { alert('Aucune donnée à exporter.'); return; }
1320
+ const progressStats = updateProgressStats(); const completionRate = progressStats.progressPercentage;
1321
+ if (completionRate < 10 && !confirm(`Review peu avancée (${completionRate.toFixed(0)}%). Continuer l'export ?`)) return;
1322
+
1323
+ const wb = XLSX.utils.book_new();
1324
+ const headerStyle = { font: { bold: true, sz: 12, color: { rgb: "FFFFFFFF"} }, fill: { fgColor: { rgb: "FF4F81BD" } }, alignment: { vertical: "center", horizontal: "center", wrapText: true } };
1325
+ const dataStyle = { alignment: { vertical: "top", wrapText: true } };
1326
+ const categoryStyle = { font: { bold: true, sz: 11 }, fill: { fgColor: { rgb: "FFE0E0E0" } }, alignment: { vertical: "top", wrapText: true } };
1327
+
1328
+ function applyStylesToSheet(ws, hasCatHeaders = false) {
1329
+ if (!ws || !ws['!ref']) return;
1330
+ const range = XLSX.utils.decode_range(ws['!ref']);
1331
+ for (let C_ = range.s.c; C_ <= range.e.c; ++C_) {
1332
+ const cell = ws[XLSX.utils.encode_cell({r: range.s.r, c: C_})];
1333
+ if (cell) cell.s = headerStyle;
1334
+ }
1335
+ for (let R_ = range.s.r + 1; R_ <= range.e.r; ++R_) {
1336
+ for (let C_ = range.s.c; C_ <= range.e.c; ++C_) {
1337
+ const cell_address = XLSX.utils.encode_cell({r:R_, c:C_});
1338
+ if (!ws[cell_address]) ws[cell_address] = { t:'s', v:'' };
1339
+ ws[cell_address].s = dataStyle;
1340
+ if (hasCatHeaders && C_ === range.s.c && ws[cell_address].v && String(ws[cell_address].v).startsWith("CATÉGORIE :")) {
1341
+ for (let CC_ = range.s.c; CC_ <= range.e.c; ++CC_) {
1342
+ const catCellAddr = XLSX.utils.encode_cell({r:R_, c:CC_});
1343
+ if (!ws[catCellAddr]) ws[catCellAddr] = {t:'s', v:''};
1344
+ ws[catCellAddr].s = categoryStyle;
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+
1351
+ // Sheet 1: RÉSUMÉ AUDIT
1352
+ const summary = generateExecutiveSummary();
1353
+ const summarySheetData = [
1354
+ { 'Information Clé': 'Entreprise auditée', 'Valeur': summary['Entreprise'] },
1355
+ { 'Information Clé': 'N° COID', 'Valeur': summary['COID'] },
1356
+ { 'Information Clé': 'Score global IFS', 'Valeur': summary['Score global'] },
1357
+ { 'Information Clé': 'Total exigences', 'Valeur': summary['Total exigences'] },
1358
+ { 'Information Clé': 'Conformités (A)', 'Valeur': summary['Conformités (A)'] },
1359
+ { 'Information Clé': 'Score B', 'Valeur': summary['Score B'] },
1360
+ { 'Information Clé': 'Score C', 'Valeur': summary['Score C'] },
1361
+ { 'Information Clé': 'Score D', 'Valeur': summary['Score D'] },
1362
+ { 'Information Clé': 'Total commentaires reviewer', 'Valeur': progressStats.totalComments },
1363
+ ];
1364
+ const wsSummary = XLSX.utils.json_to_sheet(summarySheetData);
1365
+ wsSummary['!cols'] = [{ width: 40 }, { width: 30 }];
1366
+ applyStylesToSheet(wsSummary);
1367
+ XLSX.utils.book_append_sheet(wb, wsSummary, "RÉSUMÉ AUDIT");
1368
+
1369
+ // Sheet 2: PROFIL ENTREPRISE
1370
+ const profileCategories = {
1371
+ 'Informations générales': ['Nom du site à auditer', 'N° COID du portail', 'Code GLN'],
1372
+ 'Adresse du site': ['Rue', 'Code postal', 'Nom de la ville', 'Pays', 'Téléphone', 'Email', 'Latitude', 'Longitude'],
1373
+ '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)'],
1374
+ '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 dans l'année, au pic de production", 'Commentaires employés'],
1375
+ 'Structure organisationnelle': ['Structures décentralisées', 'Fonctions centralisées'],
1376
+ 'Langues': ['Langue parlée et écrite sur le site', 'Langue du système qualité'],
1377
+ "Périmètres d'audit": ['Audit scope EN', "Périmètre de l'audit FR", 'Process et activités'],
1378
+ 'Activités spécifiques': ["Activité saisonnière ? (O/N)", "Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)", 'Si oui lister les procédés sous-traités', "Avez-vous des produits totalement sous-traités? (OUI/NON)", 'Si oui, lister les produits totalement sous-traités', "Avez-vous des produits de négoce? (OUI/NON)", 'Si oui, lister les produits de négoce'],
1379
+ 'Exclusions': ["Produits à exclure du champ d'audit (OUI/NON)", 'Préciser les produits à exclure']
1380
+ };
1381
+ const profileSheetData = [];
1382
+ Object.entries(profileCategories).forEach(([catName, fields]) => {
1383
+ profileSheetData.push({ 'Critère': `CATÉGORIE : ${catName.toUpperCase()}`, 'Valeur Déclarée': '', 'Commentaires Reviewer': '' });
1384
+ fields.forEach(field => {
1385
+ if (companyProfileData.hasOwnProperty(field)) {
1386
+ profileSheetData.push({
1387
+ 'Critère': field,
1388
+ 'Valeur Déclarée': companyProfileData[field] || 'N/R',
1389
+ 'Commentaires Reviewer': getCommentForField(`profile-${field.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase()}`) || '-'
1390
+ });
1391
+ }
1392
+ });
1393
+ profileSheetData.push({ 'Critère': '', 'Valeur Déclarée': '', 'Commentaires Reviewer': '' });
1394
+ });
1395
+ const wsProfile = XLSX.utils.json_to_sheet(profileSheetData);
1396
+ wsProfile['!cols'] = [{ width: 40 }, { width: 40 }, { width: 60 }];
1397
+ applyStylesToSheet(wsProfile, true);
1398
+ XLSX.utils.book_append_sheet(wb, wsProfile, "PROFIL ENTREPRISE");
1399
+
1400
+ // Sheet 3: NON-CONFORMITÉS & NA (sans colonne Action requise)
1401
+ const ncSheetData = checklistData
1402
+ .filter(item => ['B', 'C', 'D', 'NA'].includes(item.score))
1403
+ .map(item => ({
1404
+ 'Exigence': item.requirementNumber,
1405
+ 'Score Auditeur': item.score,
1406
+ 'Explication Auditeur': item.explanation || '-',
1407
+ 'Détail Auditeur': item.detailedExplanation || '-',
1408
+ 'Commentaires Reviewer': getCommentForField(`nc-${item.uuid}`) || '-'
1409
+ }));
1410
+ const wsNC = XLSX.utils.json_to_sheet(ncSheetData);
1411
+ wsNC['!cols'] = [{ width: 15 }, { width: 15 }, { width: 45 }, { width: 45 }, { width: 60 }];
1412
+ applyStylesToSheet(wsNC);
1413
+ XLSX.utils.book_append_sheet(wb, wsNC, "NON-CONFORMITÉS & NA");
1414
+
1415
+ // Sheet 4: CHECKLIST COMPLÈTE (sans colonne Action requise)
1416
+ const checklistSheetData = checklistData.map(item => ({
1417
+ 'Exigence': item.requirementNumber,
1418
+ 'Chap.': item.chapter,
1419
+ 'Score Auditeur': item.score,
1420
+ 'Explication Auditeur': item.explanation || '-',
1421
+ 'Détail Auditeur': item.detailedExplanation || '-',
1422
+ 'Commentaires Reviewer': getCommentForField(`req-${item.uuid}`) || (['B','C','D','NA'].includes(item.score) ? getCommentForField(`nc-${item.uuid}`) : '-')
1423
+ }));
1424
+ const wsChecklist = XLSX.utils.json_to_sheet(checklistSheetData);
1425
+ wsChecklist['!cols'] = [{ width: 15 }, { width: 10 }, { width: 15 }, { width: 40 }, { width: 40 }, { width: 60 }];
1426
+ applyStylesToSheet(wsChecklist);
1427
+ XLSX.utils.book_append_sheet(wb, wsChecklist, "CHECKLIST COMPLÈTE");
1428
+
1429
+ const companyName = companyProfileData['Nom du site à auditer'] || 'audit';
1430
+ const coid = companyProfileData['N° COID du portail'] || 'COID';
1431
+ const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
1432
+ const fileName = `RAPPORT_REVIEW_IFS_${coid}_${companyName.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}.xlsx`;
1433
+ XLSX.writeFile(wb, fileName);
1434
+ alert(`Rapport pour auditeur généré : ${fileName}`);
1435
+ }
1436
+
1437
+ function exportAllData() {
1438
+ if (!auditData) { alert('Aucune donnée pour l\'export.'); return; }
1439
+ const wb = XLSX.utils.book_new();
1440
+
1441
+ // Sheet 1: Profil Entreprise
1442
+ const profileSheetData = [];
1443
+ const profileCategories = {
1444
+ 'Informations générales': ['Nom du site à auditer', 'N° COID du portail', 'Code GLN'],
1445
+ 'Adresse du site': ['Rue', 'Code postal', 'Nom de la ville', 'Pays', 'Téléphone', 'Email', 'Latitude', 'Longitude'],
1446
+ '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)'],
1447
+ '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 dans l'année, au pic de production", 'Commentaires employés'],
1448
+ 'Structure organisationnelle': ['Structures décentralisées', 'Fonctions centralisées'],
1449
+ 'Langues': ['Langue parlée et écrite sur le site', 'Langue du système qualité'],
1450
+ "Périmètres d'audit": ['Audit scope EN', "Périmètre de l'audit FR", 'Process et activités'],
1451
+ 'Activités spécifiques': ["Activité saisonnière ? (O/N)", "Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)", 'Si oui lister les procédés sous-traités', "Avez-vous des produits totalement sous-traités? (OUI/NON)", 'Si oui, lister les produits totalement sous-traités', "Avez-vous des produits de négoce? (OUI/NON)", 'Si oui, lister les produits de négoce'],
1452
+ 'Exclusions': ["Produits à exclure du champ d'audit (OUI/NON)", 'Préciser les produits à exclure']
1453
+ };
1454
+ Object.entries(profileCategories).forEach(([catName, fields]) => {
1455
+ profileSheetData.push({ 'Catégorie': `=== ${catName.toUpperCase()} ===`, 'Information': '', 'Valeur': '', 'Commentaire Reviewer': '' });
1456
+ fields.forEach(field => {
1457
+ if (companyProfileData.hasOwnProperty(field)) {
1458
+ profileSheetData.push({
1459
+ 'Catégorie': catName, 'Information': field,
1460
+ 'Valeur': companyProfileData[field] || 'N/A',
1461
+ 'Commentaire Reviewer': getCommentForField(`profile-${field.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase()}`)
1462
+ });
1463
+ }
1464
+ });
1465
+ profileSheetData.push({ 'Catégorie': '', 'Information': '', 'Valeur': '', 'Commentaire Reviewer': '' });
1466
+ });
1467
+ const wsProfile = XLSX.utils.json_to_sheet(profileSheetData);
1468
+ wsProfile['!cols'] = [{wpx:150},{wpx:250},{wpx:200},{wpx:300}];
1469
+ XLSX.utils.book_append_sheet(wb, wsProfile, "Profil Entreprise Complet");
1470
+
1471
+ // Sheet 2: Checklist Complète (sans colonne Action requise)
1472
+ const checklistRaw = checklistData.map(item => ({
1473
+ 'UUID': item.uuid,
1474
+ 'N° Exigence': item.requirementNumber,
1475
+ 'Chapitre': item.chapter,
1476
+ 'Score': item.score,
1477
+ 'Explication': item.explanation || '',
1478
+ 'Détail': item.detailedExplanation || '',
1479
+ 'Commentaire Reviewer': getCommentForField(`req-${item.uuid}`) || (['B','C','D','NA'].includes(item.score) ? getCommentForField(`nc-${item.uuid}`) : '')
1480
+ }));
1481
+ const wsChecklistRaw = XLSX.utils.json_to_sheet(checklistRaw);
1482
+ wsChecklistRaw['!cols'] = [{wpx:250},{wpx:100},{wpx:50},{wpx:50},{wpx:300},{wpx:300},{wpx:300}];
1483
+ XLSX.utils.book_append_sheet(wb, wsChecklistRaw, "Checklist Brute");
1484
+
1485
+ // Sheet 3: Tous les Commentaires
1486
+ const allCommentsSheet = [];
1487
+ for (const key in comments) {
1488
+ if (comments[key] && comments[key].length > 0) {
1489
+ comments[key].forEach(comment => {
1490
+ allCommentsSheet.push({
1491
+ 'ID Commentaire': key,
1492
+ 'Auteur': comment.author,
1493
+ 'Date': new Date(comment.date).toLocaleString('fr-FR'),
1494
+ 'Contenu': comment.content
1495
+ });
1496
+ });
1497
+ }
1498
+ }
1499
+ const wsComments = XLSX.utils.json_to_sheet(allCommentsSheet);
1500
+ wsComments['!cols'] = [{wpx:200},{wpx:100},{wpx:150},{wpx:500}];
1501
+ XLSX.utils.book_append_sheet(wb, wsComments, "Tous Commentaires");
1502
+
1503
+ const companyName = companyProfileData['Nom du site à auditer'] || 'audit';
1504
+ const coid = companyProfileData['N° COID du portail'] || 'COID';
1505
+ const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
1506
+ XLSX.writeFile(wb, `Export_Brut_Complet_${coid}_${companyName.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}.xlsx`);
1507
+ alert("Export brut complet généré.");
1508
+ }
1509
+
1510
+ function exportChecklistToExcel() {
1511
+ if (!checklistData || checklistData.length === 0) {
1512
+ alert('Aucune donnée de checklist.');
1513
+ return;
1514
+ }
1515
+ const data = checklistData.map(item => ({
1516
+ 'N° Exigence': item.requirementNumber,
1517
+ 'Chapitre': item.chapter,
1518
+ 'Score': item.score,
1519
+ 'Explication': item.explanation || '',
1520
+ 'Détail': item.detailedExplanation || '',
1521
+ 'Commentaire Reviewer': getCommentForField(`req-${item.uuid}`)
1522
+ }));
1523
+ const ws = XLSX.utils.json_to_sheet(data);
1524
+ ws['!cols'] = [{wpx:100},{wpx:60},{wpx:50},{wpx:300},{wpx:300},{wpx:250}];
1525
+ const wb = XLSX.utils.book_new();
1526
+ XLSX.utils.book_append_sheet(wb, ws, "Checklist IFS");
1527
+ const companyName = companyProfileData['Nom du site à auditer'] || 'audit';
1528
+ const coid = companyProfileData['N° COID du portail'] || 'COID';
1529
+ const ts = new Date().toISOString().split('T')[0].replace(/-/g,'');
1530
+ XLSX.writeFile(wb, `Checklist_Export_${coid}_${companyName.replace(/[^a-zA-Z0-9]/g,'_')}_${ts}.xlsx`);
1531
+ alert("Checklist exportée.");
1532
+ }
1533
+
1534
+ function exportNonConformities() {
1535
+ if (!checklistData) {
1536
+ alert('Aucune donnée.');
1537
+ return;
1538
+ }
1539
+ const ncItems = checklistData.filter(item => ['B', 'C', 'D'].includes(item.score));
1540
+ if (ncItems.length === 0) {
1541
+ alert('Aucune NC (B,C,D) pour plan d\'action.');
1542
+ return;
1543
+ }
1544
+ const data = ncItems.map((item, idx) => ({
1545
+ 'N°': idx + 1,
1546
+ 'Exigence': item.requirementNumber,
1547
+ 'Score': item.score,
1548
+ 'Explication': item.explanation || '',
1549
+ 'Détail': item.detailedExplanation || '',
1550
+ 'Commentaire Reviewer': getCommentForField(`nc-${item.uuid}`),
1551
+ 'Délai Suggéré': item.score === 'D' ? '7 jours' : item.score === 'C' ? '30 jours' : '90 jours',
1552
+ 'Responsable': 'À définir',
1553
+ 'Statut': 'Ouvert'
1554
+ }));
1555
+ const ws = XLSX.utils.json_to_sheet(data);
1556
+ ws['!cols'] = [{wpx:30},{wpx:100},{wpx:50},{wpx:250},{wpx:250},{wpx:200},{wpx:100},{wpx:100},{wpx:100}];
1557
+ const wb = XLSX.utils.book_new();
1558
+ XLSX.utils.book_append_sheet(wb, ws, "Plan Actions NC");
1559
+ const companyName = companyProfileData['Nom du site à auditer'] || 'audit';
1560
+ const coid = companyProfileData['N° COID du portail'] || 'COID';
1561
+ const ts = new Date().toISOString().split('T')[0].replace(/-/g,'');
1562
+ XLSX.writeFile(wb, `Plan_Actions_NC_${coid}_${companyName.replace(/[^a-zA-Z0-9]/g,'_')}_${ts}.xlsx`);
1563
+ alert("Plan d'actions NC exporté.");
1564
+ }
1565
+
1566
+ function generateExecutiveSummary() {
1567
+ if (!checklistData || !auditData) return {};
1568
+ const totalReq = checklistData.length;
1569
+ const conform = checklistData.filter(i => i.score === 'A').length;
1570
+ const bCount = checklistData.filter(i => i.score === 'B').length;
1571
+ const cCount = checklistData.filter(i => i.score === 'C').length;
1572
+ const dCount = checklistData.filter(i => i.score === 'D').length;
1573
+ const score = auditData?.data?.modules?.food_8?.result?.overall?.percent || 0;
1574
+ return {
1575
+ 'Entreprise': companyProfileData['Nom du site à auditer'] || 'N/A',
1576
+ 'COID': companyProfileData['N° COID du portail'] || 'N/A',
1577
+ 'Ville': companyProfileData['Nom de la ville'] || 'N/A',
1578
+ 'Pays': companyProfileData['Pays'] || 'N/A',
1579
+ 'Score global': totalReq > 0 ? score.toFixed(1) + '%' : 'N/A',
1580
+ 'Total exigences': totalReq,
1581
+ 'Conformités (A)': conform,
1582
+ 'Score B': bCount,
1583
+ 'Score C': cCount,
1584
+ 'Score D': dCount,
1585
+ 'Taux de conformité': totalReq > 0 ? ((conform / totalReq) * 100).toFixed(1) + '%' : 'N/A'
1586
+ };
1587
+ }
1588
+
1589
+ function debugAndFix() {
1590
+ console.log('=== DEBUG AND FIX ===');
1591
+ if (auditData) {
1592
+ renderCompanyProfile();
1593
+ renderChecklistTable();
1594
+ renderNonConformitiesTable();
1595
+ updateNonConformitiesStats();
1596
+ updateProgressStats();
1597
+ }
1598
+ switchToolTab('profil');
1599
+ alert('Debug & Fix routine. Check console.');
1600
+ }
1601
+
1602
+ document.addEventListener('DOMContentLoaded', () => {
1603
+ console.log('DOM Loaded. Initializing IFS Reviewer Tool (New UI).');
1604
+ setDefaultTab();
1605
+ resetToUploadState();
1606
+ renderCompanyProfile();
1607
+ renderChecklistTable();
1608
+ renderNonConformitiesTable();
1609
+ updateNonConformitiesStats();
1610
+ updateProgressStats();
1611
+ initColumnResize();
1612
+ console.log('Application initialized with new UI.');
1613
+ });
1614
+
1615
+ </script>
1616
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MMOON/ifsneoreviewer" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1617
  </html>
prompts.txt ADDED
File without changes