Spaces:
Build error
Build error
| import streamlit as st | |
| import pandas as pd | |
| import io | |
| import zipfile | |
| import base64 | |
| import uuid | |
| import os | |
| from datetime import datetime | |
| # Configuration de la page | |
| st.set_page_config( | |
| page_title="Plan d'Actions IFS Food 8", | |
| page_icon="📋", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Styles CSS | |
| st.markdown(""" | |
| <style> | |
| .header { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #1E3D59; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| padding: 10px 0; | |
| border-bottom: 2px solid #e0f7fa; | |
| } | |
| .card { | |
| padding: 15px; | |
| border-radius: 5px; | |
| background-color: #f9f9f9; | |
| margin-bottom: 15px; | |
| border-left: 3px solid #1E3D59; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| border-radius: 10px; | |
| margin-left: 10px; | |
| } | |
| .status-completed { | |
| background-color: #28a745; | |
| color: white; | |
| } | |
| .status-progress { | |
| background-color: #ffc107; | |
| color: black; | |
| } | |
| .status-pending { | |
| background-color: #dc3545; | |
| color: white; | |
| } | |
| .attachment { | |
| padding: 5px; | |
| margin: 5px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| display: inline-block; | |
| } | |
| .section-title { | |
| color: #1E3D59; | |
| font-weight: bold; | |
| margin-top: 10px; | |
| margin-bottom: 5px; | |
| } | |
| .section-content { | |
| padding: 10px; | |
| background-color: #f5f5f5; | |
| border-radius: 5px; | |
| margin-bottom: 10px; | |
| } | |
| .info-text { | |
| font-style: italic; | |
| color: #666; | |
| } | |
| .requirement-text { | |
| font-weight: bold; | |
| } | |
| .finding-text { | |
| font-style: italic; | |
| border-left: 2px solid #ffc107; | |
| padding-left: 10px; | |
| margin: 10px 0; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialiser les états de session | |
| if 'role' not in st.session_state: | |
| st.session_state['role'] = "site" # site, auditor, reviewer | |
| if 'audit_data' not in st.session_state: | |
| st.session_state['audit_data'] = None | |
| if 'comments' not in st.session_state: | |
| st.session_state['comments'] = {} | |
| if 'attachments' not in st.session_state: | |
| st.session_state['attachments'] = {} | |
| if 'audit_metadata' not in st.session_state: | |
| st.session_state['audit_metadata'] = { | |
| "audit_id": str(uuid.uuid4()), | |
| "audit_date": datetime.now().strftime("%Y-%m-%d"), | |
| "site_name": "", | |
| "auditor_name": "", | |
| "reviewer_name": "", | |
| "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| if 'active_item' not in st.session_state: | |
| st.session_state['active_item'] = None | |
| if 'excel_metadata' not in st.session_state: | |
| st.session_state['excel_metadata'] = {} | |
| # Fonction pour extraire les métadonnées du fichier Excel | |
| def extract_excel_metadata(uploaded_file): | |
| try: | |
| # Lire les premières lignes pour extraire les métadonnées | |
| metadata_df = pd.read_excel(uploaded_file, header=None, nrows=10) | |
| # Créer un dictionnaire pour stocker les métadonnées | |
| metadata = {} | |
| # Extraire les informations pertinentes (adapter selon la structure exacte) | |
| if len(metadata_df) >= 1 and len(metadata_df.columns) >= 2: | |
| # Ligne 1: Entreprise et adresse | |
| enterprise_info = metadata_df.iloc[0, 0] if not pd.isna(metadata_df.iloc[0, 0]) else "" | |
| metadata["enterprise"] = enterprise_info | |
| # Ligne 2: Référentiel | |
| if len(metadata_df) >= 2: | |
| standard_info = metadata_df.iloc[1, 0] if not pd.isna(metadata_df.iloc[1, 0]) else "" | |
| metadata["standard"] = standard_info | |
| # Ligne 3: Type d'audit | |
| if len(metadata_df) >= 3: | |
| audit_type = metadata_df.iloc[2, 0] if not pd.isna(metadata_df.iloc[2, 0]) else "" | |
| metadata["audit_type"] = audit_type | |
| # Ligne 4: Date d'audit | |
| if len(metadata_df) >= 4: | |
| audit_date = metadata_df.iloc[3, 0] if not pd.isna(metadata_df.iloc[3, 0]) else "" | |
| metadata["audit_date"] = audit_date | |
| return metadata | |
| except Exception as e: | |
| st.error(f"Erreur lors de l'extraction des métadonnées: {str(e)}") | |
| return {} | |
| # Fonction pour charger les données d'audit | |
| def load_audit_data(uploaded_file): | |
| try: | |
| # Extraire d'abord les métadonnées | |
| metadata = extract_excel_metadata(uploaded_file) | |
| st.session_state['excel_metadata'] = metadata | |
| # Charger les données d'Excel - en sautant les lignes d'en-tête (les 11 premières lignes) | |
| df = pd.read_excel(uploaded_file, header=11) | |
| # Sélectionner uniquement les colonnes pertinentes | |
| columns_to_use = [ | |
| "requirementNo", "requirementText", "requirementScore", "requirementExplanation", | |
| "correctionDescription", "correctionResponsibility", "correctionDueDate", "correctionStatus", | |
| "correctionEvidence", "correctiveActionDescription", "correctiveActionResponsibility", | |
| "correctiveActionDueDate", "correctiveActionStatus", "releaseResponsibility", "releaseDate" | |
| ] | |
| # Vérifier que toutes les colonnes existent | |
| existing_columns = [col for col in columns_to_use if col in df.columns] | |
| df = df[existing_columns] | |
| # Création du mapping de colonnes | |
| column_mapping = { | |
| "requirementNo": "reference", | |
| "requirementText": "requirement", | |
| "requirementScore": "score", | |
| "requirementExplanation": "finding", | |
| "correctionDescription": "correction", | |
| "correctionResponsibility": "correction_responsibility", | |
| "correctionDueDate": "correction_due_date", | |
| "correctionStatus": "correction_status", | |
| "correctionEvidence": "evidence_type", | |
| "correctiveActionDescription": "corrective_action", | |
| "correctiveActionResponsibility": "corrective_action_responsibility", | |
| "correctiveActionDueDate": "corrective_action_due_date", | |
| "correctiveActionStatus": "corrective_action_status", | |
| "releaseResponsibility": "release_responsibility", | |
| "releaseDate": "release_date" | |
| } | |
| # Renommer les colonnes | |
| df = df.rename(columns=column_mapping) | |
| # Ajouter un statut global pour notre application | |
| if "status" not in df.columns: | |
| # Initialiser le statut à "Non traité" | |
| df["status"] = "Non traité" | |
| # Si les valeurs pour correction ou action corrective sont remplies, statut "En cours" | |
| in_progress_mask = df["correction"].notna() | df["corrective_action"].notna() | |
| df.loc[in_progress_mask, "status"] = "En cours" | |
| # Si les deux ont un statut "Completed", alors le statut global est "Complété" | |
| completed_mask = (df["correction_status"] == "Completed") & (df["corrective_action_status"] == "Completed") | |
| df.loc[completed_mask, "status"] = "Complété" | |
| # Si une date de validation est présente, alors le statut est "Validé" | |
| validated_mask = df["release_date"].notna() | |
| df.loc[validated_mask, "status"] = "Validé" | |
| # Ajouter une colonne pour l'analyse de cause racine si elle n'existe pas | |
| if "root_cause" not in df.columns: | |
| df["root_cause"] = "" | |
| # Ajouter une colonne pour la méthode de vérification si elle n'existe pas | |
| if "verification_method" not in df.columns: | |
| df["verification_method"] = "" | |
| # Supprimer les lignes vides (où le numéro d'exigence est vide) | |
| df = df.dropna(subset=["reference"]) | |
| return df | |
| except Exception as e: | |
| st.error(f"Erreur lors du chargement des données: {str(e)}") | |
| return None | |
| # Fonction pour créer un package d'export | |
| def create_export_package(): | |
| # Préparer les données pour l'export | |
| export_data = { | |
| "metadata": st.session_state['audit_metadata'], | |
| "excel_metadata": st.session_state['excel_metadata'], | |
| "audit_data": st.session_state['audit_data'].to_dict('records') if st.session_state['audit_data'] is not None else [], | |
| "comments": st.session_state['comments'] | |
| } | |
| # Créer un fichier ZIP en mémoire | |
| buffer = io.BytesIO() | |
| with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: | |
| # Créer un DataFrame pour l'export | |
| export_df = st.session_state['audit_data'].copy() | |
| # Renommer les colonnes pour correspondre au format d'origine | |
| reverse_mapping = { | |
| "reference": "requirementNo", | |
| "requirement": "requirementText", | |
| "score": "requirementScore", | |
| "finding": "requirementExplanation", | |
| "correction": "correctionDescription", | |
| "correction_responsibility": "correctionResponsibility", | |
| "correction_due_date": "correctionDueDate", | |
| "correction_status": "correctionStatus", | |
| "evidence_type": "correctionEvidence", | |
| "corrective_action": "correctiveActionDescription", | |
| "corrective_action_responsibility": "correctiveActionResponsibility", | |
| "corrective_action_due_date": "correctiveActionDueDate", | |
| "corrective_action_status": "correctiveActionStatus", | |
| "release_responsibility": "releaseResponsibility", | |
| "release_date": "releaseDate" | |
| } | |
| export_df = export_df.rename(columns=reverse_mapping) | |
| # Ajouter l'en-tête (à adapter selon le format exact) | |
| header_df = pd.DataFrame({ | |
| "Action plan": [st.session_state['excel_metadata'].get("enterprise", "")], | |
| "": [""], | |
| "Référentiel / Programme / Check": [st.session_state['excel_metadata'].get("standard", "IFS Food 8")], | |
| "Type d'audit/d'évaluation": [st.session_state['excel_metadata'].get("audit_type", "")], | |
| "Date de début d'audit / d'évaluation": [st.session_state['excel_metadata'].get("audit_date", "")] | |
| }) | |
| # Créer un DataFrame avec les titres des colonnes en français et anglais | |
| column_titles = pd.DataFrame({ | |
| "requirementNo": ["Numéro d'exigence"], | |
| "requirementText": ["Exigence IFS Food 8"], | |
| "requirementScore": ["Notation"], | |
| "requirementExplanation": ["Explication (par l'auditeur/l'évaluateur)"], | |
| "correctionDescription": ["Correction (par l'entreprise)"], | |
| "correctionResponsibility": ["Responsabilité (par l'entreprise)"], | |
| "correctionDueDate": ["Date (par l'entreprise)"], | |
| "correctionStatus": ["Statut de la mise en œuvre (par l'entreprise)"], | |
| "correctionEvidence": ["Type de preuve(s) et nom du/des document(s)"], | |
| "correctiveActionDescription": ["Action corrective (par l'entreprise)"], | |
| "correctiveActionResponsibility": ["Responsabilité (par l'entreprise)"], | |
| "correctiveActionDueDate": ["Date (par l'entreprise)"], | |
| "correctiveActionStatus": ["Statut de la mise en œuvre (par l'entreprise)"], | |
| "releaseResponsibility": ["Effectué par (l'auditeur/l'évaluateur)"], | |
| "releaseDate": ["Date de transmission"] | |
| }) | |
| # Créer un fichier Excel en mémoire | |
| excel_buffer = io.BytesIO() | |
| with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer: | |
| header_df.to_excel(writer, sheet_name='Plan d\'action', index=False) | |
| # Laisser des lignes vides | |
| column_titles.to_excel(writer, sheet_name='Plan d\'action', startrow=11, index=False) | |
| export_df.to_excel(writer, sheet_name='Plan d\'action', startrow=13, index=False) | |
| excel_buffer.seek(0) | |
| zip_file.writestr('action_plan_updated.xlsx', excel_buffer.getvalue()) | |
| # Ajouter les pièces jointes | |
| for ref, attachments in st.session_state['attachments'].items(): | |
| for filename, file_data in attachments.items(): | |
| zip_file.writestr(f'attachments/{ref}/{filename}', file_data) | |
| # Ajouter un fichier de commentaires au format texte | |
| if st.session_state['comments']: | |
| comments_text = "COMMENTAIRES DU PLAN D'ACTION\n\n" | |
| for ref, comments_list in st.session_state['comments'].items(): | |
| comments_text += f"Exigence {ref}:\n" | |
| for comment in comments_list: | |
| comments_text += f"- {comment['timestamp']} ({comment['role']}): {comment['text']}\n" | |
| comments_text += "\n" | |
| zip_file.writestr('comments.txt', comments_text) | |
| buffer.seek(0) | |
| return buffer.getvalue() | |
| # Fonction pour afficher les pièces jointes | |
| def display_attachments(reference, readonly=False): | |
| if reference in st.session_state['attachments'] and st.session_state['attachments'][reference]: | |
| st.markdown("<div class='section-title'>Pièces jointes</div>", unsafe_allow_html=True) | |
| cols = st.columns(4) | |
| for i, (filename, file_data) in enumerate(st.session_state['attachments'][reference].items()): | |
| col = cols[i % 4] | |
| with col: | |
| # Déterminer le type de fichier | |
| file_extension = os.path.splitext(filename)[1].lower() | |
| # Afficher une icône différente selon le type de fichier | |
| icon = "📄" # Par défaut | |
| if file_extension in ['.jpg', '.jpeg', '.png', '.gif']: | |
| icon = "🖼️" | |
| elif file_extension == '.pdf': | |
| icon = "📑" | |
| elif file_extension in ['.doc', '.docx']: | |
| icon = "📝" | |
| elif file_extension in ['.xls', '.xlsx']: | |
| icon = "📊" | |
| # Afficher le nom du fichier | |
| st.markdown(f"<div class='attachment'>{icon} {filename}</div>", unsafe_allow_html=True) | |
| # Bouton de téléchargement | |
| st.download_button( | |
| "Télécharger", | |
| file_data, | |
| file_name=filename | |
| ) | |
| # Bouton de suppression (si non readonly) | |
| if not readonly and st.session_state['role'] == "site": | |
| if st.button("Supprimer", key=f"delete_{reference}_{filename}"): | |
| del st.session_state['attachments'][reference][filename] | |
| st.rerun() | |
| # Fonction pour afficher les commentaires | |
| def display_comments(reference): | |
| if reference in st.session_state['comments'] and st.session_state['comments'][reference]: | |
| st.markdown("<div class='section-title'>Commentaires</div>", unsafe_allow_html=True) | |
| for comment in st.session_state['comments'][reference]: | |
| # Définir la couleur du rôle | |
| role_colors = { | |
| "site": "#28a745", | |
| "auditor": "#0066cc", | |
| "reviewer": "#6f42c1" | |
| } | |
| role_color = role_colors.get(comment["role"], "#6c757d") | |
| # Afficher le commentaire | |
| st.markdown(f""" | |
| <div style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; border-left: 3px solid {role_color};"> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span style="font-weight: bold; color: {role_color};">{comment["role"].capitalize()}</span> | |
| <span style="font-size: 0.8em; color: #6c757d;">{comment["timestamp"]}</span> | |
| </div> | |
| <div style="margin-top: 5px;">{comment["text"]}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Interface principale | |
| def main(): | |
| # Sidebar pour configuration | |
| with st.sidebar: | |
| st.markdown("### Configuration") | |
| # Sélection du rôle | |
| role_options = { | |
| "site": "Site Audité", | |
| "auditor": "Auditeur", | |
| "reviewer": "Reviewer" | |
| } | |
| selected_role = st.selectbox( | |
| "Rôle:", | |
| options=list(role_options.keys()), | |
| format_func=lambda x: role_options[x], | |
| index=list(role_options.keys()).index(st.session_state['role']) | |
| ) | |
| if selected_role != st.session_state['role']: | |
| st.session_state['role'] = selected_role | |
| st.rerun() | |
| # Formulaire pour les métadonnées de l'audit | |
| with st.form(key="metadata_form"): | |
| st.markdown("### Informations d'audit") | |
| if st.session_state['role'] == "site": | |
| st.session_state['audit_metadata']["site_name"] = st.text_input( | |
| "Nom du site:", | |
| value=st.session_state['audit_metadata'].get("site_name", "") | |
| ) | |
| if st.session_state['role'] == "auditor": | |
| st.session_state['audit_metadata']["auditor_name"] = st.text_input( | |
| "Nom de l'auditeur:", | |
| value=st.session_state['audit_metadata'].get("auditor_name", "") | |
| ) | |
| if st.session_state['role'] == "reviewer": | |
| st.session_state['audit_metadata']["reviewer_name"] = st.text_input( | |
| "Nom du reviewer:", | |
| value=st.session_state['audit_metadata'].get("reviewer_name", "") | |
| ) | |
| st.form_submit_button("Enregistrer") | |
| # Upload du fichier d'audit (pour le rôle site uniquement) | |
| if st.session_state['role'] == "site": | |
| st.markdown("### Charger les données d'audit") | |
| uploaded_file = st.file_uploader("Fichier d'audit IFS Food 8:", type=["xlsx", "xls"]) | |
| if uploaded_file: | |
| audit_data = load_audit_data(uploaded_file) | |
| if audit_data is not None: | |
| st.session_state['audit_data'] = audit_data | |
| st.success("Données d'audit chargées avec succès") | |
| st.rerun() | |
| # Export du rapport | |
| if st.session_state['audit_data'] is not None: | |
| st.markdown("### Export du plan d'actions") | |
| if st.button("Exporter le rapport"): | |
| export_data = create_export_package() | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| site_name = st.session_state['audit_metadata']["site_name"].replace(" ", "_") or "audit" | |
| st.download_button( | |
| "Télécharger le rapport", | |
| export_data, | |
| file_name=f"plan_actions_{site_name}_{timestamp}.zip", | |
| mime="application/zip" | |
| ) | |
| # Contenu principal | |
| st.markdown('<div class="header">Plan d\'Actions IFS Food 8</div>', unsafe_allow_html=True) | |
| # Afficher les informations d'audit | |
| if st.session_state['excel_metadata']: | |
| enterprise = st.session_state['excel_metadata'].get("enterprise", "") | |
| standard = st.session_state['excel_metadata'].get("standard", "") | |
| audit_type = st.session_state['excel_metadata'].get("audit_type", "") | |
| audit_date = st.session_state['excel_metadata'].get("audit_date", "") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Entreprise:** {enterprise}") | |
| st.markdown(f"**Référentiel:** {standard}") | |
| with col2: | |
| st.markdown(f"**Type d'audit:** {audit_type}") | |
| st.markdown(f"**Date d'audit:** {audit_date}") | |
| # Afficher les non-conformités si les données sont chargées | |
| if st.session_state['audit_data'] is not None: | |
| # Filtres | |
| st.markdown("### Filtrer les non-conformités") | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| status_options = ["Tous", "Non traité", "En cours", "Complété", "Validé"] | |
| status_filter = st.multiselect( | |
| "Statut:", | |
| options=status_options, | |
| default=["Tous"] | |
| ) | |
| with col2: | |
| search_term = st.text_input("Rechercher:", "") | |
| # Appliquer les filtres | |
| filtered_data = st.session_state['audit_data'].copy() | |
| if "Tous" not in status_filter: | |
| filtered_data = filtered_data[filtered_data["status"].isin(status_filter)] | |
| if search_term: | |
| # Recherche dans toutes les colonnes textuelles | |
| text_columns = filtered_data.select_dtypes(include='object').columns | |
| mask = False | |
| for column in text_columns: | |
| mask = mask | filtered_data[column].str.contains(search_term, case=False, na=False) | |
| filtered_data = filtered_data[mask] | |
| # Afficher les statistiques | |
| if len(filtered_data) > 0: | |
| st.markdown("### Statistiques") | |
| total = len(st.session_state['audit_data']) | |
| completed = sum(st.session_state['audit_data']["status"] == "Complété") | |
| in_progress = sum(st.session_state['audit_data']["status"] == "En cours") | |
| validated = sum(st.session_state['audit_data']["status"] == "Validé") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Total", str(total)) | |
| with col2: | |
| st.metric("En cours", str(in_progress)) | |
| with col3: | |
| st.metric("Complétés", str(completed)) | |
| with col4: | |
| st.metric("Validés", str(validated)) | |
| progress = (completed + validated) / total if total > 0 else 0 | |
| st.progress(progress) | |
| # Afficher les non-conformités | |
| st.markdown("### Non-conformités") | |
| for index, row in filtered_data.iterrows(): | |
| # Déterminer la classe du badge de statut | |
| status_class = { | |
| "Non traité": "status-pending", | |
| "En cours": "status-progress", | |
| "Complété": "status-completed", | |
| "Validé": "status-completed" | |
| }.get(row["status"], "status-pending") | |
| # Créer la carte de non-conformité | |
| reference = str(row["reference"]) if "reference" in row else str(index) | |
| requirement = row.get("requirement", "") | |
| finding = row.get("finding", "") | |
| score = row.get("score", "") | |
| st.markdown(f""" | |
| <div class="card"> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <div> | |
| <strong>Exigence {reference}</strong> | |
| <span class="status-badge {status_class}">{row["status"]}</span> | |
| </div> | |
| <div> | |
| <strong>Notation: {score}</strong> | |
| </div> | |
| </div> | |
| <div class="requirement-text" style="margin-top: 10px;">{requirement}</div> | |
| <div class="finding-text">{finding}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Options selon le rôle | |
| if st.session_state['role'] == "site": | |
| # Le site peut ajouter des actions correctives et des pièces jointes | |
| with st.expander("Répondre à cette non-conformité", expanded=st.session_state.get('active_item') == index): | |
| with st.form(key=f"form_{index}"): | |
| # Analyse de cause racine | |
| root_cause = st.text_area( | |
| "Analyse de cause racine:", | |
| value=row.get("root_cause", ""), | |
| height=80 | |
| ) | |
| # Correction immédiate | |
| correction = st.text_area( | |
| "Correction immédiate:", | |
| value=row.get("correction", ""), | |
| height=80 | |
| ) | |
| # Responsable de la correction | |
| correction_responsibility = st.text_input( | |
| "Responsable de la correction:", | |
| value=row.get("correction_responsibility", "") | |
| ) | |
| # Date prévue pour la correction | |
| correction_due_date = st.date_input( | |
| "Date prévue pour la correction:", | |
| value=datetime.now() | |
| ) | |
| # Statut de la correction | |
| correction_status = st.selectbox( | |
| "Statut de la correction:", | |
| options=["Non démarré", "En cours", "Completed"], | |
| index=0 if pd.isna(row.get("correction_status")) else ["Non démarré", "En cours", "Completed"].index(row.get("correction_status", "Non démarré")) | |
| ) | |
| # Action corrective à long terme | |
| corrective_action = st.text_area( | |
| "Action corrective (prévention de la récurrence):", | |
| value=row.get("corrective_action", ""), | |
| height=80 | |
| ) | |
| # Responsable de l'action corrective | |
| corrective_action_responsibility = st.text_input( | |
| "Responsable de l'action corrective:", | |
| value=row.get("corrective_action_responsibility", "") | |
| ) | |
| # Date prévue pour l'action corrective | |
| corrective_action_due_date = st.date_input( | |
| "Date prévue pour l'action corrective:", | |
| value=datetime.now() | |
| ) | |
| # Statut de l'action corrective | |
| corrective_action_status = st.selectbox( | |
| "Statut de l'action corrective:", | |
| options=["Non démarré", "En cours", "Completed"], | |
| 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é")) | |
| ) | |
| # Méthode de vérification | |
| verification_method = st.text_area( | |
| "Méthode de vérification de l'efficacité:", | |
| value=row.get("verification_method", ""), | |
| height=60 | |
| ) | |
| # Type de preuves | |
| evidence_type = st.text_input( | |
| "Type de preuves et nom des documents:", | |
| value=row.get("evidence_type", "") | |
| ) | |
| submitted = st.form_submit_button("Enregistrer les actions") | |
| if submitted: | |
| st.session_state['audit_data'].at[index, "root_cause"] = root_cause | |
| st.session_state['audit_data'].at[index, "correction"] = correction | |
| st.session_state['audit_data'].at[index, "correction_responsibility"] = correction_responsibility | |
| st.session_state['audit_data'].at[index, "correction_due_date"] = correction_due_date.strftime("%Y-%m-%d") | |
| st.session_state['audit_data'].at[index, "correction_status"] = correction_status | |
| st.session_state['audit_data'].at[index, "corrective_action"] = corrective_action | |
| st.session_state['audit_data'].at[index, "corrective_action_responsibility"] = corrective_action_responsibility | |
| st.session_state['audit_data'].at[index, "corrective_action_due_date"] = corrective_action_due_date.strftime("%Y-%m-%d") | |
| st.session_state['audit_data'].at[index, "corrective_action_status"] = corrective_action_status | |
| st.session_state['audit_data'].at[index, "verification_method"] = verification_method | |
| st.session_state['audit_data'].at[index, "evidence_type"] = evidence_type | |
| # Déterminer le statut global | |
| if correction_status == "Completed" and corrective_action_status == "Completed": | |
| st.session_state['audit_data'].at[index, "status"] = "Complété" | |
| elif correction != "" or corrective_action != "": | |
| st.session_state['audit_data'].at[index, "status"] = "En cours" | |
| st.success("Actions enregistrées avec succès") | |
| st.rerun() | |
| # Upload de pièces jointes | |
| uploaded_files = st.file_uploader( | |
| "Ajouter des pièces justificatives:", | |
| accept_multiple_files=True, | |
| key=f"files_{index}" | |
| ) | |
| if uploaded_files: | |
| if reference not in st.session_state['attachments']: | |
| st.session_state['attachments'][reference] = {} | |
| for file in uploaded_files: | |
| file_data = file.read() | |
| filename = file.name | |
| st.session_state['attachments'][reference][filename] = file_data | |
| st.success(f"{len(uploaded_files)} fichier(s) ajouté(s)") | |
| st.rerun() | |
| elif st.session_state['role'] == "auditor": | |
| # L'auditeur peut ajouter des commentaires et changer le statut | |
| with st.expander("Évaluer cette action", expanded=st.session_state.get('active_item') == index): | |
| # Afficher les informations sur les actions proposées | |
| if row.get("root_cause", ""): | |
| st.markdown("<div class='section-title'>Analyse de cause racine</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('root_cause', '')}</div>", unsafe_allow_html=True) | |
| if row.get("correction", ""): | |
| st.markdown("<div class='section-title'>Correction immédiate</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('correction', '')}</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Responsable:** {row.get('correction_responsibility', 'Non spécifié')}") | |
| with col2: | |
| st.markdown(f"**Date prévue:** {row.get('correction_due_date', 'Non spécifié')}") | |
| st.markdown(f"**Statut:** {row.get('correction_status', 'Non spécifié')}") | |
| if row.get("corrective_action", ""): | |
| st.markdown("<div class='section-title'>Action corrective</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('corrective_action', '')}</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Responsable:** {row.get('corrective_action_responsibility', 'Non spécifié')}") | |
| with col2: | |
| st.markdown(f"**Date prévue:** {row.get('corrective_action_due_date', 'Non spécifié')}") | |
| st.markdown(f"**Statut:** {row.get('corrective_action_status', 'Non spécifié')}") | |
| if row.get("verification_method", ""): | |
| st.markdown("<div class='section-title'>Méthode de vérification</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('verification_method', '')}</div>", unsafe_allow_html=True) | |
| # Changer le statut | |
| new_status = st.selectbox( | |
| "Statut:", | |
| options=["Non traité", "En cours", "Complété", "Validé"], | |
| index=["Non traité", "En cours", "Complété", "Validé"].index(row["status"]) if row["status"] in ["Non traité", "En cours", "Complété", "Validé"] else 0, | |
| key=f"status_{index}" | |
| ) | |
| if new_status != row["status"]: | |
| st.session_state['audit_data'].at[index, "status"] = new_status | |
| # Si le statut est "Validé", ajouter la date et le responsable de validation | |
| if new_status == "Validé": | |
| st.session_state['audit_data'].at[index, "release_responsibility"] = st.session_state['audit_metadata']["auditor_name"] | |
| st.session_state['audit_data'].at[index, "release_date"] = datetime.now().strftime("%Y-%m-%d") | |
| st.rerun() | |
| # Ajouter un commentaire | |
| with st.form(key=f"comment_{index}"): | |
| comment_text = st.text_area("Ajouter un commentaire:", key=f"comment_text_{index}") | |
| submitted = st.form_submit_button("Enregistrer le commentaire") | |
| if submitted and comment_text: | |
| if reference not in st.session_state['comments']: | |
| st.session_state['comments'][reference] = [] | |
| st.session_state['comments'][reference].append({ | |
| "role": st.session_state['role'], | |
| "text": comment_text, | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| }) | |
| st.success("Commentaire ajouté") | |
| st.rerun() | |
| elif st.session_state['role'] == "reviewer": | |
| # Le reviewer peut valider ou rejeter l'action | |
| with st.expander("Valider cette action", expanded=st.session_state.get('active_item') == index): | |
| # Afficher les informations sur les actions proposées | |
| if row.get("root_cause", ""): | |
| st.markdown("<div class='section-title'>Analyse de cause racine</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('root_cause', '')}</div>", unsafe_allow_html=True) | |
| if row.get("correction", ""): | |
| st.markdown("<div class='section-title'>Correction immédiate</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('correction', '')}</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Responsable:** {row.get('correction_responsibility', 'Non spécifié')}") | |
| with col2: | |
| st.markdown(f"**Date prévue:** {row.get('correction_due_date', 'Non spécifié')}") | |
| st.markdown(f"**Statut:** {row.get('correction_status', 'Non spécifié')}") | |
| if row.get("corrective_action", ""): | |
| st.markdown("<div class='section-title'>Action corrective</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('corrective_action', '')}</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Responsable:** {row.get('corrective_action_responsibility', 'Non spécifié')}") | |
| with col2: | |
| st.markdown(f"**Date prévue:** {row.get('corrective_action_due_date', 'Non spécifié')}") | |
| st.markdown(f"**Statut:** {row.get('corrective_action_status', 'Non spécifié')}") | |
| if row.get("verification_method", ""): | |
| st.markdown("<div class='section-title'>Méthode de vérification</div>", unsafe_allow_html=True) | |
| st.markdown(f"<div class='section-content'>{row.get('verification_method', '')}</div>", unsafe_allow_html=True) | |
| # Afficher l'évaluation de l'auditeur | |
| st.markdown("<div class='section-title'>Évaluation de l'auditeur</div>", unsafe_allow_html=True) | |
| st.markdown(f"**Statut actuel:** {row['status']}") | |
| if row.get("release_responsibility", ""): | |
| st.markdown(f"**Validé par:** {row.get('release_responsibility', '')}") | |
| if row.get("release_date", ""): | |
| st.markdown(f"**Date de validation:** {row.get('release_date', '')}") | |
| # Ajouter un commentaire et valider/rejeter | |
| with st.form(key=f"review_{index}"): | |
| review_text = st.text_area("Commentaire final:", key=f"review_text_{index}") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| approve = st.form_submit_button("Approuver") | |
| with col2: | |
| reject = st.form_submit_button("Rejeter") | |
| if approve: | |
| if reference not in st.session_state['comments']: | |
| st.session_state['comments'][reference] = [] | |
| st.session_state['comments'][reference].append({ | |
| "role": st.session_state['role'], | |
| "text": review_text + " [APPROUVÉ]", | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| }) | |
| st.session_state['audit_data'].at[index, "status"] = "Validé" | |
| st.session_state['audit_data'].at[index, "release_responsibility"] = st.session_state['audit_metadata']["reviewer_name"] | |
| st.session_state['audit_data'].at[index, "release_date"] = datetime.now().strftime("%Y-%m-%d") | |
| st.success("Action approuvée") | |
| st.rerun() | |
| if reject: | |
| if reference not in st.session_state['comments']: | |
| st.session_state['comments'][reference] = [] | |
| st.session_state['comments'][reference].append({ | |
| "role": st.session_state['role'], | |
| "text": review_text + " [REJETÉ]", | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| }) | |
| st.session_state['audit_data'].at[index, "status"] = "En cours" | |
| # Effacer les données de validation si elles existent | |
| st.session_state['audit_data'].at[index, "release_responsibility"] = None | |
| st.session_state['audit_data'].at[index, "release_date"] = None | |
| st.error("Action rejetée") | |
| st.rerun() | |
| # Afficher les pièces jointes pour tous les rôles | |
| display_attachments(reference, readonly=(st.session_state['role'] != "site")) | |
| # Afficher les commentaires pour tous les rôles | |
| display_comments(reference) | |
| st.markdown("---") | |
| else: | |
| # Message si aucune donnée n'est chargée | |
| st.info("Aucune donnée d'audit chargée. Veuillez charger un fichier d'audit depuis le panneau latéral.") | |
| # Lancer l'application | |
| if __name__ == "__main__": | |
| main() |