240 lines
No EOL
11 KiB
Python
240 lines
No EOL
11 KiB
Python
import re
|
|
|
|
import streamlit as st
|
|
|
|
from functions_ui import search_cas_inci
|
|
|
|
# Configure page
|
|
st.set_page_config(
|
|
page_title="LMB App",
|
|
page_icon="🔬",
|
|
layout="wide"
|
|
)
|
|
|
|
# Password protection
|
|
def check_password():
|
|
"""Returns `True` if the user had the correct password."""
|
|
|
|
def password_entered():
|
|
"""Checks whether a password entered by the user is correct."""
|
|
if st.session_state["password"] == st.secrets["passwords"]["app_password"]:
|
|
st.session_state["password_correct"] = True
|
|
del st.session_state["password"] # Don't store password
|
|
else:
|
|
st.session_state["password_correct"] = False
|
|
|
|
# First run, show input for password
|
|
if "password_correct" not in st.session_state:
|
|
st.text_input(
|
|
"Password", type="password", on_change=password_entered, key="password"
|
|
)
|
|
return False
|
|
# Password not correct, show input + error
|
|
elif not st.session_state["password_correct"]:
|
|
st.text_input(
|
|
"Password", type="password", on_change=password_entered, key="password"
|
|
)
|
|
st.error("😕 Password incorrect")
|
|
return False
|
|
# Password correct
|
|
else:
|
|
return True
|
|
|
|
if not check_password():
|
|
st.stop()
|
|
|
|
# Define home page function
|
|
def home():
|
|
st.title("LMB App: PIF & Database Tossicologico")
|
|
|
|
# Inizializza session_state per il CAS number se non esiste
|
|
if 'selected_cas' not in st.session_state:
|
|
st.session_state.selected_cas = None
|
|
|
|
with st.container(border=True):
|
|
col_left, col_right = st.columns([2, 1])
|
|
with col_left:
|
|
type = st.radio("Cerca per:", ("CAS", "INCI"), index=0, key="search_mode", horizontal=True)
|
|
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)
|
|
|
|
if input:
|
|
st.caption(f"Ricerca per {input}: trovati i seguenti ingredienti.")
|
|
if type == "CAS":
|
|
results = search_cas_inci(input, type='cas')
|
|
else:
|
|
results = search_cas_inci(input, type='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 and selected_display != "":
|
|
cas_pattern = r'\b\d{2,7}-\d{2}-\d\b'
|
|
found_cas = re.findall(cas_pattern, 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]
|
|
#st.info(f"CAS rilevato: {selected_cas}")
|
|
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
|
|
#st.success(f"CAS selezionato: {selected_cas}")
|
|
else:
|
|
st.warning("Nessun risultato trovato nel database.")
|
|
if st.button("Usa questo CAS") and type == "CAS":
|
|
st.session_state.selected_cas = input.strip()
|
|
st.success(f"CAS salvato: {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
|
|
|
|
|
|
|
|
# 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"):
|
|
# Placeholder for future versions
|
|
st.markdown("""
|
|
### 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 Database", icon="🧪")
|
|
#cosing_page = st.Page("pages/cosing.py", title="CosIng", icon="💄")
|
|
#dap_page = st.Page("pages/dap.py", title="DAP", 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="📦")
|
|
|
|
pg = st.navigation({
|
|
"Ricerca": [home_page, ingredients_page],
|
|
"PIF": [list_orders, order_page, exposition_page],
|
|
"Online": [echa_page],
|
|
})
|
|
|
|
pg.run() |