MMOON commited on
Commit
610dc68
·
verified ·
1 Parent(s): c3a708f

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +603 -37
src/streamlit_app.py CHANGED
@@ -1,40 +1,606 @@
1
- import altair as alt
2
- import numpy as np
3
  import pandas as pd
4
  import streamlit as st
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
 
2
  import pandas as pd
3
  import streamlit as st
4
+ from io import BytesIO
5
+ import openpyxl
6
+ from datetime import datetime
7
 
8
+ # Configuration Streamlit
9
+ st.set_page_config(
10
+ page_title="IFS NEO Data Extractor",
11
+ layout="wide",
12
+ initial_sidebar_state="expanded"
13
+ )
14
+
15
+ # CSS personnalisé pour améliorer l'apparence
16
+ def apply_custom_css():
17
+ st.markdown("""
18
+ <style>
19
+ .main-header {
20
+ font-size: 2.5rem;
21
+ color: #1f77b4;
22
+ text-align: center;
23
+ margin-bottom: 2rem;
24
+ }
25
+ .section-header {
26
+ font-size: 1.5rem;
27
+ color: #2e8b57;
28
+ border-bottom: 2px solid #2e8b57;
29
+ padding-bottom: 0.5rem;
30
+ margin: 1rem 0;
31
+ }
32
+ .info-box {
33
+ background-color: #f0f8ff;
34
+ border-left: 4px solid #1f77b4;
35
+ padding: 1rem;
36
+ margin: 1rem 0;
37
+ }
38
+ .warning-box {
39
+ background-color: #fff3cd;
40
+ border-left: 4px solid #ffc107;
41
+ padding: 1rem;
42
+ margin: 1rem 0;
43
+ }
44
+ .success-box {
45
+ background-color: #d4edda;
46
+ border-left: 4px solid #28a745;
47
+ padding: 1rem;
48
+ margin: 1rem 0;
49
+ }
50
+ </style>
51
+ """, unsafe_allow_html=True)
52
+
53
+ def flatten_json_safe(nested_json, parent_key='', sep='_'):
54
+ """Aplatit une structure JSON imbriquée de manière sécurisée."""
55
+ items = []
56
+ if isinstance(nested_json, dict):
57
+ for k, v in nested_json.items():
58
+ new_key = f'{parent_key}{sep}{k}' if parent_key else k
59
+ if isinstance(v, dict):
60
+ items.extend(flatten_json_safe(v, new_key, sep=sep).items())
61
+ elif isinstance(v, list):
62
+ for i, item in enumerate(v):
63
+ items.extend(flatten_json_safe(item, f'{new_key}{sep}{i}', sep=sep).items())
64
+ else:
65
+ items.append((new_key, v))
66
+ else:
67
+ items.append((parent_key, nested_json))
68
+ return dict(items)
69
+
70
+ def extract_from_flattened(flattened_data, mapping, selected_fields):
71
+ """Extrait les données du JSON aplati selon le mapping fourni."""
72
+ extracted_data = {}
73
+ for label, flat_path in mapping.items():
74
+ if label in selected_fields:
75
+ extracted_data[label] = flattened_data.get(flat_path, 'N/A')
76
+ return extracted_data
77
+
78
+ def get_user_comments():
79
+ """Récupère tous les commentaires utilisateur depuis la session state."""
80
+ comments = {}
81
+ for key, value in st.session_state.items():
82
+ if key.startswith(('profile_comment_', 'checklist_comment_', 'non_conformity_comment_')):
83
+ comments[key] = value
84
+ return comments
85
+
86
+ def initialize_session_state():
87
+ """Initialise les variables de session state nécessaires."""
88
+ if 'json_data' not in st.session_state:
89
+ st.session_state.json_data = None
90
+ if 'profile_data' not in st.session_state:
91
+ st.session_state.profile_data = {}
92
+ if 'checklist_data' not in st.session_state:
93
+ st.session_state.checklist_data = []
94
+ if 'non_conformities' not in st.session_state:
95
+ st.session_state.non_conformities = []
96
+
97
+ # Mapping complet des champs
98
+ FLATTENED_FIELD_MAPPING = {
99
+ "Nom du site à auditer": "data_modules_food_8_questions_companyName_answer",
100
+ "N° COID du portail": "data_modules_food_8_questions_companyCoid_answer",
101
+ "Code GLN": "data_modules_food_8_questions_companyGln_answer_0_rootQuestions_companyGlnNumber_answer",
102
+ "Rue": "data_modules_food_8_questions_companyStreetNo_answer",
103
+ "Code postal": "data_modules_food_8_questions_companyZip_answer",
104
+ "Nom de la ville": "data_modules_food_8_questions_companyCity_answer",
105
+ "Pays": "data_modules_food_8_questions_companyCountry_answer",
106
+ "Téléphone": "data_modules_food_8_questions_companyTelephone_answer",
107
+ "Latitude": "data_modules_food_8_questions_companyGpsLatitude_answer",
108
+ "Longitude": "data_modules_food_8_questions_companyGpsLongitude_answer",
109
+ "Email": "data_modules_food_8_questions_companyEmail_answer",
110
+ "Nom du siège social": "data_modules_food_8_questions_headquartersName_answer",
111
+ "Rue (siège social)": "data_modules_food_8_questions_headquartersStreetNo_answer",
112
+ "Nom de la ville (siège social)": "data_modules_food_8_questions_headquartersCity_answer",
113
+ "Code postal (siège social)": "data_modules_food_8_questions_headquartersZip_answer",
114
+ "Pays (siège social)": "data_modules_food_8_questions_headquartersCountry_answer",
115
+ "Téléphone (siège social)": "data_modules_food_8_questions_headquartersTelephone_answer",
116
+ "Surface couverte de l'entreprise (m²)": "data_modules_food_8_questions_productionAreaSize_answer",
117
+ "Nombre de bâtiments": "data_modules_food_8_questions_numberOfBuildings_answer",
118
+ "Nombre de lignes de production": "data_modules_food_8_questions_numberOfProductionLines_answer",
119
+ "Nombre d'étages": "data_modules_food_8_questions_numberOfFloors_answer",
120
+ "Nombre maximum d'employés dans l'année, au pic de production": "data_modules_food_8_questions_numberOfEmployeesForTimeCalculation_answer",
121
+ "Langue parlée et écrite sur le site": "data_modules_food_8_questions_workingLanguage_answer",
122
+ "Périmètre de l'audit": "data_modules_food_8_questions_scopeCertificateScopeDescription_en_answer",
123
+ "Process et activités": "data_modules_food_8_questions_scopeProductGroupsDescription_answer",
124
+ "Activité saisonnière ? (O/N)": "data_modules_food_8_questions_seasonalProduction_answer",
125
+ "Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)": "data_modules_food_8_questions_partlyOutsourcedProcesses_answer",
126
+ "Si oui lister les procédés sous-traités": "data_modules_food_8_questions_partlyOutsourcedProcessesDescription_answer",
127
+ "Avez-vous des produits totalement sous-traités? (OUI/NON)": "data_modules_food_8_questions_fullyOutsourcedProducts_answer",
128
+ "Si oui, lister les produits totalement sous-traités": "data_modules_food_8_questions_fullyOutsourcedProductsDescription_answer",
129
+ "Avez-vous des produits de négoce? (OUI/NON)": "data_modules_food_8_questions_tradedProductsBrokerActivity_answer",
130
+ "Si oui, lister les produits de négoce": "data_modules_food_8_questions_tradedProductsBrokerActivityDescription_answer",
131
+ "Produits à exclure du champ d'audit (OUI/NON)": "data_modules_food_8_questions_exclusions_answer",
132
+ "Préciser les produits à exclure": "data_modules_food_8_questions_exclusionsDescription_answer"
133
+ }
134
+
135
+ def process_json_file(uploaded_file):
136
+ """Traite le fichier JSON uploadé et extrait les données."""
137
+ try:
138
+ json_data = json.load(uploaded_file)
139
+ st.session_state.json_data = json_data
140
+
141
+ # Aplatir les données JSON
142
+ flattened_json_data = flatten_json_safe(json_data)
143
+
144
+ # Extraire les données de profil
145
+ profile_data = extract_from_flattened(
146
+ flattened_json_data,
147
+ FLATTENED_FIELD_MAPPING,
148
+ list(FLATTENED_FIELD_MAPPING.keys())
149
+ )
150
+ st.session_state.profile_data = profile_data
151
+
152
+ # Extraire les données de checklist
153
+ checklist_data = []
154
+ if 'data' in json_data and 'modules' in json_data['data']:
155
+ modules = json_data['data']['modules']
156
+ if 'food_8' in modules and 'checklists' in modules['food_8']:
157
+ checklists = modules['food_8']['checklists']
158
+ if 'checklistFood8' in checklists and 'resultScorings' in checklists['checklistFood8']:
159
+ for uuid, scoring in checklists['checklistFood8']['resultScorings'].items():
160
+ checklist_data.append({
161
+ "Num": uuid,
162
+ "Explanation": scoring['answers'].get('englishExplanationText', 'N/A'),
163
+ "Detailed Explanation": scoring['answers'].get('explanationText', 'N/A'),
164
+ "Score": scoring['score']['label'],
165
+ "Response": scoring['answers'].get('fieldAnswers', 'N/A')
166
+ })
167
+
168
+ st.session_state.checklist_data = checklist_data
169
+
170
+ # Extraire les non-conformités
171
+ non_conformities = [item for item in checklist_data if item['Score'] != 'A']
172
+ st.session_state.non_conformities = non_conformities
173
+
174
+ return True, "Fichier traité avec succès!"
175
+
176
+ except json.JSONDecodeError as e:
177
+ return False, f"Erreur lors du décodage JSON: {str(e)}"
178
+ except Exception as e:
179
+ return False, f"Erreur lors du traitement du fichier: {str(e)}"
180
+
181
+ def display_profile_section():
182
+ """Affiche la section du profil avec possibilité d'ajout de commentaires."""
183
+ st.markdown('<div class="section-header">📋 Profil de l\'entreprise</div>', unsafe_allow_html=True)
184
+
185
+ if not st.session_state.profile_data:
186
+ st.warning("Aucune donnée de profil disponible. Veuillez d'abord charger un fichier IFS.")
187
+ return
188
+
189
+ # Organiser les données en colonnes pour une meilleure présentation
190
+ col1, col2 = st.columns(2)
191
+
192
+ profile_items = list(st.session_state.profile_data.items())
193
+ mid_point = len(profile_items) // 2
194
+
195
+ with col1:
196
+ for field, value in profile_items[:mid_point]:
197
+ st.text_input(f"**{field}**", value=str(value), key=f"profile_field_{field}", disabled=True)
198
+ # Zone de commentaire pour chaque champ
199
+ st.text_area(
200
+ f"Commentaire - {field}",
201
+ key=f"profile_comment_{field}",
202
+ height=60,
203
+ placeholder="Ajoutez vos commentaires ici..."
204
+ )
205
+
206
+ with col2:
207
+ for field, value in profile_items[mid_point:]:
208
+ st.text_input(f"**{field}**", value=str(value), key=f"profile_field_{field}", disabled=True)
209
+ # Zone de commentaire pour chaque champ
210
+ st.text_area(
211
+ f"Commentaire - {field}",
212
+ key=f"profile_comment_{field}",
213
+ height=60,
214
+ placeholder="Ajoutez vos commentaires ici..."
215
+ )
216
+
217
+ def display_checklist_section():
218
+ """Affiche la section de la checklist complète."""
219
+ st.markdown('<div class="section-header">✅ Checklist complète</div>', unsafe_allow_html=True)
220
+
221
+ if not st.session_state.checklist_data:
222
+ st.warning("Aucune donnée de checklist disponible. Veuillez d'abord charger un fichier IFS.")
223
+ return
224
+
225
+ # Filtre par score
226
+ score_filter = st.selectbox(
227
+ "Filtrer par score:",
228
+ ["Tous", "A", "B", "C", "D", "Non applicable"]
229
+ )
230
+
231
+ # Appliquer le filtre
232
+ filtered_data = st.session_state.checklist_data
233
+ if score_filter != "Tous":
234
+ filtered_data = [item for item in st.session_state.checklist_data if item['Score'] == score_filter]
235
+
236
+ st.info(f"Affichage de {len(filtered_data)} éléments sur {len(st.session_state.checklist_data)} au total")
237
+
238
+ # Afficher les éléments de la checklist
239
+ for i, item in enumerate(filtered_data):
240
+ with st.expander(f"Exigence {item['Num']} - Score: {item['Score']}", expanded=False):
241
+ col1, col2 = st.columns([3, 1])
242
+
243
+ with col1:
244
+ st.write(f"**Explication:** {item['Explanation']}")
245
+ st.write(f"**Explication détaillée:** {item['Detailed Explanation']}")
246
+ st.write(f"**Réponse:** {item['Response']}")
247
+
248
+ # Zone de commentaire pour chaque élément
249
+ st.text_area(
250
+ "Commentaire de l'auditeur:",
251
+ key=f"checklist_comment_{item['Num']}",
252
+ height=100,
253
+ placeholder="Ajoutez vos observations, commentaires ou actions à prendre..."
254
+ )
255
+
256
+ with col2:
257
+ # Affichage du score avec couleur
258
+ score_color = {
259
+ 'A': '#28a745',
260
+ 'B': '#ffc107',
261
+ 'C': '#fd7e14',
262
+ 'D': '#dc3545',
263
+ 'Non applicable': '#6c757d'
264
+ }.get(item['Score'], '#6c757d')
265
+
266
+ st.markdown(f"""
267
+ <div style="background-color: {score_color}; color: white;
268
+ padding: 10px; border-radius: 5px; text-align: center;
269
+ font-weight: bold; font-size: 18px;">
270
+ {item['Score']}
271
+ </div>
272
+ """, unsafe_allow_html=True)
273
+
274
+ def display_non_conformities_section():
275
+ """Affiche la section des non-conformités."""
276
+ st.markdown('<div class="section-header">⚠️ Non-conformités</div>', unsafe_allow_html=True)
277
+
278
+ if not st.session_state.non_conformities:
279
+ st.success("Aucune non-conformité détectée ! Toutes les exigences sont notées A.")
280
+ return
281
+
282
+ st.warning(f"Nombre de non-conformités détectées: {len(st.session_state.non_conformities)}")
283
+
284
+ # Statistiques des non-conformités
285
+ scores_count = {}
286
+ for item in st.session_state.non_conformities:
287
+ score = item['Score']
288
+ scores_count[score] = scores_count.get(score, 0) + 1
289
+
290
+ col1, col2, col3, col4 = st.columns(4)
291
+ for i, (score, count) in enumerate(scores_count.items()):
292
+ with [col1, col2, col3, col4][i % 4]:
293
+ st.metric(f"Score {score}", count)
294
+
295
+ # Afficher chaque non-conformité
296
+ for item in st.session_state.non_conformities:
297
+ with st.container():
298
+ st.markdown(f"""
299
+ <div style="border-left: 4px solid #dc3545; padding: 15px; margin: 10px 0;
300
+ background-color: #f8f9fa;">
301
+ <h4>🔍 Exigence {item['Num']} - Score: {item['Score']}</h4>
302
+ </div>
303
+ """, unsafe_allow_html=True)
304
+
305
+ col1, col2 = st.columns([3, 1])
306
+
307
+ with col1:
308
+ st.write(f"**Explication:** {item['Explanation']}")
309
+ st.write(f"**Explication détaillée:** {item['Detailed Explanation']}")
310
+ st.write(f"**Réponse:** {item['Response']}")
311
+
312
+ # Plan d'action
313
+ st.text_area(
314
+ "Plan d'action corrective:",
315
+ key=f"non_conformity_action_{item['Num']}",
316
+ height=100,
317
+ placeholder="Décrivez les actions correctives à mettre en place..."
318
+ )
319
+
320
+ # Commentaire de l'auditeur
321
+ st.text_area(
322
+ "Commentaire de l'auditeur:",
323
+ key=f"non_conformity_comment_{item['Num']}",
324
+ height=80,
325
+ placeholder="Observations de l'auditeur..."
326
+ )
327
+
328
+ with col2:
329
+ # Sélection de la priorité
330
+ priority = st.selectbox(
331
+ "Priorité:",
332
+ ["Haute", "Moyenne", "Basse"],
333
+ key=f"priority_{item['Num']}"
334
+ )
335
+
336
+ # Date limite
337
+ deadline = st.date_input(
338
+ "Date limite:",
339
+ key=f"deadline_{item['Num']}"
340
+ )
341
+
342
+ # Responsable
343
+ responsible = st.text_input(
344
+ "Responsable:",
345
+ key=f"responsible_{item['Num']}",
346
+ placeholder="Nom du responsable"
347
+ )
348
+
349
+ def create_enhanced_excel_export():
350
+ """Crée un fichier Excel enrichi avec toutes les données et commentaires."""
351
+ if not st.session_state.profile_data:
352
+ st.error("Aucune donnée à exporter. Veuillez d'abord charger un fichier IFS.")
353
+ return None
354
+
355
+ # Récupérer tous les commentaires
356
+ comments = get_user_comments()
357
+
358
+ # Créer le fichier Excel en mémoire
359
+ output = BytesIO()
360
+
361
+ with pd.ExcelWriter(output, engine='openpyxl') as writer:
362
+ # Onglet Profil
363
+ profile_rows = []
364
+ for field, value in st.session_state.profile_data.items():
365
+ comment_key = f"profile_comment_{field}"
366
+ comment = comments.get(comment_key, "")
367
+ profile_rows.append({
368
+ "Champ": field,
369
+ "Valeur": value,
370
+ "Commentaire": comment,
371
+ "Réponse auditeur": ""
372
+ })
373
+
374
+ df_profile = pd.DataFrame(profile_rows)
375
+ df_profile.to_excel(writer, index=False, sheet_name="Profil")
376
+
377
+ # Onglet Checklist complète
378
+ checklist_rows = []
379
+ for item in st.session_state.checklist_data:
380
+ comment_key = f"checklist_comment_{item['Num']}"
381
+ comment = comments.get(comment_key, "")
382
+ checklist_rows.append({
383
+ "Numéro": item['Num'],
384
+ "Explication": item['Explanation'],
385
+ "Explication détaillée": item['Detailed Explanation'],
386
+ "Score": item['Score'],
387
+ "Réponse": item['Response'],
388
+ "Commentaire auditeur": comment,
389
+ "Action requise": ""
390
+ })
391
+
392
+ df_checklist = pd.DataFrame(checklist_rows)
393
+ df_checklist.to_excel(writer, index=False, sheet_name="Checklist")
394
+
395
+ # Onglet Non-conformités avec plan d'action
396
+ nc_rows = []
397
+ for item in st.session_state.non_conformities:
398
+ comment_key = f"non_conformity_comment_{item['Num']}"
399
+ action_key = f"non_conformity_action_{item['Num']}"
400
+ priority_key = f"priority_{item['Num']}"
401
+ deadline_key = f"deadline_{item['Num']}"
402
+ responsible_key = f"responsible_{item['Num']}"
403
+
404
+ comment = comments.get(comment_key, "")
405
+ action = st.session_state.get(action_key, "")
406
+ priority = st.session_state.get(priority_key, "")
407
+ deadline = st.session_state.get(deadline_key, "")
408
+ responsible = st.session_state.get(responsible_key, "")
409
+
410
+ nc_rows.append({
411
+ "Numéro": item['Num'],
412
+ "Score": item['Score'],
413
+ "Explication": item['Explanation'],
414
+ "Explication détaillée": item['Detailed Explanation'],
415
+ "Réponse": item['Response'],
416
+ "Commentaire auditeur": comment,
417
+ "Plan d'action": action,
418
+ "Priorité": priority,
419
+ "Date limite": deadline,
420
+ "Responsable": responsible,
421
+ "Statut": "En attente"
422
+ })
423
+
424
+ df_nc = pd.DataFrame(nc_rows)
425
+ df_nc.to_excel(writer, index=False, sheet_name="Non-conformités")
426
+
427
+ # Onglet Résumé
428
+ summary_data = {
429
+ "Indicateur": [
430
+ "Nombre total d'exigences",
431
+ "Exigences conformes (A)",
432
+ "Non-conformités mineures (B)",
433
+ "Non-conformités majeures (C)",
434
+ "Non-conformités critiques (D)",
435
+ "Taux de conformité (%)"
436
+ ],
437
+ "Valeur": [
438
+ len(st.session_state.checklist_data),
439
+ len([x for x in st.session_state.checklist_data if x['Score'] == 'A']),
440
+ len([x for x in st.session_state.checklist_data if x['Score'] == 'B']),
441
+ len([x for x in st.session_state.checklist_data if x['Score'] == 'C']),
442
+ len([x for x in st.session_state.checklist_data if x['Score'] == 'D']),
443
+ round((len([x for x in st.session_state.checklist_data if x['Score'] == 'A']) /
444
+ len(st.session_state.checklist_data)) * 100, 2) if st.session_state.checklist_data else 0
445
+ ]
446
+ }
447
+
448
+ df_summary = pd.DataFrame(summary_data)
449
+ df_summary.to_excel(writer, index=False, sheet_name="Résumé")
450
+
451
+ # Ajuster la largeur des colonnes
452
+ for sheet_name in writer.sheets:
453
+ worksheet = writer.sheets[sheet_name]
454
+ for column in worksheet.columns:
455
+ max_length = 0
456
+ column_letter = column[0].column_letter
457
+ for cell in column:
458
+ try:
459
+ if len(str(cell.value)) > max_length:
460
+ max_length = len(str(cell.value))
461
+ except:
462
+ pass
463
+ adjusted_width = min(max_length + 2, 50)
464
+ worksheet.column_dimensions[column_letter].width = adjusted_width
465
+
466
+ output.seek(0)
467
+ return output
468
+
469
+ def main():
470
+ """Fonction principale de l'application."""
471
+ # Initialiser la session state
472
+ initialize_session_state()
473
+
474
+ # Appliquer le CSS personnalisé
475
+ apply_custom_css()
476
+
477
+ # En-tête principal
478
+ st.markdown('<div class="main-header">🔍 IFS NEO Data Extractor</div>', unsafe_allow_html=True)
479
+ st.markdown('<div class="info-box">Application d\'extraction et d\'analyse des données d\'audit IFS</div>', unsafe_allow_html=True)
480
+
481
+ # Navigation dans la sidebar
482
+ st.sidebar.title("📋 Navigation")
483
+
484
+ # Upload du fichier IFS
485
+ st.sidebar.markdown("### 📁 Chargement des fichiers")
486
+ uploaded_json_file = st.sidebar.file_uploader(
487
+ "Charger le fichier IFS (.ifs)",
488
+ type="ifs",
489
+ help="Sélectionnez le fichier d'audit IFS exporté depuis NEO"
490
+ )
491
+
492
+ # Traitement du fichier JSON
493
+ if uploaded_json_file and st.session_state.json_data is None:
494
+ with st.spinner("Traitement du fichier IFS en cours..."):
495
+ success, message = process_json_file(uploaded_json_file)
496
+ if success:
497
+ st.sidebar.success(message)
498
+ else:
499
+ st.sidebar.error(message)
500
+ return
501
+
502
+ # Menu de navigation principal
503
+ if st.session_state.json_data:
504
+ st.sidebar.markdown("### 🎯 Sections disponibles")
505
+ page = st.sidebar.radio(
506
+ "Choisissez une section:",
507
+ ["📋 Profil de l'entreprise", "✅ Checklist complète", "⚠️ Non-conformités", "📊 Tableau de bord", "📄 Export Excel"]
508
+ )
509
+
510
+ # Affichage des sections selon la navigation
511
+ if page == "📋 Profil de l'entreprise":
512
+ display_profile_section()
513
+
514
+ elif page == "✅ Checklist complète":
515
+ display_checklist_section()
516
+
517
+ elif page == "⚠️ Non-conformités":
518
+ display_non_conformities_section()
519
+
520
+ elif page == "📊 Tableau de bord":
521
+ st.markdown('<div class="section-header">📊 Tableau de bord de l\'audit</div>', unsafe_allow_html=True)
522
+
523
+ # Métriques principales
524
+ col1, col2, col3, col4 = st.columns(4)
525
+
526
+ total_items = len(st.session_state.checklist_data)
527
+ conformes = len([x for x in st.session_state.checklist_data if x['Score'] == 'A'])
528
+ non_conformites = len(st.session_state.non_conformities)
529
+ taux_conformite = (conformes / total_items * 100) if total_items > 0 else 0
530
+
531
+ with col1:
532
+ st.metric("Total exigences", total_items)
533
+ with col2:
534
+ st.metric("Conformes (A)", conformes)
535
+ with col3:
536
+ st.metric("Non-conformités", non_conformites)
537
+ with col4:
538
+ st.metric("Taux conformité", f"{taux_conformite:.1f}%")
539
+
540
+ # Répartition des scores
541
+ if st.session_state.checklist_data:
542
+ scores_count = {}
543
+ for item in st.session_state.checklist_data:
544
+ score = item['Score']
545
+ scores_count[score] = scores_count.get(score, 0) + 1
546
+
547
+ # Graphique de répartition
548
+ st.subheader("Répartition des scores")
549
+ chart_data = pd.DataFrame(list(scores_count.items()), columns=['Score', 'Nombre'])
550
+ st.bar_chart(chart_data.set_index('Score'))
551
+
552
+ elif page == "📄 Export Excel":
553
+ st.markdown('<div class="section-header">📄 Export des données</div>', unsafe_allow_html=True)
554
+
555
+ st.info("Exportez toutes les données collectées avec vos commentaires dans un fichier Excel structuré.")
556
+
557
+ if st.button("🔄 Générer le fichier Excel", type="primary"):
558
+ with st.spinner("Génération du fichier Excel..."):
559
+ excel_file = create_enhanced_excel_export()
560
+
561
+ if excel_file:
562
+ # Nom du fichier avec COID et date
563
+ coid = st.session_state.profile_data.get("N° COID du portail", "inconnu")
564
+ date_str = datetime.now().strftime("%Y%m%d_%H%M")
565
+ filename = f"audit_IFS_{coid}_{date_str}.xlsx"
566
+
567
+ st.download_button(
568
+ label="📥 Télécharger le rapport Excel",
569
+ data=excel_file,
570
+ file_name=filename,
571
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
572
+ )
573
+
574
+ st.success("Fichier Excel généré avec succès!")
575
+
576
+ # Informations sur le contenu du fichier
577
+ st.markdown("### 📋 Contenu du fichier Excel:")
578
+ st.markdown("""
579
+ - **Profil**: Informations sur l'entreprise avec commentaires
580
+ - **Checklist**: Liste complète des exigences avec scores et commentaires
581
+ - **Non-conformités**: Plan d'action détaillé pour chaque non-conformité
582
+ - **Résumé**: Statistiques et indicateurs de performance
583
+ """)
584
+
585
+ else:
586
+ # Page d'accueil si aucun fichier n'est chargé
587
+ st.markdown("""
588
+ ### 🚀 Bienvenue dans l'extracteur de données IFS NEO
589
+
590
+ Cette application vous permet de:
591
+ - 📊 Extraire et analyser les données d'audit IFS
592
+ - 💬 Ajouter vos commentaires et observations
593
+ - 📋 Créer des plans d'action pour les non-conformités
594
+ - 📄 Exporter tout dans un rapport Excel structuré
595
+
596
+ **Pour commencer:**
597
+ 1. Chargez votre fichier d'audit IFS (.ifs) dans la barre latérale
598
+ 2. Naviguez entre les différentes sections
599
+ 3. Ajoutez vos commentaires et plans d'action
600
+ 4. Exportez le rapport final
601
+ """)
602
+
603
+ st.markdown('<div class="warning-box">⚠️ Veuillez charger un fichier IFS pour commencer l\'analyse.</div>', unsafe_allow_html=True)
604
+
605
+ if __name__ == "__main__":
606
+ main()