|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import requests |
|
|
from io import BytesIO |
|
|
|
|
|
|
|
|
st.set_page_config(layout="wide", page_title="Veille Sanitaire SCA - BuSCA") |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
@st.cache_data(ttl=3600) |
|
|
def load_data(): |
|
|
|
|
|
file_url = "https://www.plateforme-sca.fr/media/398/download" |
|
|
|
|
|
try: |
|
|
|
|
|
response = requests.get(file_url, headers={'User-Agent': 'Mozilla/5.0'}) |
|
|
response.raise_for_status() |
|
|
|
|
|
df = pd.read_excel(BytesIO(response.content), engine='openpyxl') |
|
|
|
|
|
|
|
|
df.columns = df.columns.str.strip().str.lower() |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
COL_BUSCA = 'busca' |
|
|
COL_TITRE = 'titre' |
|
|
COL_MATRICE = 'matrices' |
|
|
COL_DANGER = 'dangers' |
|
|
COL_SECTION = 'section' |
|
|
COL_TEXTE = 'texte' |
|
|
COL_LIEN1 = 'lien' |
|
|
COL_LIEN2 = 'lien2' |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
df_full = df_full.dropna(subset=[COL_BUSCA]) |
|
|
|
|
|
df_full[COL_BUSCA] = df_full[COL_BUSCA].astype(int) |
|
|
|
|
|
|
|
|
df_full = df_full.sort_values(by=COL_BUSCA, ascending=False) |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.header("🛠️ Filtres") |
|
|
|
|
|
if st.button("🔄 Rafraîchir les données"): |
|
|
st.cache_data.clear() |
|
|
st.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"): |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
df_display = df_full.copy() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if apply_filter or True: |
|
|
|
|
|
df_display = df_display[ |
|
|
(df_display[COL_BUSCA] >= busca_range[0]) & |
|
|
(df_display[COL_BUSCA] <= busca_range[1]) |
|
|
] |
|
|
|
|
|
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)] |
|
|
|
|
|
|
|
|
if keywords: |
|
|
|
|
|
keyword_list = [kw.strip().lower() for kw in keywords.split(',') if kw.strip()] |
|
|
|
|
|
|
|
|
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 |
|
|
)] |
|
|
|
|
|
|
|
|
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("---") |
|
|
|
|
|
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]) |