MMOON commited on
Commit
1e306a4
·
verified ·
1 Parent(s): 8e1e203

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +737 -1050
app.py CHANGED
@@ -1,21 +1,16 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import numpy as np
4
- import matplotlib.pyplot as plt
5
- import time
6
- import json
7
- import os
8
- import uuid
9
- import zipfile
10
  import io
 
11
  import base64
 
 
12
  from datetime import datetime
13
- from typing import Dict, List, Any, Tuple
14
 
15
  # Configuration de la page
16
  st.set_page_config(
17
- page_title="VisiPilot IFS Food 8",
18
- page_icon="🔍",
19
  layout="wide",
20
  initial_sidebar_state="expanded"
21
  )
@@ -23,29 +18,21 @@ st.set_page_config(
23
  # Styles CSS
24
  st.markdown("""
25
  <style>
26
- .main-header {
27
- font-size: 28px;
28
  font-weight: bold;
29
- color: #0066cc;
30
  text-align: center;
31
  margin-bottom: 20px;
32
  padding: 10px 0;
33
  border-bottom: 2px solid #e0f7fa;
34
  }
35
- .banner {
36
- background-image: url('https://github.com/M00N69/BUSCAR/blob/main/logo%2002%20copie.jpg?raw=true');
37
- background-size: cover;
38
- height: 150px;
39
- background-position: center;
40
- margin-bottom: 20px;
41
- border-radius: 10px;
42
- }
43
  .card {
44
  padding: 15px;
45
- border-radius: 10px;
46
  background-color: #f9f9f9;
47
  margin-bottom: 15px;
48
- border-left: 5px solid #0066cc;
49
  }
50
  .status-badge {
51
  display: inline-block;
@@ -67,65 +54,37 @@ st.markdown("""
67
  background-color: #dc3545;
68
  color: white;
69
  }
70
- .button-container {
71
- display: flex;
72
- gap: 10px;
73
- }
74
-
75
- /* Style personnalisé pour les expanders de recommandation */
76
- .recommendation-expander {
77
- background-color: #e6f2ff !important;
78
- border-radius: 8px !important;
79
- border: 1px solid #b3d9ff !important;
80
- margin-top: 10px !important;
81
- }
82
-
83
- /* Modification du style des éléments d'expander de Streamlit */
84
- .st-emotion-cache-1abe2ax, .st-emotion-cache-ue6h4q, .st-emotion-cache-1y4p8pa {
85
- background-color: #e6f2ff !important;
86
- }
87
- /* Modification pour le header d'expander */
88
- .st-emotion-cache-19rxjzo {
89
- background-color: #cce5ff !important;
90
- }
91
-
92
- /* Style pour les onglets de rôle */
93
- .role-tab {
94
- padding: 15px;
95
- border-radius: 10px;
96
- text-align: center;
97
- cursor: pointer;
98
- transition: all 0.3s;
99
  }
100
- .role-tab:hover {
101
- transform: translateY(-3px);
102
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 
 
103
  }
104
- .role-tab-active {
105
- border: 2px solid #0066cc;
106
- background-color: #e6f2ff;
 
 
107
  }
108
- .role-tab-inactive {
109
- border: 1px solid #cccccc;
110
- background-color: #f9f9f9;
111
  }
112
-
113
- /* Style pour les vignettes de pièces jointes */
114
- .attachment-thumbnail {
115
- border: 1px solid #ddd;
116
- border-radius: 5px;
117
- padding: 5px;
118
- margin: 5px;
119
- display: inline-block;
120
- max-width: 150px;
121
  }
122
- .attachment-name {
123
- font-size: 12px;
124
- overflow: hidden;
125
- text-overflow: ellipsis;
126
- white-space: nowrap;
127
- max-width: 140px;
128
- text-align: center;
129
  }
130
  </style>
131
  """, unsafe_allow_html=True)
@@ -133,16 +92,12 @@ st.markdown("""
133
  # Initialiser les états de session
134
  if 'role' not in st.session_state:
135
  st.session_state['role'] = "site" # site, auditor, reviewer
136
- if 'action_plan_df' not in st.session_state:
137
- st.session_state['action_plan_df'] = None
138
- if 'recommendations' not in st.session_state:
139
- st.session_state['recommendations'] = {}
140
- if 'responses' not in st.session_state:
141
- st.session_state['responses'] = {}
142
- if 'attachments' not in st.session_state:
143
- st.session_state['attachments'] = {}
144
  if 'comments' not in st.session_state:
145
  st.session_state['comments'] = {}
 
 
146
  if 'audit_metadata' not in st.session_state:
147
  st.session_state['audit_metadata'] = {
148
  "audit_id": str(uuid.uuid4()),
@@ -150,1041 +105,773 @@ if 'audit_metadata' not in st.session_state:
150
  "site_name": "",
151
  "auditor_name": "",
152
  "reviewer_name": "",
153
- "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
154
- "last_modified": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
155
- "validation": {
156
- "site": False,
157
- "auditor": False,
158
- "reviewer": False
159
- }
160
  }
161
- if 'history' not in st.session_state:
162
- st.session_state['history'] = []
163
  if 'active_item' not in st.session_state:
164
  st.session_state['active_item'] = None
165
- if 'ask_questions' not in st.session_state:
166
- st.session_state['ask_questions'] = {}
167
- if 'api_key' not in st.session_state:
168
- st.session_state['api_key'] = ""
169
-
170
- # Fonctions pour gérer le fichier .auditpack
171
-
172
- def create_auditpack() -> bytes:
173
- """Crée un fichier .auditpack contenant toutes les données actuelles"""
174
- # Préparer les données pour le JSON principal
175
- audit_data = {
176
- "metadata": st.session_state['audit_metadata'],
177
- "plan_action": st.session_state['action_plan_df'].to_dict() if st.session_state['action_plan_df'] is not None else {},
178
- "recommendations": st.session_state['recommendations'],
179
- "responses": st.session_state['responses'],
180
- "comments": st.session_state['comments'],
181
- "history": st.session_state['history']
182
- }
183
 
184
- # Mettre à jour la date de dernière modification
185
- audit_data["metadata"]["last_modified"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
186
-
187
- # Créer un fichier ZIP en mémoire
188
- zip_buffer = io.BytesIO()
189
-
190
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as audit_zip:
191
- # Ajouter le JSON principal
192
- audit_zip.writestr("audit_data.json", json.dumps(audit_data, indent=2))
193
-
194
- # Ajouter les pièces jointes
195
- for idx, attachments in st.session_state['attachments'].items():
196
- for file_name, file_data in attachments.items():
197
- file_path = f"attachments/{idx}/{file_name}"
198
- audit_zip.writestr(file_path, file_data)
199
-
200
- zip_buffer.seek(0)
201
- return zip_buffer.getvalue()
202
-
203
- def load_auditpack(file_data: bytes) -> bool:
204
- """Charge les données d'un fichier .auditpack"""
205
  try:
206
- # Créer un buffer en mémoire avec les données du fichier
207
- zip_buffer = io.BytesIO(file_data)
208
-
209
- with zipfile.ZipFile(zip_buffer, "r") as audit_zip:
210
- # Extraire et charger le JSON principal
211
- with audit_zip.open("audit_data.json") as f:
212
- audit_data = json.loads(f.read())
213
-
214
- # Mettre à jour les données de session
215
- st.session_state['audit_metadata'] = audit_data["metadata"]
216
-
217
- # Charger le plan d'action
218
- if "plan_action" in audit_data and audit_data["plan_action"]:
219
- st.session_state['action_plan_df'] = pd.DataFrame.from_dict(audit_data["plan_action"])
220
-
221
- st.session_state['recommendations'] = audit_data.get("recommendations", {})
222
- st.session_state['responses'] = audit_data.get("responses", {})
223
- st.session_state['comments'] = audit_data.get("comments", {})
224
- st.session_state['history'] = audit_data.get("history", [])
225
-
226
- # Extraire les pièces jointes
227
- attachments = {}
228
- for file_info in audit_zip.namelist():
229
- if file_info.startswith("attachments/"):
230
- # Format expected: attachments/item_id/filename
231
- parts = file_info.split("/")
232
- if len(parts) == 3:
233
- item_id = parts[1]
234
- filename = parts[2]
235
-
236
- if item_id not in attachments:
237
- attachments[item_id] = {}
238
-
239
- attachments[item_id][filename] = audit_zip.read(file_info)
240
-
241
- st.session_state['attachments'] = attachments
242
-
243
- # Ajouter une entrée d'historique
244
- history_entry = {
245
- "action": "load_auditpack",
246
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
247
- "role": st.session_state['role'],
248
- "details": f"Chargement du fichier audit par {st.session_state['role']}"
249
- }
250
- st.session_state['history'].append(history_entry)
251
-
252
- return True
253
  except Exception as e:
254
- error_message = f"⚠️ Erreur lors du chargement du fichier : {str(e)}"
255
- st.error(error_message)
256
- return False
257
 
258
- # Charger le fichier Excel avec le plan d'action
259
- def load_action_plan(uploaded_file):
260
  try:
261
- # Utiliser header=11 pour sauter les lignes d'en-tête
262
- action_plan_df = pd.read_excel(uploaded_file, header=11)
263
- action_plan_df = action_plan_df[["requirementNo", "requirementText", "requirementExplanation"]]
264
- action_plan_df.columns = ["Numéro d'exigence", "Exigence IFS Food 8", "Explication (par l'auditeur/l'évaluateur)"]
265
-
266
- # Ajouter une colonne de statut
267
- action_plan_df["Statut"] = "Non traité"
268
-
269
- # Supprimer les lignes vides (où le numéro d'exigence est NaN ou vide)
270
- action_plan_df = action_plan_df.dropna(subset=["Numéro d'exigence"])
271
- action_plan_df = action_plan_df[action_plan_df["Numéro d'exigence"].astype(str).str.strip() != ""]
272
-
273
- # Ajouter à l'historique
274
- history_entry = {
275
- "action": "load_excel",
276
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
277
- "role": st.session_state['role'],
278
- "details": f"Chargement du plan d'action Excel par {st.session_state['role']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }
280
- st.session_state['history'].append(history_entry)
281
-
282
- return action_plan_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  except Exception as e:
284
- error_message = f"⚠️ Erreur lors de la lecture du fichier: {str(e)}"
285
- st.error(error_message)
286
  return None
287
 
288
- # Détecter la langue du texte
289
- def detect_language(text):
290
- # Liste de mots français courants pour détection simple
291
- french_words = ["et", "les", "des", "dans", "pour", "avec", "par", "sur", "en", "au", "aux",
292
- "de", "la", "le", "du", "un", "une", "cette", "est", "sont", "ont", "qui",
293
- "non", "conformité", "exigence", "auditeur"]
294
-
295
- # Compter les mots français
296
- text_lower = text.lower()
297
- french_count = sum(1 for word in french_words if f" {word} " in f" {text_lower} ")
298
-
299
- # Si au moins 3 mots français sont détectés, considérer comme français
300
- return "fr" if french_count >= 3 else "en"
301
-
302
- # Générer des questions adaptées à la non-conformité
303
- def generate_questions(non_conformity):
304
- req_text = non_conformity["Exigence IFS Food 8"]
305
- audit_comment = non_conformity["Explication (par l'auditeur/l'évaluateur)"]
306
-
307
- # Détecter les mots-clés pour personnaliser les questions
308
- keywords = {
309
- "formation": ["formation", "compétence", "qualification", "personnel"],
310
- "documentation": ["document", "procédure", "enregistrement", "contrôle"],
311
- "équipement": ["équipement", "matériel", "maintenance", "installation"],
312
- "hygiène": ["hygiène", "nettoyage", "désinfection", "contamination"],
313
- "traçabilité": ["traçabilité", "lot", "identification", "rappel"]
314
  }
315
-
316
- # Questions de base
317
- questions = [
318
- {
319
- "id": "context",
320
- "question": "Décrivez brièvement le contexte actuel lié à cette exigence dans votre entreprise."
321
- },
322
- {
323
- "id": "cause",
324
- "question": "Selon vous, quelle est la cause principale de cette non-conformité ?"
325
- }
326
- ]
327
-
328
- # Ajouter des questions spécifiques en fonction des mots-clés
329
- for category, terms in keywords.items():
330
- for term in terms:
331
- if term.lower() in req_text.lower() or term.lower() in audit_comment.lower():
332
- if category == "formation":
333
- questions.append({
334
- "id": "training",
335
- "question": "Le personnel a-t-il reçu une formation spécifique sur ce sujet ? Si oui, quand était la dernière formation ?"
336
- })
337
- break
338
- elif category == "documentation":
339
- questions.append({
340
- "id": "documentation",
341
- "question": "Disposez-vous d'une procédure ou d'instructions pour ce processus ? Est-elle à jour ?"
342
- })
343
- break
344
- elif category == "équipement":
345
- questions.append({
346
- "id": "equipment",
347
- "question": "Les équipements concernés sont-ils adaptés et correctement entretenus ?"
348
- })
349
- break
350
- elif category == "hygiène":
351
- questions.append({
352
- "id": "hygiene",
353
- "question": "Quelles sont vos pratiques actuelles de nettoyage/désinfection dans cette zone ?"
354
- })
355
- break
356
- elif category == "traçabilité":
357
- questions.append({
358
- "id": "traceability",
359
- "question": "Comment assurez-vous actuellement la traçabilité dans ce processus ?"
360
- })
361
- break
362
-
363
- # Limiter à 3 questions maximum
364
- return questions[:3]
365
-
366
- # Fonction pour vérifier si l'API est disponible
367
- def is_groq_api_available():
368
- return st.session_state.get('api_key', "") != ""
369
-
370
- # Générer une recommandation (version hybride API/locale) - CORRIGÉ
371
- def generate_recommendation(non_conformity, responses=None, direct=False):
372
- # Vérifier si l'API Groq est disponible
373
- use_api = is_groq_api_available()
374
-
375
- # Détection de la langue
376
- combined_text = f"{non_conformity['Exigence IFS Food 8']} {non_conformity['Explication (par l'auditeur/l'évaluateur)']}"
377
- language = detect_language(combined_text)
378
-
379
- if use_api:
380
- try:
381
- # Tentative d'utilisation de l'API
382
- from pocketgroq import GroqProvider
383
- groq = GroqProvider(api_key=st.session_state['api_key'])
384
-
385
- # Construire le prompt selon la langue détectée
386
- if language == "fr":
387
- if direct:
388
- prompt = (
389
- f"En tant qu'expert en IFS Food 8 et en sécurité alimentaire, "
390
- f"analysez cette non-conformité et proposez un plan d'action complet.\n\n"
391
- f"# NON-CONFORMITÉ\n"
392
- f"- Exigence N°: {non_conformity['Numéro d\'exigence']}\n"
393
- f"- Exigence IFS Food 8: {non_conformity['Exigence IFS Food 8']}\n"
394
- f"- Constat de l'auditeur: {non_conformity['Explication (par l\'auditeur/l\'évaluateur)']}\n\n"
395
- f"# VOTRE MISSION\n"
396
- "Fournir une analyse et un plan d'action structuré avec les sections suivantes:\n\n"
397
- "1. ANALYSE DE LA NON-CONFORMITÉ\n"
398
- "Analysez la situation et identifiez clairement le problème.\n\n"
399
- "2. ANALYSE DES CAUSES\n"
400
- "Identifiez et détaillez les causes racines probables.\n\n"
401
- "3. PLAN D'ACTION\n"
402
- " a) Actions immédiates (corrections)\n"
403
- " b) Type de preuves à fournir\n"
404
- " c) Actions correctives à long terme\n\n"
405
- "4. MÉTHODES DE VALIDATION DE L'EFFICACITÉ\n"
406
- "Comment vérifier que les actions mises en place sont efficaces.\n\n"
407
- "5. RECOMMANDATIONS COMPLÉMENTAIRES\n\n"
408
- "Rédigez l'ensemble de l'analyse en français et fournissez des recommandations spécifiques, réalistes et conformes à l'IFS Food 8."
409
- )
410
- else:
411
- prompt = (
412
- f"En tant qu'expert en IFS Food 8 et en sécurité alimentaire, "
413
- f"analysez cette non-conformité et les informations fournies pour proposer un plan d'action adapté.\n\n"
414
- f"# NON-CONFORMITÉ\n"
415
- f"- Exigence N°: {non_conformity['Numéro d\'exigence']}\n"
416
- f"- Exigence IFS Food 8: {non_conformity['Exigence IFS Food 8']}\n"
417
- f"- Constat de l'auditeur: {non_conformity['Explication (par l\'auditeur/l\'évaluateur)']}\n\n"
418
- f"# INFORMATIONS FOURNIES PAR L'UTILISATEUR\n"
419
- f"{json.dumps(responses, indent=2)}\n\n"
420
- f"# VOTRE MISSION\n"
421
- "Fournir une analyse et un plan d'action structuré avec les sections suivantes:\n\n"
422
- "1. ANALYSE DE LA NON-CONFORMITÉ\n"
423
- "Analysez la situation et identifiez clairement le problème en tenant compte des informations fournies.\n\n"
424
- "2. ANALYSE DES CAUSES\n"
425
- "Identifiez et détaillez les causes racines probables.\n\n"
426
- "3. PLAN D'ACTION\n"
427
- " a) Actions immédiates (corrections)\n"
428
- " b) Type de preuves à fournir\n"
429
- " c) Actions correctives à long terme\n\n"
430
- "4. MÉTHODES DE VALIDATION DE L'EFFICACITÉ\n"
431
- "Comment vérifier que les actions mises en place sont efficaces.\n\n"
432
- "5. RECOMMANDATIONS COMPLÉMENTAIRES\n\n"
433
- "Rédigez l'ensemble de l'analyse en français et fournissez des recommandations spécifiques, réalistes et conformes à l'IFS Food 8."
434
- )
435
- else:
436
- # En anglais
437
- if direct:
438
- prompt = (
439
- f"As an IFS Food 8 and food safety expert, analyze this non-conformity and provide a comprehensive action plan.\n\n"
440
- f"# NON-CONFORMITY\n"
441
- f"- Requirement No.: {non_conformity['Numéro d\'exigence']}\n"
442
- f"- IFS Food 8 Requirement: {non_conformity['Exigence IFS Food 8']}\n"
443
- f"- Auditor's finding: {non_conformity['Explication (par l\'auditeur/l\'évaluateur)']}\n\n"
444
- f"# YOUR MISSION\n"
445
- "Provide an analysis and structured action plan with the following sections:\n\n"
446
- "1. ANALYSIS OF THE NON-CONFORMITY\n"
447
- "Analyze the situation and clearly identify the problem.\n\n"
448
- "2. ROOT CAUSE ANALYSIS\n"
449
- "Identify and detail the probable root causes.\n\n"
450
- "3. ACTION PLAN\n"
451
- " a) Immediate actions (corrections)\n"
452
- " b) Type of evidence to be provided\n"
453
- " c) Long-term corrective actions\n\n"
454
- "4. METHODS FOR VALIDATING EFFECTIVENESS\n"
455
- "How to verify that the implemented actions are effective.\n\n"
456
- "5. ADDITIONAL RECOMMENDATIONS\n\n"
457
- "Write the entire analysis in English and provide specific, realistic recommendations that comply with IFS Food 8."
458
- )
459
- else:
460
- prompt = (
461
- f"As an IFS Food 8 and food safety expert, analyze this non-conformity and the information provided to propose an adapted action plan.\n\n"
462
- f"# NON-CONFORMITY\n"
463
- f"- Requirement No.: {non_conformity['Numéro d\'exigence']}\n"
464
- f"- IFS Food 8 Requirement: {non_conformity['Exigence IFS Food 8']}\n"
465
- f"- Auditor's finding: {non_conformity['Explication (par l\'auditeur/l\'évaluateur)']}\n\n"
466
- f"# INFORMATION PROVIDED BY THE USER\n"
467
- f"{json.dumps(responses, indent=2)}\n\n"
468
- f"# YOUR MISSION\n"
469
- "Provide an analysis and structured action plan with the following sections:\n\n"
470
- "1. ANALYSIS OF THE NON-CONFORMITY\n"
471
- "Analyze the situation and clearly identify the problem, taking into account the information provided.\n\n"
472
- "2. ROOT CAUSE ANALYSIS\n"
473
- "Identify and detail the probable root causes.\n\n"
474
- "3. ACTION PLAN\n"
475
- " a) Immediate actions (corrections)\n"
476
- " b) Type of evidence to be provided\n"
477
- " c) Long-term corrective actions\n\n"
478
- "4. METHODS FOR VALIDATING EFFECTIVENESS\n"
479
- "How to verify that the implemented actions are effective.\n\n"
480
- "5. ADDITIONAL RECOMMENDATIONS\n\n"
481
- "Write the entire analysis in English and provide specific, realistic recommendations that comply with IFS Food 8."
482
- )
483
-
484
- # Envoi de la requête à l'API
485
- api_result = groq.generate(prompt, max_tokens=1500, temperature=0.2, use_cot=True)
486
-
487
- # Ajouter une note indiquant que la recommandation vient de l'API
488
- api_note = "\n\n> *Cette recommandation a été générée par l'API Groq pour une meilleure personnalisation.*" if language == "fr" else "\n\n> *This recommendation was generated by the Groq API for better personalization.*"
489
- recommendation = f"{api_result}{api_note}"
490
-
491
- # Ajouter à l'historique
492
- history_entry = {
493
- "action": "generate_recommendation_api",
494
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
495
- "role": st.session_state['role'],
496
- "details": f"Génération de recommandation via API pour l'exigence {non_conformity['Numéro d\'exigence']}"
497
- }
498
- st.session_state['history'].append(history_entry)
499
-
500
- return recommendation
501
-
502
- except Exception as e:
503
- # En cas d'erreur, afficher un message et utiliser la version locale
504
- warning_message = f"Erreur lors de l'utilisation de l'API Groq: {str(e)}. Utilisation de la recommandation locale à la place."
505
- st.warning(warning_message)
506
- use_api = False
507
-
508
- # Si pas d'API disponible ou en cas d'erreur, utiliser la version locale
509
- if not use_api:
510
- # Extraction des valeurs pour les f-strings
511
- requirement_num = non_conformity["Numéro d'exigence"]
512
- requirement_text = non_conformity["Exigence IFS Food 8"]
513
- auditor_explanation = non_conformity["Explication (par l'auditeur/l'évaluateur)"]
514
-
515
- # Version simplifiée qui renvoie un template de recommandation
516
- if language == "fr":
517
- # Template en français
518
- recommendation = f"""
519
- ## ANALYSE DE LA NON-CONFORMITÉ
520
- La non-conformité concerne l'exigence {requirement_num} qui stipule: "{requirement_text}".
521
- Le constat de l'auditeur indique: "{auditor_explanation}"
522
-
523
- ## ANALYSE DES CAUSES
524
- Les causes probables de cette non-conformité incluent:
525
- - Manque de procédures documentées
526
- - Formation insuffisante du personnel
527
- - Ressources inadéquates allouées à ce processus
528
-
529
- ## PLAN D'ACTION
530
- ### Actions immédiates (corrections)
531
- 1. Réaliser un état des lieux complet
532
- 2. Mettre en place une solution temporaire immédiate
533
- 3. Informer et former rapidement le personnel concerné
534
-
535
- ### Type de preuves à fournir
536
- - Photos de la mise en conformité
537
- - Procédures mises à jour
538
- - Feuilles d'émargement des formations
539
- - Rapports de vérification
540
-
541
- ### Actions correctives à long terme
542
- 1. Réviser la procédure complète
543
- 2. Mettre en place un plan de formation régulier
544
- 3. Établir des contrôles périodiques
545
- 4. Intégrer ce point dans les audits internes
546
-
547
- ## MÉTHODES DE VALIDATION DE L'EFFICACITÉ
548
- - Audits internes ciblés
549
- - Indicateurs de performance à suivre mensuellement
550
- - Revue de direction trimestrielle sur ce sujet
551
-
552
- ## RECOMMANDATIONS COMPLÉMENTAIRES
553
- - Envisager une approche plus globale de ce processus
554
- - Évaluer l'impact sur d'autres exigences liées
555
- - Prévoir une revue systématique annuelle
556
- """
557
- else:
558
- # Template en anglais
559
- recommendation = f"""
560
- ## ANALYSIS OF THE NON-CONFORMITY
561
- The non-conformity relates to requirement {requirement_num} which states: "{requirement_text}".
562
- The auditor's finding indicates: "{auditor_explanation}"
563
-
564
- ## ROOT CAUSE ANALYSIS
565
- The probable causes of this non-conformity include:
566
- - Lack of documented procedures
567
- - Insufficient staff training
568
- - Inadequate resources allocated to this process
569
-
570
- ## ACTION PLAN
571
- ### Immediate actions (corrections)
572
- 1. Conduct a complete assessment
573
- 2. Implement immediate temporary solution
574
- 3. Rapidly inform and train relevant staff
575
-
576
- ### Type of evidence to be provided
577
- - Photos of compliance implementation
578
- - Updated procedures
579
- - Training attendance records
580
- - Verification reports
581
-
582
- ### Long-term corrective actions
583
- 1. Revise the complete procedure
584
- 2. Establish a regular training program
585
- 3. Set up periodic controls
586
- 4. Integrate this point into internal audits
587
-
588
- ## METHODS FOR VALIDATING EFFECTIVENESS
589
- - Targeted internal audits
590
- - Performance indicators to be monitored monthly
591
- - Quarterly management review on this topic
592
-
593
- ## ADDITIONAL RECOMMENDATIONS
594
- - Consider a more comprehensive approach to this process
595
- - Evaluate the impact on other related requirements
596
- - Plan for a systematic annual review
597
- """
598
-
599
- # Ajouter à l'historique
600
- history_entry = {
601
- "action": "generate_recommendation_local",
602
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
603
- "role": st.session_state['role'],
604
- "details": f"Génération de recommandation locale pour l'exigence {requirement_num}"
605
  }
606
- st.session_state['history'].append(history_entry)
607
-
608
- return recommendation
609
-
610
- # Fonction pour afficher l'historique - CORRIGÉ
611
- def display_history():
612
- if st.session_state['history']:
613
- with st.expander("📜 Historique des actions", expanded=False):
614
- for entry in sorted(st.session_state['history'], key=lambda x: x["timestamp"], reverse=True):
615
- timestamp = entry['timestamp']
616
- details = entry['details']
617
- role = entry['role']
618
- history_entry = f"**{timestamp}** - {details} (par {role})"
619
- st.markdown(history_entry)
620
-
621
- # Afficher les pièces jointes - CORRIGÉ
622
- def display_attachments(index, readonly=False):
623
- if index in st.session_state['attachments'] and st.session_state['attachments'][index]:
624
- st.markdown("#### 📎 Pièces jointes")
625
-
626
- attachments = st.session_state['attachments'][index]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  cols = st.columns(4)
628
-
629
- for i, (filename, file_data) in enumerate(attachments.items()):
630
  col = cols[i % 4]
631
-
632
- # Détecter le type de fichier
633
- file_extension = os.path.splitext(filename)[1].lower()
634
-
635
- if file_extension in ['.jpg', '.jpeg', '.png', '.gif']:
636
- # Pour les images - f-string HTML corrigé
637
- b64_data = base64.b64encode(file_data).decode()
638
- html_content = f"""
639
- <div class="attachment-thumbnail">
640
- <img src="data:image/{file_extension[1:]};base64,{b64_data}" width="100%">
641
- <div class="attachment-name">{filename}</div>
642
- </div>
643
- """
644
- st.markdown(html_content, unsafe_allow_html=True)
645
-
646
- # Bouton de téléchargement
647
- st.download_button(
648
- "📥",
649
- file_data,
650
- file_name=filename,
651
- mime=f"image/{file_extension[1:]}"
652
- )
653
- elif file_extension == '.pdf':
654
- # Pour les PDF - f-string HTML corrigé
655
- html_content = f"""
656
- <div class="attachment-thumbnail">
657
- <div style="text-align: center; font-size: 40px;">📄</div>
658
- <div class="attachment-name">{filename}</div>
659
- </div>
660
- """
661
- st.markdown(html_content, unsafe_allow_html=True)
662
-
663
- # Bouton de téléchargement
664
- st.download_button(
665
- "📥",
666
- file_data,
667
- file_name=filename,
668
- mime="application/pdf"
669
- )
670
- else:
671
- # Pour les autres fichiers - f-string HTML corrigé
672
- html_content = f"""
673
- <div class="attachment-thumbnail">
674
- <div style="text-align: center; font-size: 40px;">📎</div>
675
- <div class="attachment-name">{filename}</div>
676
- </div>
677
- """
678
- st.markdown(html_content, unsafe_allow_html=True)
679
-
680
  # Bouton de téléchargement
681
  st.download_button(
682
- "📥",
683
  file_data,
684
  file_name=filename
685
  )
 
 
 
 
 
 
686
 
687
- # Bouton de suppression (si non readonly)
688
- if not readonly:
689
- delete_button_key = f"delete_{index}_{filename}"
690
- if st.button("🗑️", key=delete_button_key):
691
- del st.session_state['attachments'][index][filename]
692
- # Ajouter à l'historique
693
- history_entry = {
694
- "action": "delete_attachment",
695
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
696
- "role": st.session_state['role'],
697
- "details": f"Suppression de la pièce jointe {filename} pour l'exigence {index}"
698
- }
699
- st.session_state['history'].append(history_entry)
700
- st.rerun()
 
 
 
 
 
 
 
 
 
 
701
 
702
  # Interface principale
703
  def main():
704
- # Sidebar pour la configuration
705
  with st.sidebar:
706
- st.markdown("### 👤 Rôle utilisateur")
707
-
708
  # Sélection du rôle
709
- col1, col2, col3 = st.columns(3)
710
-
711
- with col1:
712
- site_class = "role-tab-active" if st.session_state['role'] == "site" else "role-tab-inactive"
713
- # F-string HTML corrigé
714
- site_html = f"""
715
- <div class="role-tab {site_class}" id="role-site">
716
- <div style="font-size: 24px;">🏭</div>
717
- <div>Site Audité</div>
718
- </div>
719
- """
720
- st.markdown(site_html, unsafe_allow_html=True)
721
- if st.button("Site Audité", key="btn_site"):
722
- st.session_state['role'] = "site"
723
- st.rerun()
724
-
725
- with col2:
726
- auditor_class = "role-tab-active" if st.session_state['role'] == "auditor" else "role-tab-inactive"
727
- # F-string HTML corrigé
728
- auditor_html = f"""
729
- <div class="role-tab {auditor_class}" id="role-auditor">
730
- <div style="font-size: 24px;">🔍</div>
731
- <div>Auditeur</div>
732
- </div>
733
- """
734
- st.markdown(auditor_html, unsafe_allow_html=True)
735
- if st.button("Auditeur", key="btn_auditor"):
736
- st.session_state['role'] = "auditor"
737
- st.rerun()
738
-
739
- with col3:
740
- reviewer_class = "role-tab-active" if st.session_state['role'] == "reviewer" else "role-tab-inactive"
741
- # F-string HTML corrigé
742
- reviewer_html = f"""
743
- <div class="role-tab {reviewer_class}" id="role-reviewer">
744
- <div style="font-size: 24px;">✅</div>
745
- <div>Reviewer</div>
746
- </div>
747
- """
748
- st.markdown(reviewer_html, unsafe_allow_html=True)
749
- if st.button("Reviewer", key="btn_reviewer"):
750
- st.session_state['role'] = "reviewer"
751
- st.rerun()
752
-
753
- st.markdown("---")
754
-
755
- # API Key Groq (optionnelle)
756
- st.session_state['api_key'] = st.text_input(
757
- "Clé API Groq (optionnelle) :",
758
- type="password",
759
- value=st.session_state.get('api_key', '')
760
  )
761
-
762
- # Metadata form en fonction du rôle
763
- st.markdown("### 📋 Informations d'audit")
764
-
 
 
765
  with st.form(key="metadata_form"):
 
 
766
  if st.session_state['role'] == "site":
767
  st.session_state['audit_metadata']["site_name"] = st.text_input(
768
- "Nom du site :",
769
  value=st.session_state['audit_metadata'].get("site_name", "")
770
  )
771
- audit_date_value = datetime.strptime(
772
- st.session_state['audit_metadata'].get("audit_date", datetime.now().strftime("%Y-%m-%d")),
773
- "%Y-%m-%d"
774
- )
775
- st.session_state['audit_metadata']["audit_date"] = st.date_input(
776
- "Date de l'audit :",
777
- value=audit_date_value
778
- ).strftime("%Y-%m-%d")
779
-
780
- elif st.session_state['role'] == "auditor":
781
  st.session_state['audit_metadata']["auditor_name"] = st.text_input(
782
- "Nom de l'auditeur :",
783
  value=st.session_state['audit_metadata'].get("auditor_name", "")
784
  )
785
-
786
- elif st.session_state['role'] == "reviewer":
787
  st.session_state['audit_metadata']["reviewer_name"] = st.text_input(
788
- "Nom du reviewer :",
789
  value=st.session_state['audit_metadata'].get("reviewer_name", "")
790
  )
791
-
792
  st.form_submit_button("Enregistrer")
793
-
794
- st.markdown("---")
795
-
796
- # Actions spécifiques au rôle
797
  if st.session_state['role'] == "site":
798
- st.markdown("### 📤 Plan d'Action")
799
- uploaded_file = st.file_uploader("Fichier Excel du plan d'action :", type=["xlsx"])
800
-
801
  if uploaded_file:
802
- action_plan_df = load_action_plan(uploaded_file)
803
- if action_plan_df is not None:
804
- st.session_state['action_plan_df'] = action_plan_df
805
- st.success(" Plan d'action chargé avec succès")
806
-
807
- # Sauvegarde/Chargement de session pour tous les rôles
808
- st.markdown("### 💾 Gestion du dossier")
809
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
  col1, col2 = st.columns(2)
811
-
812
  with col1:
813
- if st.button("💾 Exporter .auditpack"):
814
- if 'action_plan_df' in st.session_state and st.session_state['action_plan_df'] is not None:
815
- # Mettre à jour le statut de validation selon le rôle
816
- st.session_state['audit_metadata']["validation"][st.session_state['role']] = True
817
-
818
- # Créer l'auditpack
819
- auditpack_data = create_auditpack()
820
-
821
- # Créer un lien de téléchargement
822
- timestamp = time.strftime("%Y%m%d-%H%M%S")
823
- site_name = st.session_state['audit_metadata']["site_name"].replace(" ", "_") or "audit"
824
- file_name = f"{site_name}_audit_{timestamp}.auditpack"
825
-
826
- st.download_button(
827
- label="📥 Télécharger le fichier",
828
- data=auditpack_data,
829
- file_name=file_name,
830
- mime="application/zip"
831
- )
832
-
833
- # Ajouter à l'historique
834
- history_entry = {
835
- "action": "export_auditpack",
836
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
837
- "role": st.session_state['role'],
838
- "details": f"Export du fichier auditpack par {st.session_state['role']}"
839
- }
840
- st.session_state['history'].append(history_entry)
841
- else:
842
- st.warning("Chargez d'abord un plan d'action.")
843
-
844
  with col2:
845
- uploaded_auditpack = st.file_uploader("📂 Importer .auditpack", type=["auditpack", "zip"])
846
- if uploaded_auditpack is not None:
847
- file_content = uploaded_auditpack.getvalue()
848
- if load_auditpack(file_content):
849
- st.success("✅ Dossier d'audit chargé avec succès")
850
- st.rerun()
851
-
852
- st.markdown("---")
853
-
854
- # Statistiques
855
- if 'action_plan_df' in st.session_state and st.session_state['action_plan_df'] is not None:
856
- st.markdown("### 📈 Progression")
857
-
858
- total = len(st.session_state['action_plan_df'])
859
- completed = sum(st.session_state['action_plan_df']["Statut"] == "Complété")
860
- in_progress = sum(st.session_state['action_plan_df']["Statut"] == "En cours")
861
-
862
- st.progress(completed / total if total > 0 else 0)
863
-
864
- # Calcul du pourcentage avec sécurité
865
- completed_percent = int((completed/total)*100) if total > 0 else 0
866
- progress_text = f"**{completed}/{total}** complétées ({completed_percent}%)"
867
- st.markdown(progress_text)
868
-
869
- # Afficher les statuts de validation
870
- st.markdown("### ✅ Validation")
871
-
872
- validation = st.session_state['audit_metadata']["validation"]
873
-
874
- site_status = "✓" if validation.get("site", False) else "✗"
875
- auditor_status = "✓" if validation.get("auditor", False) else "✗"
876
- reviewer_status = "✓" if validation.get("reviewer", False) else "✗"
877
-
878
- validation_text = f"""
879
- - Site: {site_status}
880
- - Auditeur: {auditor_status}
881
- - Reviewer: {reviewer_status}
882
- """
883
- st.markdown(validation_text)
884
-
885
- # Afficher la bannière dans la page principale
886
- st.markdown('<div class="banner"></div>', unsafe_allow_html=True)
887
-
888
- # Afficher le titre avec indication du rôle - CORRIG��
889
- role_dict = {
890
- "site": "Site Audité",
891
- "auditor": "Auditeur",
892
- "reviewer": "Reviewer"
893
- }
894
- role_name = role_dict.get(st.session_state['role'], "")
895
- title_html = f'<div class="main-header">📊 VisiPilot - Plan d\'Actions IFS Food 8 [{role_name}]</div>'
896
- st.markdown(title_html, unsafe_allow_html=True)
897
-
898
- # Afficher l'historique
899
- display_history()
900
-
901
- # Section principale - contenu spécifique au rôle
902
- if 'action_plan_df' not in st.session_state or st.session_state['action_plan_df'] is None:
903
- st.info("👈 Commencez par configurer les informations d'audit et charger un plan d'action dans la barre latérale.")
904
- else:
905
  # Filtres
 
906
  col1, col2 = st.columns([1, 2])
 
907
  with col1:
908
- status_filter = st.multiselect("Filtrer par statut",
909
- options=["Tous", "Non traité", "En cours", "Complété"],
910
- default=["Tous"])
 
 
 
 
911
  with col2:
912
- search_term = st.text_input("Rechercher une exigence", "")
913
-
914
  # Appliquer les filtres
915
- filtered_df = st.session_state['action_plan_df'].copy()
916
-
917
  if "Tous" not in status_filter:
918
- filtered_df = filtered_df[filtered_df["Statut"].isin(status_filter)]
919
-
920
  if search_term:
921
- filtered_df = filtered_df[
922
- filtered_df["Exigence IFS Food 8"].str.contains(search_term, case=False) |
923
- filtered_df["Numéro d'exigence"].astype(str).str.contains(search_term, case=False)
924
- ]
925
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  # Afficher les non-conformités
927
- for index, row in filtered_df.iterrows():
 
 
928
  # Déterminer la classe du badge de statut
929
- status_class_dict = {
930
- "Complété": "status-completed",
931
  "En cours": "status-progress",
932
- "Non traité": "status-pending"
933
- }
934
- status_class = status_class_dict.get(row["Statut"], "status-pending")
935
-
936
- # Afficher la carte de non-conformité - CORRIGÉ
937
- requirement_num = row["Numéro d'exigence"]
938
- requirement_text = row["Exigence IFS Food 8"]
939
- auditor_explanation = row["Explication (par l'auditeur/l'évaluateur)"]
940
- status = row["Statut"]
941
-
942
- card_html = f"""
943
  <div class="card">
944
- <div>
945
- <strong>Exigence {requirement_num}</strong>
946
- <span class="status-badge {status_class}">{status}</span>
947
- </div>
948
- <div style="margin-top: 5px;">
949
- {requirement_text}
950
- </div>
951
- <div style="font-style: italic; margin-top: 5px; color: #666;">
952
- <strong>Constat de l'auditeur:</strong> {auditor_explanation}
953
  </div>
 
 
954
  </div>
955
- """
956
- st.markdown(card_html, unsafe_allow_html=True)
957
-
958
- # Options d'action selon le rôle
959
  if st.session_state['role'] == "site":
960
- # Boutons pour le site
961
- col1, col2, col3 = st.columns([1, 1, 1])
962
-
963
- with col1:
964
- button_key = f"questions_{index}"
965
- if st.button("💡 Questions ciblées", key=button_key):
966
- st.session_state['active_item'] = index
967
- st.session_state['ask_questions'][index] = True
968
-
969
- with col2:
970
- button_key = f"direct_{index}"
971
- if st.button("🤖 Recommandation directe", key=button_key):
972
- with st.spinner("Génération en cours..."):
973
- recommendation = generate_recommendation(row, direct=True)
974
- if recommendation:
975
- st.session_state['recommendations'][str(index)] = recommendation
976
- st.session_state['active_item'] = index
977
- # Mettre à jour le statut
978
- st.session_state['action_plan_df'].loc[index, "Statut"] = "En cours"
979
- st.rerun()
980
-
981
- with col3:
982
- # Téléchargement de pièces jointes
983
- upload_key = f"upload_{index}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  uploaded_files = st.file_uploader(
985
- "📎 Ajouter des preuves",
986
- type=["pdf", "jpg", "jpeg", "png", "docx", "xlsx"],
987
  accept_multiple_files=True,
988
- key=upload_key
989
  )
990
-
991
  if uploaded_files:
992
- # Initialiser le dictionnaire pour l'index si nécessaire
993
- if str(index) not in st.session_state['attachments']:
994
- st.session_state['attachments'][str(index)] = {}
995
-
996
- for uploaded_file in uploaded_files:
997
- file_data = uploaded_file.read() # Lire les données binaires
998
- file_name = uploaded_file.name # Récupérer le nom de fichier
999
-
1000
- # Stocker le fichier
1001
- st.session_state['attachments'][str(index)][file_name] = file_data
1002
-
1003
- # Ajouter à l'historique
1004
- history_entry = {
1005
- "action": "upload_attachment",
1006
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1007
- "role": st.session_state['role'],
1008
- "details": f"Ajout de la pièce jointe {file_name} pour l'exigence {index}"
1009
- }
1010
- st.session_state['history'].append(history_entry)
1011
-
1012
- success_message = f"{len(uploaded_files)} fichiers ajoutés"
1013
- st.success(success_message)
1014
-
1015
- elif st.session_state['role'] == "auditor":
1016
- # Boutons pour l'auditeur
1017
- col1, col2 = st.columns(2)
1018
-
1019
- with col1:
1020
- # Ajouter commentaire
1021
- button_key = f"comment_{index}"
1022
- if st.button("💬 Ajouter commentaire", key=button_key):
1023
- st.session_state['active_item'] = index
1024
-
1025
- with col2:
1026
- # Valider action
1027
- button_key = f"validate_{index}"
1028
- if st.button("✅ Valider action", key=button_key):
1029
- # Ajouter à l'historique
1030
- history_entry = {
1031
- "action": "validate_item",
1032
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1033
- "role": st.session_state['role'],
1034
- "details": f"Validation de l'action pour l'exigence {index} par l'auditeur"
1035
- }
1036
- st.session_state['history'].append(history_entry)
1037
- # Mettre à jour le statut
1038
- st.session_state['action_plan_df'].loc[index, "Statut"] = "Complété"
1039
  st.rerun()
1040
-
1041
- elif st.session_state['role'] == "reviewer":
1042
- # Boutons pour le reviewer
1043
- col1, col2 = st.columns(2)
1044
-
1045
- with col1:
1046
- # Ajouter commentaire
1047
- button_key = f"review_comment_{index}"
1048
- if st.button("💬 Ajouter commentaire", key=button_key):
1049
- st.session_state['active_item'] = index
1050
-
1051
- with col2:
1052
- # Approuver final
1053
- button_key = f"approve_{index}"
1054
- if st.button("✅ Approuver final", key=button_key):
1055
- # Ajouter à l'historique
1056
- history_entry = {
1057
- "action": "approve_final",
1058
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1059
- "role": st.session_state['role'],
1060
- "details": f"Approbation finale pour l'exigence {index} par le reviewer"
1061
- }
1062
- st.session_state['history'].append(history_entry)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  st.rerun()
1064
-
1065
- # Options de statut visibles pour tous les rôles
1066
- status_options = ["Non traité", "En cours", "Complété"]
1067
- select_key = f"status_{index}"
1068
- new_status = st.selectbox(
1069
- "Statut",
1070
- options=status_options,
1071
- index=status_options.index(row["Statut"]),
1072
- key=select_key
1073
- )
1074
-
1075
- if new_status != row["Statut"]:
1076
- st.session_state['action_plan_df'].loc[index, "Statut"] = new_status
1077
- # Ajouter à l'historique
1078
- history_entry = {
1079
- "action": "change_status",
1080
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1081
- "role": st.session_state['role'],
1082
- "details": f"Changement de statut pour l'exigence {index} : {row['Statut']} -> {new_status}"
1083
- }
1084
- st.session_state['history'].append(history_entry)
1085
-
1086
- # Si cet élément est actif pour les questions (site uniquement)
1087
- if st.session_state['role'] == "site" and index == st.session_state.get('active_item') and st.session_state['ask_questions'].get(index, False):
1088
- with st.container():
1089
- st.markdown("#### Questions pour analyse ciblée")
1090
-
1091
- # Générer des questions adaptées
1092
- questions = generate_questions(row)
1093
-
1094
- form_key = f"questions_form_{index}"
1095
- with st.form(key=form_key):
1096
- responses = {}
1097
- for q in questions:
1098
- question_key = f"q_{index}_{q['id']}"
1099
- responses[q["id"]] = st.text_area(q["question"], key=question_key)
1100
-
1101
- submit_btn = st.form_submit_button("Générer recommandation")
1102
- if submit_btn:
1103
- with st.spinner("Génération en cours..."):
1104
- recommendation = generate_recommendation(row, responses)
1105
- if recommendation:
1106
- st.session_state['recommendations'][str(index)] = recommendation
1107
- st.session_state['responses'][str(index)] = responses
1108
- # Mettre à jour le statut
1109
- st.session_state['action_plan_df'].loc[index, "Statut"] = "En cours"
1110
- st.rerun()
1111
-
1112
- # Si cet élément est actif pour ajouter un commentaire (auditeur ou reviewer)
1113
- if index == st.session_state.get('active_item') and st.session_state['role'] in ["auditor", "reviewer"]:
1114
- with st.container():
1115
- st.markdown("#### Ajouter un commentaire")
1116
-
1117
- form_key = f"comment_form_{index}"
1118
- with st.form(key=form_key):
1119
- comment_key = f"comment_{index}_text"
1120
- comment_text = st.text_area("Votre commentaire:", key=comment_key)
1121
-
1122
- submit_btn = st.form_submit_button("Enregistrer le commentaire")
1123
- if submit_btn and comment_text:
1124
- # Initialiser la liste de commentaires pour cet index si nécessaire
1125
- if str(index) not in st.session_state['comments']:
1126
- st.session_state['comments'][str(index)] = []
1127
-
1128
- # Ajouter le commentaire
1129
- comment_data = {
1130
  "role": st.session_state['role'],
1131
  "text": comment_text,
1132
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1133
- }
1134
- st.session_state['comments'][str(index)].append(comment_data)
1135
-
1136
- # Ajouter à l'historique
1137
- history_entry = {
1138
- "action": "add_comment",
1139
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1140
  "role": st.session_state['role'],
1141
- "details": f"Ajout d'un commentaire pour l'exigence {index}"
1142
- }
1143
- st.session_state['history'].append(history_entry)
1144
-
1145
- st.success("Commentaire ajouté avec succès")
1146
- st.session_state['active_item'] = None
 
 
 
1147
  st.rerun()
1148
-
1149
- # Afficher les pièces jointes
1150
- if str(index) in st.session_state['attachments']:
1151
- display_attachments(str(index), readonly=(st.session_state['role'] != "site"))
1152
-
1153
- # Afficher les commentaires s'il y en a
1154
- if str(index) in st.session_state['comments'] and st.session_state['comments'][str(index)]:
1155
- st.markdown("#### 💬 Commentaires")
1156
-
1157
- for comment in st.session_state['comments'][str(index)]:
1158
- role_colors = {
1159
- "site": "#28a745",
1160
- "auditor": "#0066cc",
1161
- "reviewer": "#6f42c1"
1162
- }
1163
- role_color = role_colors.get(comment["role"], "#6c757d")
1164
-
1165
- # f-string HTML corrigé
1166
- comment_html = f"""
1167
- <div style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; border-left: 3px solid {role_color};">
1168
- <div style="display: flex; justify-content: space-between;">
1169
- <span style="font-weight: bold; color: {role_color};">{comment["role"].capitalize()}</span>
1170
- <span style="font-size: 0.8em; color: #6c757d;">{comment["timestamp"]}</span>
1171
- </div>
1172
- <div style="margin-top: 5px;">{comment["text"]}</div>
1173
- </div>
1174
- """
1175
- st.markdown(comment_html, unsafe_allow_html=True)
1176
-
1177
- # Afficher la recommandation si elle existe
1178
- if str(index) in st.session_state.get('recommendations', {}):
1179
- is_expanded = index == st.session_state.get('active_item')
1180
- with st.expander("📋 Recommandation IA", expanded=is_expanded):
1181
- # Appliquer la classe personnalisée à l'expander via HTML
1182
- st.markdown('<div class="recommendation-expander">', unsafe_allow_html=True)
1183
- st.markdown(st.session_state['recommendations'][str(index)])
1184
- st.markdown('</div>', unsafe_allow_html=True)
1185
-
1186
  st.markdown("---")
 
 
 
1187
 
 
1188
  if __name__ == "__main__":
1189
- main()
1190
-
 
1
  import streamlit as st
2
  import pandas as pd
 
 
 
 
 
 
 
3
  import io
4
+ import zipfile
5
  import base64
6
+ import uuid
7
+ import os
8
  from datetime import datetime
 
9
 
10
  # Configuration de la page
11
  st.set_page_config(
12
+ page_title="Plan d'Actions IFS Food 8",
13
+ page_icon="📋",
14
  layout="wide",
15
  initial_sidebar_state="expanded"
16
  )
 
18
  # Styles CSS
19
  st.markdown("""
20
  <style>
21
+ .header {
22
+ font-size: 24px;
23
  font-weight: bold;
24
+ color: #1E3D59;
25
  text-align: center;
26
  margin-bottom: 20px;
27
  padding: 10px 0;
28
  border-bottom: 2px solid #e0f7fa;
29
  }
 
 
 
 
 
 
 
 
30
  .card {
31
  padding: 15px;
32
+ border-radius: 5px;
33
  background-color: #f9f9f9;
34
  margin-bottom: 15px;
35
+ border-left: 3px solid #1E3D59;
36
  }
37
  .status-badge {
38
  display: inline-block;
 
54
  background-color: #dc3545;
55
  color: white;
56
  }
57
+ .attachment {
58
+ padding: 5px;
59
+ margin: 5px;
60
+ border: 1px solid #ddd;
61
+ border-radius: 4px;
62
+ display: inline-block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
+ .section-title {
65
+ color: #1E3D59;
66
+ font-weight: bold;
67
+ margin-top: 10px;
68
+ margin-bottom: 5px;
69
  }
70
+ .section-content {
71
+ padding: 10px;
72
+ background-color: #f5f5f5;
73
+ border-radius: 5px;
74
+ margin-bottom: 10px;
75
  }
76
+ .info-text {
77
+ font-style: italic;
78
+ color: #666;
79
  }
80
+ .requirement-text {
81
+ font-weight: bold;
 
 
 
 
 
 
 
82
  }
83
+ .finding-text {
84
+ font-style: italic;
85
+ border-left: 2px solid #ffc107;
86
+ padding-left: 10px;
87
+ margin: 10px 0;
 
 
88
  }
89
  </style>
90
  """, unsafe_allow_html=True)
 
92
  # Initialiser les états de session
93
  if 'role' not in st.session_state:
94
  st.session_state['role'] = "site" # site, auditor, reviewer
95
+ if 'audit_data' not in st.session_state:
96
+ st.session_state['audit_data'] = None
 
 
 
 
 
 
97
  if 'comments' not in st.session_state:
98
  st.session_state['comments'] = {}
99
+ if 'attachments' not in st.session_state:
100
+ st.session_state['attachments'] = {}
101
  if 'audit_metadata' not in st.session_state:
102
  st.session_state['audit_metadata'] = {
103
  "audit_id": str(uuid.uuid4()),
 
105
  "site_name": "",
106
  "auditor_name": "",
107
  "reviewer_name": "",
108
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
 
 
109
  }
 
 
110
  if 'active_item' not in st.session_state:
111
  st.session_state['active_item'] = None
112
+ if 'excel_metadata' not in st.session_state:
113
+ st.session_state['excel_metadata'] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # Fonction pour extraire les métadonnées du fichier Excel
116
+ def extract_excel_metadata(uploaded_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  try:
118
+ # Lire les premières lignes pour extraire les métadonnées
119
+ metadata_df = pd.read_excel(uploaded_file, header=None, nrows=10)
120
+
121
+ # Créer un dictionnaire pour stocker les métadonnées
122
+ metadata = {}
123
+
124
+ # Extraire les informations pertinentes (adapter selon la structure exacte)
125
+ if len(metadata_df) >= 1 and len(metadata_df.columns) >= 2:
126
+ # Ligne 1: Entreprise et adresse
127
+ enterprise_info = metadata_df.iloc[0, 0] if not pd.isna(metadata_df.iloc[0, 0]) else ""
128
+ metadata["enterprise"] = enterprise_info
129
+
130
+ # Ligne 2: Référentiel
131
+ if len(metadata_df) >= 2:
132
+ standard_info = metadata_df.iloc[1, 0] if not pd.isna(metadata_df.iloc[1, 0]) else ""
133
+ metadata["standard"] = standard_info
134
+
135
+ # Ligne 3: Type d'audit
136
+ if len(metadata_df) >= 3:
137
+ audit_type = metadata_df.iloc[2, 0] if not pd.isna(metadata_df.iloc[2, 0]) else ""
138
+ metadata["audit_type"] = audit_type
139
+
140
+ # Ligne 4: Date d'audit
141
+ if len(metadata_df) >= 4:
142
+ audit_date = metadata_df.iloc[3, 0] if not pd.isna(metadata_df.iloc[3, 0]) else ""
143
+ metadata["audit_date"] = audit_date
144
+
145
+ return metadata
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  except Exception as e:
147
+ st.error(f"Erreur lors de l'extraction des métadonnées: {str(e)}")
148
+ return {}
 
149
 
150
+ # Fonction pour charger les données d'audit
151
+ def load_audit_data(uploaded_file):
152
  try:
153
+ # Extraire d'abord les métadonnées
154
+ metadata = extract_excel_metadata(uploaded_file)
155
+ st.session_state['excel_metadata'] = metadata
156
+
157
+ # Charger les données d'Excel - en sautant les lignes d'en-tête (les 11 premières lignes)
158
+ df = pd.read_excel(uploaded_file, header=11)
159
+
160
+ # Sélectionner uniquement les colonnes pertinentes
161
+ columns_to_use = [
162
+ "requirementNo", "requirementText", "requirementScore", "requirementExplanation",
163
+ "correctionDescription", "correctionResponsibility", "correctionDueDate", "correctionStatus",
164
+ "correctionEvidence", "correctiveActionDescription", "correctiveActionResponsibility",
165
+ "correctiveActionDueDate", "correctiveActionStatus", "releaseResponsibility", "releaseDate"
166
+ ]
167
+
168
+ # Vérifier que toutes les colonnes existent
169
+ existing_columns = [col for col in columns_to_use if col in df.columns]
170
+ df = df[existing_columns]
171
+
172
+ # Création du mapping de colonnes
173
+ column_mapping = {
174
+ "requirementNo": "reference",
175
+ "requirementText": "requirement",
176
+ "requirementScore": "score",
177
+ "requirementExplanation": "finding",
178
+ "correctionDescription": "correction",
179
+ "correctionResponsibility": "correction_responsibility",
180
+ "correctionDueDate": "correction_due_date",
181
+ "correctionStatus": "correction_status",
182
+ "correctionEvidence": "evidence_type",
183
+ "correctiveActionDescription": "corrective_action",
184
+ "correctiveActionResponsibility": "corrective_action_responsibility",
185
+ "correctiveActionDueDate": "corrective_action_due_date",
186
+ "correctiveActionStatus": "corrective_action_status",
187
+ "releaseResponsibility": "release_responsibility",
188
+ "releaseDate": "release_date"
189
  }
190
+
191
+ # Renommer les colonnes
192
+ df = df.rename(columns=column_mapping)
193
+
194
+ # Ajouter un statut global pour notre application
195
+ if "status" not in df.columns:
196
+ # Initialiser le statut à "Non traité"
197
+ df["status"] = "Non traité"
198
+
199
+ # Si les valeurs pour correction ou action corrective sont remplies, statut "En cours"
200
+ in_progress_mask = df["correction"].notna() | df["corrective_action"].notna()
201
+ df.loc[in_progress_mask, "status"] = "En cours"
202
+
203
+ # Si les deux ont un statut "Completed", alors le statut global est "Complété"
204
+ completed_mask = (df["correction_status"] == "Completed") & (df["corrective_action_status"] == "Completed")
205
+ df.loc[completed_mask, "status"] = "Complété"
206
+
207
+ # Si une date de validation est présente, alors le statut est "Validé"
208
+ validated_mask = df["release_date"].notna()
209
+ df.loc[validated_mask, "status"] = "Validé"
210
+
211
+ # Ajouter une colonne pour l'analyse de cause racine si elle n'existe pas
212
+ if "root_cause" not in df.columns:
213
+ df["root_cause"] = ""
214
+
215
+ # Ajouter une colonne pour la méthode de vérification si elle n'existe pas
216
+ if "verification_method" not in df.columns:
217
+ df["verification_method"] = ""
218
+
219
+ # Supprimer les lignes vides (où le numéro d'exigence est vide)
220
+ df = df.dropna(subset=["reference"])
221
+
222
+ return df
223
  except Exception as e:
224
+ st.error(f"Erreur lors du chargement des données: {str(e)}")
 
225
  return None
226
 
227
+ # Fonction pour créer un package d'export
228
+ def create_export_package():
229
+ # Préparer les données pour l'export
230
+ export_data = {
231
+ "metadata": st.session_state['audit_metadata'],
232
+ "excel_metadata": st.session_state['excel_metadata'],
233
+ "audit_data": st.session_state['audit_data'].to_dict('records') if st.session_state['audit_data'] is not None else [],
234
+ "comments": st.session_state['comments']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
+
237
+ # Créer un fichier ZIP en mémoire
238
+ buffer = io.BytesIO()
239
+
240
+ with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
241
+ # Créer un DataFrame pour l'export
242
+ export_df = st.session_state['audit_data'].copy()
243
+
244
+ # Renommer les colonnes pour correspondre au format d'origine
245
+ reverse_mapping = {
246
+ "reference": "requirementNo",
247
+ "requirement": "requirementText",
248
+ "score": "requirementScore",
249
+ "finding": "requirementExplanation",
250
+ "correction": "correctionDescription",
251
+ "correction_responsibility": "correctionResponsibility",
252
+ "correction_due_date": "correctionDueDate",
253
+ "correction_status": "correctionStatus",
254
+ "evidence_type": "correctionEvidence",
255
+ "corrective_action": "correctiveActionDescription",
256
+ "corrective_action_responsibility": "correctiveActionResponsibility",
257
+ "corrective_action_due_date": "correctiveActionDueDate",
258
+ "corrective_action_status": "correctiveActionStatus",
259
+ "release_responsibility": "releaseResponsibility",
260
+ "release_date": "releaseDate"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
+
263
+ export_df = export_df.rename(columns=reverse_mapping)
264
+
265
+ # Ajouter l'en-tête (à adapter selon le format exact)
266
+ header_df = pd.DataFrame({
267
+ "Action plan": [st.session_state['excel_metadata'].get("enterprise", "")],
268
+ "": [""],
269
+ "Référentiel / Programme / Check": [st.session_state['excel_metadata'].get("standard", "IFS Food 8")],
270
+ "Type d'audit/d'évaluation": [st.session_state['excel_metadata'].get("audit_type", "")],
271
+ "Date de début d'audit / d'évaluation": [st.session_state['excel_metadata'].get("audit_date", "")]
272
+ })
273
+
274
+ # Créer un DataFrame avec les titres des colonnes en français et anglais
275
+ column_titles = pd.DataFrame({
276
+ "requirementNo": ["Numéro d'exigence"],
277
+ "requirementText": ["Exigence IFS Food 8"],
278
+ "requirementScore": ["Notation"],
279
+ "requirementExplanation": ["Explication (par l'auditeur/l'évaluateur)"],
280
+ "correctionDescription": ["Correction (par l'entreprise)"],
281
+ "correctionResponsibility": ["Responsabilité (par l'entreprise)"],
282
+ "correctionDueDate": ["Date (par l'entreprise)"],
283
+ "correctionStatus": ["Statut de la mise en œuvre (par l'entreprise)"],
284
+ "correctionEvidence": ["Type de preuve(s) et nom du/des document(s)"],
285
+ "correctiveActionDescription": ["Action corrective (par l'entreprise)"],
286
+ "correctiveActionResponsibility": ["Responsabilité (par l'entreprise)"],
287
+ "correctiveActionDueDate": ["Date (par l'entreprise)"],
288
+ "correctiveActionStatus": ["Statut de la mise en œuvre (par l'entreprise)"],
289
+ "releaseResponsibility": ["Effectué par (l'auditeur/l'évaluateur)"],
290
+ "releaseDate": ["Date de transmission"]
291
+ })
292
+
293
+ # Créer un fichier Excel en mémoire
294
+ excel_buffer = io.BytesIO()
295
+ with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
296
+ header_df.to_excel(writer, sheet_name='Plan d\'action', index=False)
297
+ # Laisser des lignes vides
298
+ column_titles.to_excel(writer, sheet_name='Plan d\'action', startrow=11, index=False)
299
+ export_df.to_excel(writer, sheet_name='Plan d\'action', startrow=13, index=False)
300
+
301
+ excel_buffer.seek(0)
302
+ zip_file.writestr('action_plan_updated.xlsx', excel_buffer.getvalue())
303
+
304
+ # Ajouter les pièces jointes
305
+ for ref, attachments in st.session_state['attachments'].items():
306
+ for filename, file_data in attachments.items():
307
+ zip_file.writestr(f'attachments/{ref}/{filename}', file_data)
308
+
309
+ # Ajouter un fichier de commentaires au format texte
310
+ if st.session_state['comments']:
311
+ comments_text = "COMMENTAIRES DU PLAN D'ACTION\n\n"
312
+ for ref, comments_list in st.session_state['comments'].items():
313
+ comments_text += f"Exigence {ref}:\n"
314
+ for comment in comments_list:
315
+ comments_text += f"- {comment['timestamp']} ({comment['role']}): {comment['text']}\n"
316
+ comments_text += "\n"
317
+
318
+ zip_file.writestr('comments.txt', comments_text)
319
+
320
+ buffer.seek(0)
321
+ return buffer.getvalue()
322
+
323
+ # Fonction pour afficher les pièces jointes
324
+ def display_attachments(reference, readonly=False):
325
+ if reference in st.session_state['attachments'] and st.session_state['attachments'][reference]:
326
+ st.markdown("<div class='section-title'>Pièces jointes</div>", unsafe_allow_html=True)
327
+
328
  cols = st.columns(4)
329
+
330
+ for i, (filename, file_data) in enumerate(st.session_state['attachments'][reference].items()):
331
  col = cols[i % 4]
332
+
333
+ with col:
334
+ # Déterminer le type de fichier
335
+ file_extension = os.path.splitext(filename)[1].lower()
336
+
337
+ # Afficher une icône différente selon le type de fichier
338
+ icon = "📄" # Par défaut
339
+ if file_extension in ['.jpg', '.jpeg', '.png', '.gif']:
340
+ icon = "🖼️"
341
+ elif file_extension == '.pdf':
342
+ icon = "📑"
343
+ elif file_extension in ['.doc', '.docx']:
344
+ icon = "📝"
345
+ elif file_extension in ['.xls', '.xlsx']:
346
+ icon = "📊"
347
+
348
+ # Afficher le nom du fichier
349
+ st.markdown(f"<div class='attachment'>{icon} {filename}</div>", unsafe_allow_html=True)
350
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  # Bouton de téléchargement
352
  st.download_button(
353
+ "Télécharger",
354
  file_data,
355
  file_name=filename
356
  )
357
+
358
+ # Bouton de suppression (si non readonly)
359
+ if not readonly and st.session_state['role'] == "site":
360
+ if st.button("Supprimer", key=f"delete_{reference}_{filename}"):
361
+ del st.session_state['attachments'][reference][filename]
362
+ st.rerun()
363
 
364
+ # Fonction pour afficher les commentaires
365
+ def display_comments(reference):
366
+ if reference in st.session_state['comments'] and st.session_state['comments'][reference]:
367
+ st.markdown("<div class='section-title'>Commentaires</div>", unsafe_allow_html=True)
368
+
369
+ for comment in st.session_state['comments'][reference]:
370
+ # Définir la couleur du rôle
371
+ role_colors = {
372
+ "site": "#28a745",
373
+ "auditor": "#0066cc",
374
+ "reviewer": "#6f42c1"
375
+ }
376
+ role_color = role_colors.get(comment["role"], "#6c757d")
377
+
378
+ # Afficher le commentaire
379
+ st.markdown(f"""
380
+ <div style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; border-left: 3px solid {role_color};">
381
+ <div style="display: flex; justify-content: space-between;">
382
+ <span style="font-weight: bold; color: {role_color};">{comment["role"].capitalize()}</span>
383
+ <span style="font-size: 0.8em; color: #6c757d;">{comment["timestamp"]}</span>
384
+ </div>
385
+ <div style="margin-top: 5px;">{comment["text"]}</div>
386
+ </div>
387
+ """, unsafe_allow_html=True)
388
 
389
  # Interface principale
390
  def main():
391
+ # Sidebar pour configuration
392
  with st.sidebar:
393
+ st.markdown("### Configuration")
394
+
395
  # Sélection du rôle
396
+ role_options = {
397
+ "site": "Site Audité",
398
+ "auditor": "Auditeur",
399
+ "reviewer": "Reviewer"
400
+ }
401
+
402
+ selected_role = st.selectbox(
403
+ "Rôle:",
404
+ options=list(role_options.keys()),
405
+ format_func=lambda x: role_options[x],
406
+ index=list(role_options.keys()).index(st.session_state['role'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  )
408
+
409
+ if selected_role != st.session_state['role']:
410
+ st.session_state['role'] = selected_role
411
+ st.rerun()
412
+
413
+ # Formulaire pour les métadonnées de l'audit
414
  with st.form(key="metadata_form"):
415
+ st.markdown("### Informations d'audit")
416
+
417
  if st.session_state['role'] == "site":
418
  st.session_state['audit_metadata']["site_name"] = st.text_input(
419
+ "Nom du site:",
420
  value=st.session_state['audit_metadata'].get("site_name", "")
421
  )
422
+
423
+ if st.session_state['role'] == "auditor":
 
 
 
 
 
 
 
 
424
  st.session_state['audit_metadata']["auditor_name"] = st.text_input(
425
+ "Nom de l'auditeur:",
426
  value=st.session_state['audit_metadata'].get("auditor_name", "")
427
  )
428
+
429
+ if st.session_state['role'] == "reviewer":
430
  st.session_state['audit_metadata']["reviewer_name"] = st.text_input(
431
+ "Nom du reviewer:",
432
  value=st.session_state['audit_metadata'].get("reviewer_name", "")
433
  )
434
+
435
  st.form_submit_button("Enregistrer")
436
+
437
+ # Upload du fichier d'audit (pour le rôle site uniquement)
 
 
438
  if st.session_state['role'] == "site":
439
+ st.markdown("### Charger les données d'audit")
440
+ uploaded_file = st.file_uploader("Fichier d'audit IFS Food 8:", type=["xlsx", "xls"])
441
+
442
  if uploaded_file:
443
+ audit_data = load_audit_data(uploaded_file)
444
+ if audit_data is not None:
445
+ st.session_state['audit_data'] = audit_data
446
+ st.success("Données d'audit chargées avec succès")
447
+ st.rerun()
448
+
449
+ # Export du rapport
450
+ if st.session_state['audit_data'] is not None:
451
+ st.markdown("### Export du plan d'actions")
452
+
453
+ if st.button("Exporter le rapport"):
454
+ export_data = create_export_package()
455
+
456
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
457
+ site_name = st.session_state['audit_metadata']["site_name"].replace(" ", "_") or "audit"
458
+
459
+ st.download_button(
460
+ "Télécharger le rapport",
461
+ export_data,
462
+ file_name=f"plan_actions_{site_name}_{timestamp}.zip",
463
+ mime="application/zip"
464
+ )
465
+
466
+ # Contenu principal
467
+ st.markdown('<div class="header">Plan d\'Actions IFS Food 8</div>', unsafe_allow_html=True)
468
+
469
+ # Afficher les informations d'audit
470
+ if st.session_state['excel_metadata']:
471
+ enterprise = st.session_state['excel_metadata'].get("enterprise", "")
472
+ standard = st.session_state['excel_metadata'].get("standard", "")
473
+ audit_type = st.session_state['excel_metadata'].get("audit_type", "")
474
+ audit_date = st.session_state['excel_metadata'].get("audit_date", "")
475
+
476
  col1, col2 = st.columns(2)
 
477
  with col1:
478
+ st.markdown(f"**Entreprise:** {enterprise}")
479
+ st.markdown(f"**Référentiel:** {standard}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  with col2:
481
+ st.markdown(f"**Type d'audit:** {audit_type}")
482
+ st.markdown(f"**Date d'audit:** {audit_date}")
483
+
484
+ # Afficher les non-conformités si les données sont chargées
485
+ if st.session_state['audit_data'] is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  # Filtres
487
+ st.markdown("### Filtrer les non-conformités")
488
  col1, col2 = st.columns([1, 2])
489
+
490
  with col1:
491
+ status_options = ["Tous", "Non traité", "En cours", "Complété", "Validé"]
492
+ status_filter = st.multiselect(
493
+ "Statut:",
494
+ options=status_options,
495
+ default=["Tous"]
496
+ )
497
+
498
  with col2:
499
+ search_term = st.text_input("Rechercher:", "")
500
+
501
  # Appliquer les filtres
502
+ filtered_data = st.session_state['audit_data'].copy()
503
+
504
  if "Tous" not in status_filter:
505
+ filtered_data = filtered_data[filtered_data["status"].isin(status_filter)]
506
+
507
  if search_term:
508
+ # Recherche dans toutes les colonnes textuelles
509
+ text_columns = filtered_data.select_dtypes(include='object').columns
510
+
511
+ mask = False
512
+ for column in text_columns:
513
+ mask = mask | filtered_data[column].str.contains(search_term, case=False, na=False)
514
+
515
+ filtered_data = filtered_data[mask]
516
+
517
+ # Afficher les statistiques
518
+ if len(filtered_data) > 0:
519
+ st.markdown("### Statistiques")
520
+
521
+ total = len(st.session_state['audit_data'])
522
+ completed = sum(st.session_state['audit_data']["status"] == "Complété")
523
+ in_progress = sum(st.session_state['audit_data']["status"] == "En cours")
524
+ validated = sum(st.session_state['audit_data']["status"] == "Validé")
525
+
526
+ col1, col2, col3, col4 = st.columns(4)
527
+
528
+ with col1:
529
+ st.metric("Total", str(total))
530
+
531
+ with col2:
532
+ st.metric("En cours", str(in_progress))
533
+
534
+ with col3:
535
+ st.metric("Complétés", str(completed))
536
+
537
+ with col4:
538
+ st.metric("Validés", str(validated))
539
+
540
+ progress = (completed + validated) / total if total > 0 else 0
541
+ st.progress(progress)
542
+
543
  # Afficher les non-conformités
544
+ st.markdown("### Non-conformités")
545
+
546
+ for index, row in filtered_data.iterrows():
547
  # Déterminer la classe du badge de statut
548
+ status_class = {
549
+ "Non traité": "status-pending",
550
  "En cours": "status-progress",
551
+ "Complété": "status-completed",
552
+ "Validé": "status-completed"
553
+ }.get(row["status"], "status-pending")
554
+
555
+ # Créer la carte de non-conformité
556
+ reference = str(row["reference"]) if "reference" in row else str(index)
557
+ requirement = row.get("requirement", "")
558
+ finding = row.get("finding", "")
559
+ score = row.get("score", "")
560
+
561
+ st.markdown(f"""
562
  <div class="card">
563
+ <div style="display: flex; justify-content: space-between;">
564
+ <div>
565
+ <strong>Exigence {reference}</strong>
566
+ <span class="status-badge {status_class}">{row["status"]}</span>
567
+ </div>
568
+ <div>
569
+ <strong>Notation: {score}</strong>
570
+ </div>
 
571
  </div>
572
+ <div class="requirement-text" style="margin-top: 10px;">{requirement}</div>
573
+ <div class="finding-text">{finding}</div>
574
  </div>
575
+ """, unsafe_allow_html=True)
576
+
577
+ # Options selon le rôle
 
578
  if st.session_state['role'] == "site":
579
+ # Le site peut ajouter des actions correctives et des pièces jointes
580
+ with st.expander("Répondre à cette non-conformité", expanded=st.session_state.get('active_item') == index):
581
+ with st.form(key=f"form_{index}"):
582
+ # Analyse de cause racine
583
+ root_cause = st.text_area(
584
+ "Analyse de cause racine:",
585
+ value=row.get("root_cause", ""),
586
+ height=80
587
+ )
588
+
589
+ # Correction immédiate
590
+ correction = st.text_area(
591
+ "Correction immédiate:",
592
+ value=row.get("correction", ""),
593
+ height=80
594
+ )
595
+
596
+ # Responsable de la correction
597
+ correction_responsibility = st.text_input(
598
+ "Responsable de la correction:",
599
+ value=row.get("correction_responsibility", "")
600
+ )
601
+
602
+ # Date prévue pour la correction
603
+ correction_due_date = st.date_input(
604
+ "Date prévue pour la correction:",
605
+ value=datetime.now()
606
+ )
607
+
608
+ # Statut de la correction
609
+ correction_status = st.selectbox(
610
+ "Statut de la correction:",
611
+ options=["Non démarré", "En cours", "Completed"],
612
+ index=0 if pd.isna(row.get("correction_status")) else ["Non démarré", "En cours", "Completed"].index(row.get("correction_status", "Non démarré"))
613
+ )
614
+
615
+ # Action corrective à long terme
616
+ corrective_action = st.text_area(
617
+ "Action corrective (prévention de la récurrence):",
618
+ value=row.get("corrective_action", ""),
619
+ height=80
620
+ )
621
+
622
+ # Responsable de l'action corrective
623
+ corrective_action_responsibility = st.text_input(
624
+ "Responsable de l'action corrective:",
625
+ value=row.get("corrective_action_responsibility", "")
626
+ )
627
+
628
+ # Date prévue pour l'action corrective
629
+ corrective_action_due_date = st.date_input(
630
+ "Date prévue pour l'action corrective:",
631
+ value=datetime.now()
632
+ )
633
+
634
+ # Statut de l'action corrective
635
+ corrective_action_status = st.selectbox(
636
+ "Statut de l'action corrective:",
637
+ options=["Non démarré", "En cours", "Completed"],
638
+ index=0 if pd.isna(row.get("corrective_action_status")) else ["Non démarré", "En cours", "Completed"].index(row.get("corrective_action_status", "Non démarré"))
639
+ )
640
+
641
+ # Méthode de vérification
642
+ verification_method = st.text_area(
643
+ "Méthode de vérification de l'efficacité:",
644
+ value=row.get("verification_method", ""),
645
+ height=60
646
+ )
647
+
648
+ # Type de preuves
649
+ evidence_type = st.text_input(
650
+ "Type de preuves et nom des documents:",
651
+ value=row.get("evidence_type", "")
652
+ )
653
+
654
+ submitted = st.form_submit_button("Enregistrer les actions")
655
+
656
+ if submitted:
657
+ st.session_state['audit_data'].at[index, "root_cause"] = root_cause
658
+ st.session_state['audit_data'].at[index, "correction"] = correction
659
+ st.session_state['audit_data'].at[index, "correction_responsibility"] = correction_responsibility
660
+ st.session_state['audit_data'].at[index, "correction_due_date"] = correction_due_date.strftime("%Y-%m-%d")
661
+ st.session_state['audit_data'].at[index, "correction_status"] = correction_status
662
+ st.session_state['audit_data'].at[index, "corrective_action"] = corrective_action
663
+ st.session_state['audit_data'].at[index, "corrective_action_responsibility"] = corrective_action_responsibility
664
+ st.session_state['audit_data'].at[index, "corrective_action_due_date"] = corrective_action_due_date.strftime("%Y-%m-%d")
665
+ st.session_state['audit_data'].at[index, "corrective_action_status"] = corrective_action_status
666
+ st.session_state['audit_data'].at[index, "verification_method"] = verification_method
667
+ st.session_state['audit_data'].at[index, "evidence_type"] = evidence_type
668
+
669
+ # Déterminer le statut global
670
+ if correction_status == "Completed" and corrective_action_status == "Completed":
671
+ st.session_state['audit_data'].at[index, "status"] = "Complété"
672
+ elif correction != "" or corrective_action != "":
673
+ st.session_state['audit_data'].at[index, "status"] = "En cours"
674
+
675
+ st.success("Actions enregistrées avec succès")
676
+ st.rerun()
677
+
678
+ # Upload de pièces jointes
679
  uploaded_files = st.file_uploader(
680
+ "Ajouter des pièces justificatives:",
 
681
  accept_multiple_files=True,
682
+ key=f"files_{index}"
683
  )
684
+
685
  if uploaded_files:
686
+ if reference not in st.session_state['attachments']:
687
+ st.session_state['attachments'][reference] = {}
688
+
689
+ for file in uploaded_files:
690
+ file_data = file.read()
691
+ filename = file.name
692
+
693
+ st.session_state['attachments'][reference][filename] = file_data
694
+
695
+ st.success(f"{len(uploaded_files)} fichier(s) ajouté(s)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  st.rerun()
697
+
698
+ elif st.session_state['role'] == "auditor":
699
+ # L'auditeur peut ajouter des commentaires et changer le statut
700
+ with st.expander("Évaluer cette action", expanded=st.session_state.get('active_item') == index):
701
+ # Afficher les informations sur les actions proposées
702
+ if row.get("root_cause", ""):
703
+ st.markdown("<div class='section-title'>Analyse de cause racine</div>", unsafe_allow_html=True)
704
+ st.markdown(f"<div class='section-content'>{row.get('root_cause', '')}</div>", unsafe_allow_html=True)
705
+
706
+ if row.get("correction", ""):
707
+ st.markdown("<div class='section-title'>Correction immédiate</div>", unsafe_allow_html=True)
708
+ st.markdown(f"<div class='section-content'>{row.get('correction', '')}</div>", unsafe_allow_html=True)
709
+
710
+ col1, col2 = st.columns(2)
711
+ with col1:
712
+ st.markdown(f"**Responsable:** {row.get('correction_responsibility', 'Non spécifié')}")
713
+ with col2:
714
+ st.markdown(f"**Date prévue:** {row.get('correction_due_date', 'Non spécifié')}")
715
+
716
+ st.markdown(f"**Statut:** {row.get('correction_status', 'Non spécifié')}")
717
+
718
+ if row.get("corrective_action", ""):
719
+ st.markdown("<div class='section-title'>Action corrective</div>", unsafe_allow_html=True)
720
+ st.markdown(f"<div class='section-content'>{row.get('corrective_action', '')}</div>", unsafe_allow_html=True)
721
+
722
+ col1, col2 = st.columns(2)
723
+ with col1:
724
+ st.markdown(f"**Responsable:** {row.get('corrective_action_responsibility', 'Non spécifié')}")
725
+ with col2:
726
+ st.markdown(f"**Date prévue:** {row.get('corrective_action_due_date', 'Non spécifié')}")
727
+
728
+ st.markdown(f"**Statut:** {row.get('corrective_action_status', 'Non spécifié')}")
729
+
730
+ if row.get("verification_method", ""):
731
+ st.markdown("<div class='section-title'>Méthode de vérification</div>", unsafe_allow_html=True)
732
+ st.markdown(f"<div class='section-content'>{row.get('verification_method', '')}</div>", unsafe_allow_html=True)
733
+
734
+ # Changer le statut
735
+ new_status = st.selectbox(
736
+ "Statut:",
737
+ options=["Non traité", "En cours", "Complété", "Validé"],
738
+ index=["Non traité", "En cours", "Complété", "Validé"].index(row["status"]) if row["status"] in ["Non traité", "En cours", "Complété", "Validé"] else 0,
739
+ key=f"status_{index}"
740
+ )
741
+
742
+ if new_status != row["status"]:
743
+ st.session_state['audit_data'].at[index, "status"] = new_status
744
+
745
+ # Si le statut est "Validé", ajouter la date et le responsable de validation
746
+ if new_status == "Validé":
747
+ st.session_state['audit_data'].at[index, "release_responsibility"] = st.session_state['audit_metadata']["auditor_name"]
748
+ st.session_state['audit_data'].at[index, "release_date"] = datetime.now().strftime("%Y-%m-%d")
749
+
750
  st.rerun()
751
+
752
+ # Ajouter un commentaire
753
+ with st.form(key=f"comment_{index}"):
754
+ comment_text = st.text_area("Ajouter un commentaire:", key=f"comment_text_{index}")
755
+
756
+ submitted = st.form_submit_button("Enregistrer le commentaire")
757
+
758
+ if submitted and comment_text:
759
+ if reference not in st.session_state['comments']:
760
+ st.session_state['comments'][reference] = []
761
+
762
+ st.session_state['comments'][reference].append({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  "role": st.session_state['role'],
764
  "text": comment_text,
765
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
766
+ })
767
+
768
+ st.success("Commentaire ajouté")
769
+ st.rerun()
770
+
771
+ elif st.session_state['role'] == "reviewer":
772
+ # Le reviewer peut valider ou rejeter l'action
773
+ with st.expander("Valider cette action", expanded=st.session_state.get('active_item') == index):
774
+ # Afficher les informations sur les actions proposées
775
+ if row.get("root_cause", ""):
776
+ st.markdown("<div class='section-title'>Analyse de cause racine</div>", unsafe_allow_html=True)
777
+ st.markdown(f"<div class='section-content'>{row.get('root_cause', '')}</div>", unsafe_allow_html=True)
778
+
779
+ if row.get("correction", ""):
780
+ st.markdown("<div class='section-title'>Correction immédiate</div>", unsafe_allow_html=True)
781
+ st.markdown(f"<div class='section-content'>{row.get('correction', '')}</div>", unsafe_allow_html=True)
782
+
783
+ col1, col2 = st.columns(2)
784
+ with col1:
785
+ st.markdown(f"**Responsable:** {row.get('correction_responsibility', 'Non spécifié')}")
786
+ with col2:
787
+ st.markdown(f"**Date prévue:** {row.get('correction_due_date', 'Non spécifié')}")
788
+
789
+ st.markdown(f"**Statut:** {row.get('correction_status', 'Non spécifié')}")
790
+
791
+ if row.get("corrective_action", ""):
792
+ st.markdown("<div class='section-title'>Action corrective</div>", unsafe_allow_html=True)
793
+ st.markdown(f"<div class='section-content'>{row.get('corrective_action', '')}</div>", unsafe_allow_html=True)
794
+
795
+ col1, col2 = st.columns(2)
796
+ with col1:
797
+ st.markdown(f"**Responsable:** {row.get('corrective_action_responsibility', 'Non spécifié')}")
798
+ with col2:
799
+ st.markdown(f"**Date prévue:** {row.get('corrective_action_due_date', 'Non spécifié')}")
800
+
801
+ st.markdown(f"**Statut:** {row.get('corrective_action_status', 'Non spécifié')}")
802
+
803
+ if row.get("verification_method", ""):
804
+ st.markdown("<div class='section-title'>Méthode de vérification</div>", unsafe_allow_html=True)
805
+ st.markdown(f"<div class='section-content'>{row.get('verification_method', '')}</div>", unsafe_allow_html=True)
806
+
807
+ # Afficher l'évaluation de l'auditeur
808
+ st.markdown("<div class='section-title'>Évaluation de l'auditeur</div>", unsafe_allow_html=True)
809
+ st.markdown(f"**Statut actuel:** {row['status']}")
810
+
811
+ if row.get("release_responsibility", ""):
812
+ st.markdown(f"**Validé par:** {row.get('release_responsibility', '')}")
813
+
814
+ if row.get("release_date", ""):
815
+ st.markdown(f"**Date de validation:** {row.get('release_date', '')}")
816
+
817
+ # Ajouter un commentaire et valider/rejeter
818
+ with st.form(key=f"review_{index}"):
819
+ review_text = st.text_area("Commentaire final:", key=f"review_text_{index}")
820
+
821
+ col1, col2 = st.columns(2)
822
+
823
+ with col1:
824
+ approve = st.form_submit_button("Approuver")
825
+
826
+ with col2:
827
+ reject = st.form_submit_button("Rejeter")
828
+
829
+ if approve:
830
+ if reference not in st.session_state['comments']:
831
+ st.session_state['comments'][reference] = []
832
+
833
+ st.session_state['comments'][reference].append({
834
  "role": st.session_state['role'],
835
+ "text": review_text + " [APPROUVÉ]",
836
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
837
+ })
838
+
839
+ st.session_state['audit_data'].at[index, "status"] = "Validé"
840
+ st.session_state['audit_data'].at[index, "release_responsibility"] = st.session_state['audit_metadata']["reviewer_name"]
841
+ st.session_state['audit_data'].at[index, "release_date"] = datetime.now().strftime("%Y-%m-%d")
842
+
843
+ st.success("Action approuvée")
844
  st.rerun()
845
+
846
+ if reject:
847
+ if reference not in st.session_state['comments']:
848
+ st.session_state['comments'][reference] = []
849
+
850
+ st.session_state['comments'][reference].append({
851
+ "role": st.session_state['role'],
852
+ "text": review_text + " [REJETÉ]",
853
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
854
+ })
855
+
856
+ st.session_state['audit_data'].at[index, "status"] = "En cours"
857
+ # Effacer les données de validation si elles existent
858
+ st.session_state['audit_data'].at[index, "release_responsibility"] = None
859
+ st.session_state['audit_data'].at[index, "release_date"] = None
860
+
861
+ st.error("Action rejetée")
862
+ st.rerun()
863
+
864
+ # Afficher les pièces jointes pour tous les rôles
865
+ display_attachments(reference, readonly=(st.session_state['role'] != "site"))
866
+
867
+ # Afficher les commentaires pour tous les rôles
868
+ display_comments(reference)
869
+
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  st.markdown("---")
871
+ else:
872
+ # Message si aucune donnée n'est chargée
873
+ st.info("Aucune donnée d'audit chargée. Veuillez charger un fichier d'audit depuis le panneau latéral.")
874
 
875
+ # Lancer l'application
876
  if __name__ == "__main__":
877
+ main()