import streamlit as st import pandas as pd import requests from io import BytesIO # 🖌️ Configurer le mode wide et le titre de la page st.set_page_config(layout="wide", page_title="Veille Sanitaire SCA - BuSCA") # 🖌️ Importer Google Fonts et personnaliser l'interface st.markdown(""" """, unsafe_allow_html=True) st.title("🔍 Veille Sanitaire BuSCA") st.info("Les données sont chargées depuis la Plateforme SCA. Utilisez les filtres dans le menu latéral.") # 📦 Fonction pour charger les données avec cache @st.cache_data(ttl=3600) # Mettre en cache les données pendant 1 heure def load_data(): # --- MODIFICATION DE L'URL ICI --- file_url = "https://www.plateforme-sca.fr/media/398/download" try: # En Python (côté serveur), pas besoin de proxy CORS, mais on garde le User-Agent response = requests.get(file_url, headers={'User-Agent': 'Mozilla/5.0'}) response.raise_for_status() df = pd.read_excel(BytesIO(response.content), engine='openpyxl') # Nettoyage des noms de colonnes (minuscules, sans espaces) df.columns = df.columns.str.strip().str.lower() # --- ROBUSTESSE : Renommage pour gérer Singulier/Pluriel --- # Si le fichier contient "matrice" au lieu de "matrices", on normalise rename_map = { 'matrice': 'matrices', 'danger': 'dangers' } df.rename(columns=rename_map, inplace=True) return df except Exception as e: st.error(f"Erreur critique lors du chargement des données : {e}") return None # --- Chargement des données --- df_full = load_data() if df_full is None: st.error("Impossible de continuer car les données n'ont pas pu être chargées.") st.stop() # --- Définition des noms de colonnes (Normalisés) --- COL_BUSCA = 'busca' COL_TITRE = 'titre' COL_MATRICE = 'matrices' # On utilise le pluriel car on a normalisé ci-dessus COL_DANGER = 'dangers' # On utilise le pluriel car on a normalisé ci-dessus COL_SECTION = 'section' COL_TEXTE = 'texte' COL_LIEN1 = 'lien' COL_LIEN2 = 'lien2' # --- Vérification de la présence des colonnes essentielles --- essential_cols = [COL_BUSCA, COL_TITRE, COL_TEXTE, COL_MATRICE, COL_DANGER] missing_cols = [col for col in essential_cols if col not in df_full.columns] if missing_cols: st.error(f"ERREUR : Les colonnes essentielles suivantes sont manquantes dans le fichier Excel : {', '.join(missing_cols)}") st.write("Colonnes trouvées :", df_full.columns.tolist()) st.stop() # Nettoyage des données (suppression des lignes sans N° BuSCA) df_full = df_full.dropna(subset=[COL_BUSCA]) # Conversion du N° BuSCA en entier df_full[COL_BUSCA] = df_full[COL_BUSCA].astype(int) # Tri des données df_full = df_full.sort_values(by=COL_BUSCA, ascending=False) # 🌟 Menu latéral with st.sidebar: st.header("🛠️ Filtres") if st.button("🔄 Rafraîchir les données"): st.cache_data.clear() st.rerun() # Utilisation de st.rerun() au lieu de experimental_rerun with st.expander("📌 Plage de numéros de BuSCA", expanded=True): min_val = int(df_full[COL_BUSCA].min()) max_val = int(df_full[COL_BUSCA].max()) busca_range = st.slider("Numéros de BuSCA", min_val, max_val, (max_val - 20, max_val)) with st.expander("🌍 Matrices"): # Conversion en string pour éviter les erreurs de tri si données mixtes unique_matrices = sorted(df_full[COL_MATRICE].fillna('Non spécifié').astype(str).unique()) matrices = st.multiselect("Sélectionner les matrices", options=unique_matrices) with st.expander("⚠️ Dangers"): unique_dangers = sorted(df_full[COL_DANGER].fillna('Non spécifié').astype(str).unique()) dangers = st.multiselect("Sélectionner les dangers", options=unique_dangers) # --- FILTRE TEXTE LIBRE --- with st.expander("🔎 Recherche par mots-clés"): keywords = st.text_area("Mots-clés (séparés par des virgules)", placeholder="ex: listeria, lait, rappel...") apply_filter = st.button("Appliquer les filtres", use_container_width=True) # Logique de filtrage df_display = df_full.copy() # Note: Streamlit relance le script à chaque interaction, donc si on n'appuie pas sur le bouton # on affiche quand même les données filtrées par défaut (tout ou dernière action). # Si vous voulez que rien ne change tant qu'on ne clique pas, il faut gérer le state, # mais ici on applique la logique standard : if apply_filter or True: # 'or True' permet un affichage dynamique réactif immédiat (optionnel selon préférence UX) # Filtre par plage de BuSCA df_display = df_display[ (df_display[COL_BUSCA] >= busca_range[0]) & (df_display[COL_BUSCA] <= busca_range[1]) ] # Filtre par listes if matrices: df_display = df_display[df_display[COL_MATRICE].astype(str).isin(matrices)] if dangers: df_display = df_display[df_display[COL_DANGER].astype(str).isin(dangers)] # --- LOGIQUE DE FILTRAGE PAR MOTS-CLÉS --- if keywords: # Prépare la liste de mots-clés : minuscule, sans espaces superflus keyword_list = [kw.strip().lower() for kw in keywords.split(',') if kw.strip()] # Applique le filtre si des mots-clés ont été saisis if keyword_list: df_display = df_display[df_display.apply( lambda row: any( kw in str(row[COL_TITRE]).lower() or kw in str(row[COL_TEXTE]).lower() or kw in str(row[COL_DANGER]).lower() or kw in str(row[COL_MATRICE]).lower() for kw in keyword_list ), axis=1 )] # Affichage des résultats st.markdown(f"### 📑 Affichage de {len(df_display)} résultats") if df_display.empty: st.warning("Aucun bulletin ne correspond à vos critères de recherche.") else: for index, row in df_display.iterrows(): titre = row.get(COL_TITRE, 'Titre manquant') busca_num = row.get(COL_BUSCA, 'N/A') with st.expander(f"📄 **{titre}** (BuSCA n°{busca_num})"): danger_val = row.get(COL_DANGER, 'N/A') matrice_val = row.get(COL_MATRICE, 'N/A') st.markdown(f"**Danger :** `{danger_val}` | **Matrice :** `{matrice_val}`") st.markdown("---") # Remplacement des sauts de ligne pour un affichage propre texte_content = str(row.get(COL_TEXTE, 'Texte manquant')).replace('\n', ' \n') st.markdown(texte_content) st.markdown("---") col1, col2 = st.columns(2) with col1: if pd.notna(row.get(COL_LIEN1)): st.link_button("🔗 Lien Principal", row[COL_LIEN1]) with col2: if pd.notna(row.get(COL_LIEN2)): st.link_button("🔗 Lien Secondaire", row[COL_LIEN2])