Spaces:
Build error
Build error
Update app.py
Browse files
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="
|
| 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 |
-
.
|
| 27 |
-
font-size:
|
| 28 |
font-weight: bold;
|
| 29 |
-
color: #
|
| 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:
|
| 46 |
background-color: #f9f9f9;
|
| 47 |
margin-bottom: 15px;
|
| 48 |
-
border-left:
|
| 49 |
}
|
| 50 |
.status-badge {
|
| 51 |
display: inline-block;
|
|
@@ -67,65 +54,37 @@ st.markdown("""
|
|
| 67 |
background-color: #dc3545;
|
| 68 |
color: white;
|
| 69 |
}
|
| 70 |
-
.
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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 |
-
.
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
-
.
|
| 105 |
-
|
| 106 |
-
background-color: #
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
-
.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
}
|
| 112 |
-
|
| 113 |
-
|
| 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 |
-
.
|
| 123 |
-
font-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 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 '
|
| 137 |
-
st.session_state['
|
| 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 '
|
| 166 |
-
st.session_state['
|
| 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 |
-
|
| 185 |
-
|
| 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 |
-
#
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
#
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 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 |
-
|
| 255 |
-
|
| 256 |
-
return False
|
| 257 |
|
| 258 |
-
#
|
| 259 |
-
def
|
| 260 |
try:
|
| 261 |
-
#
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
}
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
except Exception as e:
|
| 284 |
-
|
| 285 |
-
st.error(error_message)
|
| 286 |
return None
|
| 287 |
|
| 288 |
-
#
|
| 289 |
-
def
|
| 290 |
-
#
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 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 |
-
#
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 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 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
cols = st.columns(4)
|
| 628 |
-
|
| 629 |
-
for i, (filename, file_data) in enumerate(attachments.items()):
|
| 630 |
col = cols[i % 4]
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
#
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 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 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
|
| 702 |
# Interface principale
|
| 703 |
def main():
|
| 704 |
-
# Sidebar pour
|
| 705 |
with st.sidebar:
|
| 706 |
-
st.markdown("###
|
| 707 |
-
|
| 708 |
# Sélection du rôle
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 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 |
-
|
| 763 |
-
|
| 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 |
-
|
| 772 |
-
|
| 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 |
-
|
| 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 |
-
|
| 795 |
-
|
| 796 |
-
# Actions spécifiques au rôle
|
| 797 |
if st.session_state['role'] == "site":
|
| 798 |
-
st.markdown("###
|
| 799 |
-
uploaded_file = st.file_uploader("Fichier
|
| 800 |
-
|
| 801 |
if uploaded_file:
|
| 802 |
-
|
| 803 |
-
if
|
| 804 |
-
st.session_state['
|
| 805 |
-
st.success("
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
col1, col2 = st.columns(2)
|
| 811 |
-
|
| 812 |
with col1:
|
| 813 |
-
|
| 814 |
-
|
| 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 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 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 |
-
|
| 909 |
-
|
| 910 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
with col2:
|
| 912 |
-
search_term = st.text_input("Rechercher
|
| 913 |
-
|
| 914 |
# Appliquer les filtres
|
| 915 |
-
|
| 916 |
-
|
| 917 |
if "Tous" not in status_filter:
|
| 918 |
-
|
| 919 |
-
|
| 920 |
if search_term:
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
# Afficher les non-conformités
|
| 927 |
-
|
|
|
|
|
|
|
| 928 |
# Déterminer la classe du badge de statut
|
| 929 |
-
|
| 930 |
-
"
|
| 931 |
"En cours": "status-progress",
|
| 932 |
-
"
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
#
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
<div class="card">
|
| 944 |
-
<div>
|
| 945 |
-
<
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
<strong>Constat de l'auditeur:</strong> {auditor_explanation}
|
| 953 |
</div>
|
|
|
|
|
|
|
| 954 |
</div>
|
| 955 |
-
"""
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
# Options d'action selon le rôle
|
| 959 |
if st.session_state['role'] == "site":
|
| 960 |
-
#
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
uploaded_files = st.file_uploader(
|
| 985 |
-
"
|
| 986 |
-
type=["pdf", "jpg", "jpeg", "png", "docx", "xlsx"],
|
| 987 |
accept_multiple_files=True,
|
| 988 |
-
key=
|
| 989 |
)
|
| 990 |
-
|
| 991 |
if uploaded_files:
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 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'] == "
|
| 1042 |
-
#
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
"
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
st.rerun()
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 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 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
"role": st.session_state['role'],
|
| 1141 |
-
"
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
st.
|
| 1146 |
-
st.session_state['
|
|
|
|
|
|
|
|
|
|
| 1147 |
st.rerun()
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 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()
|
|
|