BUSCA / app.py
MMOON's picture
Update app.py
34e3a4a verified
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("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');
* { font-family: 'Roboto', sans-serif; }
[data-testid="stSidebar"] {
background-color: #2E3B4E;
color: #FFFFFF;
}
.stButton > button {
background-color: #1E88E5;
color: white;
border-radius: 5px;
}
.stButton > button:hover {
background-color: #1565C0;
}
</style>
""", 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])