MMOON's picture
Update app.py
1e306a4 verified
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()