import streamlit as st
import pandas as pd
from datetime import datetime, timedelta
import PyPDF2
import re
import io
# --- CONFIGURAZIONE ---
st.set_page_config(page_title="Programma Canile Pro", layout="wide")
# Inizializzazione session_state per sostituire il database
if 'storico' not in st.session_state:
st.session_state.storico = []
if 'anagrafica_cani' not in st.session_state:
st.session_state.anagrafica_cani = {}
if 'programma' not in st.session_state:
st.session_state.programma = []
def parse_dog_pdf(uploaded_file):
"""
Legge il PDF del cane ed estrae i dati strutturati.
I titoli sono: CIBO, GUINZAGLIERIA, STRUMENTI, ATTIVITÀ, NOTE, TEMPO
"""
reader = PyPDF2.PdfReader(uploaded_file)
# Estrai tutto il testo dal PDF
full_text = ""
for page in reader.pages:
full_text += page.extract_text()
# Lista dei titoli attesi nell'ordine
TITOLI = ["CIBO", "GUINZAGLIERIA", "STRUMENTI", "ATTIVITÀ", "NOTE", "TEMPO"]
# Dizionario per memorizzare i dati
dati = {
"nome": uploaded_file.name.replace(".pdf", "").upper(),
"cibo": "",
"guinzaglieria": "",
"strumenti": "",
"attivita": "",
"note": "",
"tempo": ""
}
# Usa regex per trovare ogni titolo e il suo contenuto
for i, titolo in enumerate(TITOLI):
if i < len(TITOLI) - 1:
next_titolo = TITOLI[i + 1]
pattern = rf'{titolo}\s+(.*?)\s*(?={next_titolo})'
else:
pattern = rf'{titolo}\s+(.*?)$'
match = re.search(pattern, full_text, re.DOTALL | re.MULTILINE)
if match:
contenuto = match.group(1).strip()
campo_map = {
'CIBO': 'cibo',
'GUINZAGLIERIA': 'guinzaglieria',
'STRUMENTI': 'strumenti',
'ATTIVITÀ': 'attivita',
'NOTE': 'note',
'TEMPO': 'tempo'
}
campo = campo_map.get(titolo)
if campo:
dati[campo] = contenuto
return dati
def salva_anagrafica(dati):
"""Salva i dati del cane nella session_state."""
st.session_state.anagrafica_cani[dati["nome"]] = dati
def get_anagrafica_cane(nome_cane):
"""Recupera l'anagrafica di un cane."""
if nome_cane in st.session_state.anagrafica_cani:
return st.session_state.anagrafica_cani[nome_cane]
return {
"cibo": "",
"guinzaglieria": "",
"strumenti": "",
"attivita": "",
"note": "",
"tempo": ""
}
def genera_excel_anagrafica():
"""Genera un file Excel con l'anagrafica dei cani."""
if st.session_state.anagrafica_cani:
df = pd.DataFrame.from_dict(st.session_state.anagrafica_cani, orient='index')
df.reset_index(inplace=True)
df.rename(columns={'index': 'nome'}, inplace=True)
output = io.BytesIO()
df.to_excel(output, index=False, engine='openpyxl')
output.seek(0)
return output
return None
def genera_excel_programma(programma, data_turno):
"""Genera un file Excel con il programma completo del turno."""
df = pd.DataFrame(programma)
cols_order = ["Orario", "Cane", "Volontario", "Luogo", "Tipo", "CIBO", "GUINZAGLIERIA", "STRUMENTI", "ATTIVITÀ", "NOTE", "TEMPO"]
df = df[cols_order]
output = io.BytesIO()
df.to_excel(output, index=False, engine='openpyxl')
output.seek(0)
return output
def load_gsheets(sheet_name):
"""Carica dati da Google Sheets."""
url = f"https://docs.google.com/spreadsheets/d/1pcFa454IT1tlykbcK-BeAU9hnIQ_D8V_UuZaKI_KtYM/gviz/tq?tqx=out:csv&sheet={sheet_name}"
try:
df = pd.read_csv(url)
df.columns = [c.strip().lower() for c in df.columns]
if sheet_name == "Luoghi":
if 'automatico' not in df.columns:
df['automatico'] = 'sì'
if 'adiacente' not in df.columns:
df['adiacente'] = ''
if sheet_name == "Cani":
if 'reattività' not in df.columns:
df['reattività'] = 0
df['reattività'] = pd.to_numeric(df['reattività'], errors='coerce').fillna(0)
return df.dropna(how='all')
except:
return pd.DataFrame()
def get_reattivita_cane(nome_cane, df_cani):
"""Restituisce il livello di reattività di un cane."""
if df_cani.empty or 'reattività' not in df_cani.columns:
return 0
riga = df_cani[df_cani['nome'] == nome_cane]
return float(riga.iloc[0]['reattività']) if not riga.empty else 0
def get_campi_adiacenti(campo, df_luoghi):
"""Restituisce la lista dei campi adiacenti a un campo dato."""
if df_luoghi.empty or 'adiacente' not in df_luoghi.columns:
return []
riga = df_luoghi[df_luoghi['nome'] == campo]
if not riga.empty:
adiacenti_str = str(riga.iloc[0]['adiacente']).strip()
if adiacenti_str and adiacenti_str != 'nan':
return [c.strip() for c in adiacenti_str.split(',') if c.strip()]
return []
def campo_valido_per_reattivita(cane, campo, turni_attuali, ora_attuale_str, df_cani, df_luoghi):
"""Verifica se un campo è valido per un cane considerando la reattività dei cani adiacenti."""
reattivita_cane = get_reattivita_cane(cane, df_cani)
if reattivita_cane == 0:
return True
campi_adiacenti = get_campi_adiacenti(campo, df_luoghi)
if not campi_adiacenti:
return True
for turno in turni_attuali:
if turno["Orario"] == ora_attuale_str and turno["Luogo"] in campi_adiacenti:
cane_adiacente = turno["Cane"]
reattivita_adiacente = get_reattivita_cane(cane_adiacente, df_cani)
if reattivita_cane + reattivita_adiacente > 3:
return False
return True
def salva_programma_nel_db(programma, data):
"""Salva il programma nello storico."""
for entry in programma:
if entry["Tipo"] != "Pasti":
st.session_state.storico.append({
"data": data.strftime('%Y-%m-%d'),
"inizio": entry["Orario"],
"cane": entry["Cane"],
"volontario": entry["Volontario"],
"luogo": entry["Luogo"]
})
def get_storico_volontario_cane(cane, volontario):
"""Conta quante volte un volontario ha portato fuori un cane."""
count = 0
for entry in st.session_state.storico:
if entry["cane"] == cane and entry["volontario"] == volontario:
count += 1
return count
# --- SIDEBAR ---
with st.sidebar:
st.title("🐕 Gestione Canile")
st.divider()
st.subheader("📤 Caricamento PDF Cani")
uploaded_files = st.file_uploader(
"Carica i PDF dei cani",
type=["pdf"],
accept_multiple_files=True,
help="Ogni PDF deve contenere: CIBO, GUINZAGLIERIA, STRUMENTI, ATTIVITÀ, NOTE, TEMPO"
)
if uploaded_files:
if st.button("📥 Aggiorna anagrafica da PDF", type="primary", use_container_width=True):
for pdf_file in uploaded_files:
try:
dati_cane = parse_dog_pdf(pdf_file)
salva_anagrafica(dati_cane)
st.success(f"✅ {dati_cane['nome']} caricato!")
except Exception as e:
st.error(f"❌ Errore nel file {pdf_file.name}: {e}")
st.rerun()
# --- MAIN CONTENT ---
tab_prog, tab_ana, tab_stats = st.tabs(["📅 Programma Turno", "📋 Anagrafica", "📊 Statistiche"])
with tab_prog:
st.header("📅 Generazione Programma Turno")
df_cani = load_gsheets("Cani")
df_volontari = load_gsheets("Volontari")
df_luoghi = load_gsheets("Luoghi")
if df_cani.empty or df_volontari.empty or df_luoghi.empty:
st.error("❌ Impossibile caricare i dati da Google Sheets. Verifica la connessione.")
st.stop()
col_param1, col_param2, col_param3 = st.columns(3)
data_t = col_param1.date_input("📅 Data Turno", datetime.today())
ora_ini = col_param2.time_input("⏰ Inizio Turno", datetime.strptime("09:00", "%H:%M").time())
ora_pasti = col_param3.time_input("🍖 Orario Pasti", datetime.strptime("12:30", "%H:%M").time())
st.divider()
with st.expander("➕ Inserimento Manuale"):
col_m1, col_m2, col_m3, col_m4 = st.columns(4)
ora_man = col_m1.time_input("Orario", datetime.strptime("10:00", "%H:%M").time(), key="man_ora")
cane_man = col_m2.selectbox("Cane", df_cani['nome'].tolist(), key="man_cane")
vol_man = col_m3.selectbox("Volontario", df_volontari['nome'].tolist(), key="man_vol")
luogo_man = col_m4.selectbox("Luogo", df_luoghi['nome'].tolist(), key="man_luogo")
if st.button("➕ Aggiungi Turno Manuale", use_container_width=True):
ana_data = get_anagrafica_cane(cane_man)
st.session_state.programma.append({
"Orario": ora_man.strftime('%H:%M'),
"Cane": cane_man,
"Volontario": vol_man,
"Luogo": luogo_man,
"Tipo": "Manuale",
"Inizio_Sort": ora_man.strftime('%H:%M'),
"CIBO": ana_data["cibo"],
"GUINZAGLIERIA": ana_data["guinzaglieria"],
"STRUMENTI": ana_data["strumenti"],
"ATTIVITÀ": ana_data["attivita"],
"NOTE": ana_data["note"],
"TEMPO": ana_data["tempo"]
})
st.success("✅ Turno aggiunto!")
st.rerun()
st.divider()
c1, c2, c3 = st.columns(3)
if c1.button("🤖 Genera Automatico", type="primary", use_container_width=True):
st.session_state.programma = []
cani_auto = df_cani[df_cani['automatico'] == 'sì']['nome'].tolist()
luoghi_auto = df_luoghi[df_luoghi['automatico'] == 'sì']['nome'].tolist()
volontari_list = df_volontari['nome'].tolist()
manuali = [p for p in st.session_state.programma if p["Tipo"] == "Manuale"]
inizio_dt = datetime.combine(data_t, ora_ini)
pasti_dt = datetime.combine(data_t, ora_pasti)
curr_t = inizio_dt
while curr_t < pasti_dt:
ora_s = curr_t.strftime('%H:%M')
turni_ora = [p for p in st.session_state.programma if p["Orario"] == ora_s]
v_liberi = [v for v in volontari_list if v not in [t["Volontario"] for t in turni_ora]]
c_liberi = [c for c in cani_auto if c not in [t["Cane"] for t in turni_ora]]
l_liberi = [l for l in luoghi_auto if l not in [t["Luogo"] for t in turni_ora]]
for cane in c_liberi[:]:
if not v_liberi or not l_liberi:
break
campo_scelto = None
for campo in l_liberi:
if campo_valido_per_reattivita(cane, campo, turni_ora, ora_s, df_cani, df_luoghi):
campo_scelto = campo
l_liberi.remove(campo)
break
if campo_scelto:
v_scores = [(v, get_storico_volontario_cane(cane, v)) for v in v_liberi]
v_scores.sort(key=lambda x: x[1], reverse=True)
lead = v_scores[0][0]
v_liberi.remove(lead)
ana_data = get_anagrafica_cane(cane)
st.session_state.programma.append({
"Orario": ora_s,
"Cane": cane,
"Volontario": lead,
"Luogo": campo_scelto,
"Tipo": "Auto",
"Inizio_Sort": ora_s,
"CIBO": ana_data["cibo"],
"GUINZAGLIERIA": ana_data["guinzaglieria"],
"STRUMENTI": ana_data["strumenti"],
"ATTIVITÀ": ana_data["attivita"],
"NOTE": ana_data["note"],
"TEMPO": ana_data["tempo"]
})
curr_t += timedelta(minutes=45)
st.session_state.programma.extend(manuali)
st.session_state.programma.append({
"Orario": pasti_dt.strftime('%H:%M'),
"Cane": "TUTTI",
"Volontario": "TUTTI",
"Luogo": "Box",
"Tipo": "Pasti",
"Inizio_Sort": pasti_dt.strftime('%H:%M'),
"CIBO": "",
"GUINZAGLIERIA": "",
"STRUMENTI": "",
"ATTIVITÀ": "",
"NOTE": "",
"TEMPO": ""
})
st.success("✅ Programma generato automaticamente")
st.rerun()
if c2.button("💾 Conferma e Salva Storico", type="primary", use_container_width=True):
if st.session_state.programma:
salva_programma_nel_db(st.session_state.programma, data_t)
st.success("✅ Programma salvato con successo nello storico!")
else:
st.warning("⚠️ Nessun programma da salvare")
if c3.button("🗑️ Svuota Tutto", use_container_width=True):
st.session_state.programma = []
st.success("✅ Programma svuotato")
st.rerun()
st.divider()
if st.session_state.programma:
st.subheader("📋 Programma Corrente")
df_p = pd.DataFrame(st.session_state.programma).sort_values("Inizio_Sort")
cols_order = ["Orario", "Cane", "Volontario", "Luogo", "Tipo", "CIBO", "GUINZAGLIERIA", "STRUMENTI", "ATTIVITÀ", "NOTE", "TEMPO"]
df_p_display = df_p[cols_order]
st.dataframe(
df_p_display,
use_container_width=True,
hide_index=True,
column_config={
"Orario": st.column_config.TextColumn("Orario", width="small"),
"Cane": st.column_config.TextColumn("Cane", width="medium"),
"Volontario": st.column_config.TextColumn("Volontario", width="medium"),
"Luogo": st.column_config.TextColumn("Luogo", width="medium"),
"Tipo": st.column_config.TextColumn("Tipo", width="small"),
"CIBO": st.column_config.TextColumn("CIBO", width="medium"),
"GUINZAGLIERIA": st.column_config.TextColumn("GUINZAGLIERIA", width="medium"),
"STRUMENTI": st.column_config.TextColumn("STRUMENTI", width="medium"),
"ATTIVITÀ": st.column_config.TextColumn("ATTIVITÀ", width="medium"),
"NOTE": st.column_config.TextColumn("NOTE", width="large"),
"TEMPO": st.column_config.TextColumn("TEMPO", width="small")
}
)
st.divider()
excel_data = genera_excel_programma(st.session_state.programma, data_t)
if excel_data:
st.download_button(
"📊 Scarica Programma Excel",
excel_data,
file_name=f"programma_turno_{data_t.strftime('%Y%m%d')}.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True
)
else:
st.info("ℹ️ Nessun turno programmato. Usa 'Genera Automatico' o 'Inserimento Manuale'")
with tab_ana:
st.header("📋 Anagrafica Cani")
st.markdown("*Database completo dei cani caricati tramite PDF*")
if st.session_state.anagrafica_cani:
df_db = pd.DataFrame.from_dict(st.session_state.anagrafica_cani, orient='index')
df_db.reset_index(inplace=True)
df_db.rename(columns={'index': 'nome'}, inplace=True)
st.success(f"✅ {len(df_db)} cani in anagrafica")
st.dataframe(
df_db,
use_container_width=True,
hide_index=True,
column_config={
"nome": st.column_config.TextColumn("Nome", width="medium"),
"cibo": st.column_config.TextColumn("CIBO", width="medium"),
"guinzaglieria": st.column_config.TextColumn("GUINZAGLIERIA", width="medium"),
"strumenti": st.column_config.TextColumn("STRUMENTI", width="medium"),
"attivita": st.column_config.TextColumn("ATTIVITÀ", width="medium"),
"note": st.column_config.TextColumn("NOTE", width="large"),
"tempo": st.column_config.TextColumn("TEMPO", width="small")
}
)
st.divider()
excel_data = genera_excel_anagrafica()
if excel_data:
st.download_button(
"📊 Scarica Anagrafica Excel",
excel_data,
file_name="anagrafica_cani.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True
)
else:
st.info("ℹ️ Nessun cane in anagrafica. Carica i PDF dalla barra laterale.")
st.markdown("""
**Come procedere:**
1. Prepara i PDF dei cani con i seguenti campi:
- **CIBO**: informazioni sull'alimentazione
- **GUINZAGLIERIA**: tipo di guinzaglio/pettorina
- **STRUMENTI**: attrezzature necessarie
- **ATTIVITÀ**: attività consigliate
- **NOTE**: osservazioni comportamentali
- **TEMPO**: durata consigliata uscita
2. Carica i PDF dalla sidebar
3. Clicca su "Aggiorna anagrafica da PDF"
""")
with tab_stats:
st.header("📊 Statistiche Storiche")
col_a, col_b = st.columns(2)
d_ini = col_a.date_input("Inizio Periodo", datetime.today() - timedelta(days=30))
d_end = col_b.date_input("Fine Periodo", datetime.today())
# Filtra storico per date
df_h = pd.DataFrame(st.session_state.storico)
if not df_h.empty:
df_h['data'] = pd.to_datetime(df_h['data'])
mask = (df_h['data'] >= pd.to_datetime(d_ini)) & (df_h['data'] <= pd.to_datetime(d_end))
df_h = df_h[mask]
if not df_h.empty:
st.success(f"✅ Trovate {len(df_h)} attività nel periodo selezionato")
filtro = st.radio("Filtra per:", ["Volontario", "Cane"], horizontal=True)
ogg = st.selectbox(f"Seleziona {filtro}", sorted(df_h[filtro.lower()].unique()))
res = df_h[df_h[filtro.lower()] == ogg]
col_stat1, col_stat2 = st.columns(2)
col_stat1.metric(f"Attività totali per {ogg}", len(res))
if filtro == "Cane":
volontari_unici = res['volontario'].nunique()
col_stat2.metric("Volontari diversi", volontari_unici)
else:
cani_unici = res['cane'].nunique()
col_stat2.metric("Cani diversi", cani_unici)
st.divider()
st.dataframe(res, hide_index=True, use_container_width=True)
else:
st.warning("⚠️ Nessun dato presente per le date selezionate.")