import re import streamlit as st from functions import check_auth, do_logout, get_cookie_manager from functions_ui import search_cas_inci, search_cir, show_login_page # Configure page st.set_page_config( page_title="LMB App", page_icon="🔬", layout="wide" ) get_cookie_manager() # deve essere renderizzato prima di check_auth if not check_auth(): show_login_page() st.stop() # Define home page function def home(): name = st.session_state.get("user_name") or st.session_state.get("user_email") or "" greeting = f"Benvenuto, {name}" if name else "Benvenuto" st.title("LMB App: PIF & Database Tossicologico") st.caption(greeting) if "selected_cas" not in st.session_state: st.session_state.selected_cas = None if "selected_inci" not in st.session_state: st.session_state.selected_inci = None with st.container(border=True): col_left, col_right = st.columns([2, 1]) with col_left: search_type = st.radio("Cerca per:", ("CAS", "INCI"), index=0, key="search_mode", horizontal=True) search_input = st.text_input("Inserisci:", "") with col_right: real_time = st.checkbox("Ricerca online", value=False, key="real_time_search") force_refresh = st.checkbox("Aggiorna Ingrediente", value=False) search_cir_enabled = st.checkbox("Cerca in CIR", value=False, key="search_cir") if search_input: st.caption(f"Ricerca per {search_input}: trovati i seguenti ingredienti.") results = search_cas_inci(search_input, type="cas" if search_type == "CAS" else "inci") if results: display_options = [f"{cas} - {inci}" for cas, inci in results] selected_display = st.selectbox("Risultati", options=[""] + display_options, key="cas_selectbox") if selected_display: found_cas = re.findall(r"\b\d{2,7}-\d{2}-\d\b", selected_display) unique_cas = list(dict.fromkeys(found_cas)) if not unique_cas: st.warning("Nessun pattern CAS valido trovato nella stringa.") selected_cas = None elif len(unique_cas) == 1: selected_cas = unique_cas[0] else: selected_cas = st.selectbox( label="Sono stati rilevati più CAS. Selezionane uno:", options=unique_cas ) if selected_cas: st.session_state.selected_cas = selected_cas match = next((inci for cas, inci in results if cas == selected_cas), None) st.session_state.selected_inci = match else: st.warning("Nessun risultato trovato nel database.") if st.button("Usa questo CAS") and search_type == "CAS": st.session_state.selected_cas = search_input.strip() st.session_state.selected_inci = None st.success(f"CAS salvato: {search_input}") else: st.info("INCI non trovato, cerca per CAS o modifica l'input.") if st.session_state.selected_cas: st.info(f"CAS corrente: {st.session_state.selected_cas}") if st.button("Vai a Ingredienti →", type="primary"): st.switch_page(ingredients_page) if real_time: if st.button("Vai a ECHA →"): st.switch_page(echa_page) st.session_state.force_refresh = force_refresh # CIR search results if search_cir_enabled and search_input: st.markdown("---") st.markdown("**Rapporti CIR:**") cir_results = search_cir(search_input) if cir_results: for name, inci, url in cir_results: label = name if name == inci else f"{name} ({inci})" st.markdown(f"- [{label}]({url})") else: st.caption("Nessun rapporto CIR trovato.") # Guide section st.divider() with st.expander("📖 Guida all'utilizzo"): st.markdown(""" ### Ricerca ingredienti La barra di ricerca in questa pagina permette di cercare un ingrediente per **CAS** o per **INCI**. Il risultato viene poi visualizzato nella pagina **Ingrediente**, che contiene: - I dati per determinare il **DAP** (caratteristiche fisico-chimiche da PubChem) - Le **restrizioni normative** da CosIng (annex, restrizioni cosmetiche, opinioni SCCS) - I dati di **tossicologia** con il miglior indicatore disponibile (NOAEL/LOAEL ecc.) - La possibilità di **scaricare direttamente le fonti** (PDF ECHA e CosIng) > **Ricerca online** — mostra anche la pagina **ECHA** con il dettaglio completo dei dossier. > È un metodo di visualizzazione più vecchio; si consiglia di usare la pagina **Ingrediente**. > **Aggiorna Ingrediente** — forza il ricalcolo e l'aggiornamento dell'ingrediente nel database, > utile per verificare eventuali cambi di restrizioni normative. --- ### Ordini PIF (Punti 6, 7, 8) Dalla sezione **PIF** è possibile: - **Nuovo PIF** — inserire i dati del prodotto e la tabella QQ degli ingredienti per avviare il calcolo automatico del **Margin of Safety (MoS)**. Il sistema seleziona automaticamente gli indicatori tossicologici migliori per ciascun ingrediente e produce un **file Excel** con SED, MoS e tabella QQ pronti per il PIF. Per prodotti simili a uno già esistente, si può inserire il **numero d'ordine precedente** per pre-compilare la tabella QQ. - **Ordini PIF** — visualizza tutti gli ordini effettuati con il relativo stato. Da qui si può: - Scaricare l'**Excel** (SED, MoS, tabella QQ) - Scaricare lo **ZIP delle fonti** (tutti i PDF ECHA e CosIng dell'ordine) - **Cancellare** un ordine (sconsigliato salvo necessità) Tutti gli ordini vengono salvati nel database, inclusa la lista degli ingredienti, così da poter individuare facilmente eventuali PIF da rivedere in caso di modifiche alle restrizioni. È possibile filtrare gli ordini per **stato**, **nome cliente** e **data**. --- ### Preset di esposizione Prima di calcolare il MoS è necessario avere almeno un **preset di esposizione** configurato nella pagina **Esposizione**. Il preset definisce i parametri del prodotto (superficie esposta, quantità giornaliera, fattore di ritenzione, vie di esposizione, ecc.). - Scegliere un nome **chiaro ed esplicativo** (es. *Crema viso leave-on*) - Non possono esistere due preset con lo stesso nome - I preset possono essere cancellati se non più necessari - I preset sono riutilizzabili per più ordini dello stesso tipo di prodotto --- > Ogni pagina dell'applicazione contiene una propria guida dettagliata sul suo funzionamento. """) # Changelog section with st.expander("📝 Registro degli aggiornamenti"): st.markdown(""" ### v1.0 - Aggiunta autenticazione con mail e password - Modificato il backend per maggiore sicurezza - Ottimizzazioni generali e refactoring del codice - Aggiunto form per segnalare errori - Migliorato l'algoritmo per scegliere l'indicatore tossicologico migliore: * Ora si preferiscono indicatori con route più rilevante per il MoS (es. NOAEL dermal > NOAEL orale) * In caso di più indicatori con stesso endpoint, si sceglie quello con la dose più bassa (solo per NOAEL/LOAEL) ### v0.9 - 2026-03-01 - Ottimizzazione della pagina 'Ingrediente', risolto un bug che impediva di caricare i dati CosIng - Modificato la validazione dei dati in esposizione - Creata una pagina di impostazione per: * Creazione di parametri custom di tossicologia per un ingrediente * Cancellazione di clienti (e relativi dati) * Visualizzare gli ingredienti salvati nel database ### v0.8 *v0.8.0 - 2026-02-22 - Versione iniziale (v0.8.0) rilasciata per test interno e feedback - Tutte le funzionalità principali implementate, ma in fase di test e ottimizzazione - Modificato il flusso di ricerca, ora tutte le informazioni si trovano su Ingrediente - Aggiustati numerosi bug minori e migliorata l'usabilità generale * Si può visionare grado di ionizzazione e altri dati DAP direttamente su Ingrediente * Ora si può scaricare direttamente il PDF del CosIng ufficiale (se esiste) dalla pagina Ingrediente * Il migliore indicatore tossicologico viene automaticamente individuato * Si può forzare l'aggiornamento dei dati di un ingrediente in caso di modifiche normative o nuovi dati tossicologici - Aggiunta la funzionalità di creare ordini per PIF e scaricare i risultati in Excel - Aggiunta la funzionalità di creare preset di esposizione personalizzati e cancellarli - Aggiunta la funzionalità di visualizzare tutti i PIF creati e il loro stato di avanzamento (con possibilità di cancellarli) - Aggiunta la funzionalità di scaricare un file ZIP con tutte le fonti (PDF ECHA e CosIng) per ogni ordine ### v0.3 *v0.3.0 - 2026-02-08 - Aggiunta pagina per la gestione dei preset dei parametri di esposizione * Permette di creare, modificare e salvare preset personalizzati per i calcoli di esposizione * In futuro per calcolare il MoS bisognerà prima selezionare un preset di esposizione - Aggiunta pagina per la creazione degli ingredienti a partire da un CAS: * Facendo una ricerca in 'Ingredienti' e selezionando un CAS, è possibile cliccare su 'Ricerca' per generare un nuovo ingrediente nel database a partire da quel CAS. * Gli ingredienti creati in questo modo hanno già i dati di base, tossicologici e quelli normativi da CosIng * E' consigliato cercare nuovi ingredienti tramite questa nuova funzione ### v0.2 *v0.2.1 - 2026-01-13* - Fix minore su ricerca CosIng **v0.2.0 - 2026-01-05** - Aggiunta ricerca per nome INCI - Possibilità di filtrare per singoli CAS in caso di multipli per stesso INCI - Verifica se il link al download esiste prima di generare il PDF - Aggiunta pagina per verificare i valori per determinare il DAP (da PubChem) - La ricerca per ingrediente su ECHA non va più in errore se almeno uno dei tre dossier esiste - Filtrati i dossier ECHA se sono di tipo 'full' e sono di tipo 'Lead' (sempre Active) --- ### v0.1 **v0.1.0 - 2025-12-18** - Release iniziale - Funzionalità di ricerca per Numero CAS - Integrazione con ECHA - Integrazione con CosIng - Protezione con password - Sistema di navigazione multi-pagina - Download del PDF ECHA dossier - Visualizzazione dati tossicologici ECHA e CosIng """) # Navigation home_page = st.Page(home, title="Cerca", icon="🏠", default=True) echa_page = st.Page("pages/echa.py", title="ECHA (legacy)", icon="🧪") exposition_page = st.Page("pages/exposition_page.py", title="Esposizione", icon="☀️") ingredients_page = st.Page("pages/ingredients_page.py", title="Ingrediente", icon="📋") order_page = st.Page("pages/order_page.py", title="Nuovo PIF", icon="🛒") list_orders = st.Page("pages/list_orders.py", title="Ordini PIF", icon="📦") settings_page = st.Page("pages/settings_page.py", title="Impostazioni", icon="⚙️") ticket_page = st.Page("pages/ticket.py", title="Segnala problema", icon="🚩") pg = st.navigation({ "Ricerca": [home_page, ingredients_page], "PIF": [list_orders, order_page, exposition_page], "Utilità": [echa_page, settings_page, ticket_page], }) with st.sidebar: if st.button("Esci", use_container_width=True): do_logout() st.rerun() pg.run()