import streamlit as st import requests import pandas as pd from datetime import datetime, date API_BASE = "http://localhost:8000/api/v1" STATUS_MAP = { 1: ("Ricevuto", "🔵"), 2: ("Validato", "🟡"), 3: ("Compilazione", "🟠"), 5: ("Arricchito", "🟢"), 6: ("Calcolo", "🔵"), 8: ("Completato", "✅"), 9: ("Errore", "🔴"), } def status_label(stato_ordine): """Ritorna label con emoji per lo stato.""" name, emoji = STATUS_MAP.get(stato_ordine, ("Sconosciuto", "⚪")) return f"{emoji} {name}" # --------------------------------------------------------------------------- # API helpers # --------------------------------------------------------------------------- def fetch_orders(): """Recupera la lista ordini dall'API.""" try: resp = requests.get(f"{API_BASE}/orders/list", timeout=15) data = resp.json() if data.get("success"): return data.get("data", []) return [] except requests.ConnectionError: st.error("Impossibile connettersi all'API. Verifica che il server sia attivo su localhost:8000.") return [] except Exception as e: st.error(f"Errore nel caricamento degli ordini: {e}") return [] def fetch_order_detail(id_ordine): """Recupera il dettaglio completo di un ordine.""" try: resp = requests.get(f"{API_BASE}/orders/detail/{id_ordine}", timeout=15) data = resp.json() if data.get("success"): return data.get("order") st.error(data.get("detail", "Errore nel recupero dettaglio ordine")) return None except requests.ConnectionError: st.error("Impossibile connettersi all'API.") return None except Exception as e: st.error(f"Errore: {e}") return None def api_retry_order(id_ordine): """Chiama POST /orders/retry/{id_ordine}.""" try: resp = requests.post(f"{API_BASE}/orders/retry/{id_ordine}", timeout=15) return resp.json() except Exception as e: return {"success": False, "error": str(e)} def api_delete_order(id_ordine): """Chiama DELETE /orders/{id_ordine}.""" try: resp = requests.delete(f"{API_BASE}/orders/{id_ordine}", timeout=15) return resp.json() except Exception as e: return {"success": False, "error": str(e)} def download_excel(id_ordine): """Scarica il file Excel per un ordine.""" try: resp = requests.get(f"{API_BASE}/orders/export/{id_ordine}", timeout=120) if resp.status_code == 200: return resp.content st.error(f"Errore download Excel: {resp.json().get('detail', resp.status_code)}") return None except Exception as e: st.error(f"Errore download: {e}") return None def download_sources(id_ordine): """Scarica lo ZIP delle fonti PDF per un ordine.""" try: resp = requests.get(f"{API_BASE}/orders/export-sources/{id_ordine}", timeout=300) if resp.status_code == 200: return resp.content st.error(f"Errore download fonti: {resp.json().get('detail', resp.status_code)}") return None except Exception as e: st.error(f"Errore download: {e}") return None # --------------------------------------------------------------------------- # Session state init # --------------------------------------------------------------------------- if "selected_order_id" not in st.session_state: st.session_state.selected_order_id = None if "confirm_delete" not in st.session_state: st.session_state.confirm_delete = False if "orders_cache" not in st.session_state: st.session_state.orders_cache = None # --------------------------------------------------------------------------- # Page config # --------------------------------------------------------------------------- st.set_page_config(page_title="Gestione Ordini", page_icon="📋", layout="wide") st.title("📋 Gestione Ordini") with st.expander("ℹ️ Come usare questa pagina"): st.markdown(""" Questa pagina mostra tutti gli **ordini PIF** effettuati e il loro stato di avanzamento. #### Cosa puoi fare - **Visualizzare** la lista di tutti gli ordini con prodotto, cliente, data e stato - **Aprire un ordine** cliccando sul suo numero (`#ID`) per vedere il dettaglio completo, inclusa la **lista QQ** degli ingredienti - **Scaricare l'Excel** contenente il SED, il calcolo del MoS e la tabella QQ - **Scaricare le Fonti PDF** — uno ZIP con tutti i PDF di ECHA e CosIng relativi all'ordine - **Eliminare** un ordine dal dettaglio (richiede conferma) #### Stati degli ordini | Stato | Significato | |---|---| | 🔵 Ricevuto | Ordine appena creato, in attesa di elaborazione | | 🟡 Validato | Dati validati, pronto per la compilazione | | 🟠 Compilazione | Elaborazione in corso | | 🟢 Arricchito | Dati arricchiti, pronto per il calcolo | | 🔵 Calcolo | Calcolo MoS in corso | | ✅ Completato | Ordine completato, Excel e PDF disponibili | | 🔴 Errore | Errore durante l'elaborazione — usa **Retry** per riprovare | #### Filtri Usa i filtri in cima alla lista per restringere la ricerca per **data**, **cliente** o **stato**. """) # --------------------------------------------------------------------------- # Detail View # --------------------------------------------------------------------------- def show_order_detail(id_ordine): """Mostra il dettaglio di un ordine selezionato.""" # Bottone per tornare alla lista if st.button("← Torna alla lista", key="back_btn"): st.session_state.selected_order_id = None st.session_state.confirm_delete = False st.rerun() st.divider() with st.spinner("Caricamento dettaglio ordine..."): order = fetch_order_detail(id_ordine) if not order: st.error(f"Impossibile caricare l'ordine {id_ordine}") return stato = order.get("stato_ordine", 0) nome_stato = order.get("nome_stato", "Sconosciuto") _, emoji = STATUS_MAP.get(stato, ("Sconosciuto", "⚪")) has_project = order.get("uuid_progetto") is not None # Header st.header(f"{order.get('product_name', 'N/D')} — #{id_ordine}") st.markdown(f"**Stato:** {emoji} {nome_stato}") # Metriche col1, col2, col3, col4 = st.columns(4) col1.metric("Cliente", order.get("nome_cliente", "N/D")) col2.metric("Compilatore", order.get("nome_compilatore", "N/D") or "N/D") col3.metric("Preset", order.get("preset_esposizione", "N/D") or "N/D") col4.metric("Data", order.get("data_ordine", "N/D")[:10] if order.get("data_ordine") else "N/D") # Note / Log note = order.get("note") if note: with st.expander("📝 Note / Log", expanded=(stato == 9)): if stato == 9: st.error(note) else: st.info(note) # Ingredienti st.subheader("Ingredienti") ingredients = order.get("ingredients", []) if ingredients: df = pd.DataFrame(ingredients) display_cols = [] col_rename = {} for col_name in ["inci", "cas", "percentage", "is_colorante", "skip_tox"]: if col_name in df.columns: display_cols.append(col_name) col_rename[col_name] = { "inci": "INCI", "cas": "CAS", "percentage": "Percentuale (%)", "is_colorante": "Colorante", "skip_tox": "Skip Tox", }.get(col_name, col_name) df_display = df[display_cols].rename(columns=col_rename) # Mappa booleani a Si/No for col_name in ["Colorante", "Skip Tox"]: if col_name in df_display.columns: df_display[col_name] = df_display[col_name].map({True: "Sì", False: "-"}) st.dataframe(df_display, use_container_width=True, hide_index=True) else: st.info("Nessun ingrediente disponibile") st.divider() # Action buttons st.subheader("Azioni") btn_cols = st.columns(5) # 1. Aggiorna stato with btn_cols[0]: if st.button("🔄 Aggiorna", key="refresh_btn", use_container_width=True): st.rerun() # 2. Retry (solo se ERRORE) with btn_cols[1]: if stato == 9: if st.button("🔁 Retry", key="retry_btn", type="primary", use_container_width=True): with st.spinner("Retry in corso..."): result = api_retry_order(id_ordine) if result.get("success"): st.success(result.get("message", "Retry avviato")) st.rerun() else: st.error(result.get("error") or result.get("detail", "Errore retry")) else: st.button("🔁 Retry", key="retry_btn_disabled", disabled=True, use_container_width=True) # 3. Scarica Excel with btn_cols[2]: if has_project: excel_data = None if st.button("📊 Scarica Excel", key="excel_prep_btn", use_container_width=True): with st.spinner("Generazione Excel..."): excel_data = download_excel(id_ordine) if excel_data: st.download_button( label="💾 Salva Excel", data=excel_data, file_name=f"progetto_{id_ordine}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", key="excel_download" ) else: st.button("📊 Scarica Excel", key="excel_disabled", disabled=True, use_container_width=True, help="Progetto non ancora disponibile") # 4. Scarica Fonti PDF with btn_cols[3]: if has_project: if st.button("📄 Scarica Fonti PDF", key="pdf_prep_btn", use_container_width=True): with st.spinner("Generazione PDF fonti (può richiedere tempo)..."): zip_data = download_sources(id_ordine) if zip_data: st.download_button( label="💾 Salva ZIP", data=zip_data, file_name=f"fonti_ordine_{id_ordine}.zip", mime="application/zip", key="pdf_download" ) else: st.button("📄 Scarica Fonti PDF", key="pdf_disabled", disabled=True, use_container_width=True, help="Progetto non ancora disponibile") # 5. Elimina ordine with btn_cols[4]: if not st.session_state.confirm_delete: if st.button("🗑️ Elimina", key="delete_btn", use_container_width=True): st.session_state.confirm_delete = True st.rerun() else: st.warning("Confermi l'eliminazione?") sub_cols = st.columns(2) with sub_cols[0]: if st.button("✅ Conferma", key="confirm_yes", type="primary", use_container_width=True): with st.spinner("Eliminazione in corso..."): result = api_delete_order(id_ordine) if result.get("success"): st.success("Ordine eliminato") st.session_state.selected_order_id = None st.session_state.confirm_delete = False st.session_state.orders_cache = None st.rerun() else: st.error(result.get("detail") or result.get("error", "Errore eliminazione")) with sub_cols[1]: if st.button("❌ Annulla", key="confirm_no", use_container_width=True): st.session_state.confirm_delete = False st.rerun() # --------------------------------------------------------------------------- # List View # --------------------------------------------------------------------------- def show_orders_list(): """Mostra la lista degli ordini con filtri.""" # Filtri st.subheader("Filtri") filter_cols = st.columns([2, 2, 2, 1]) with filter_cols[0]: date_range = st.date_input( "Intervallo date", value=[], key="date_filter", help="Filtra per data ordine" ) with filter_cols[1]: client_search = st.text_input("Cerca cliente", key="client_filter", placeholder="Nome cliente...") with filter_cols[2]: status_options = [f"{v[1]} {v[0]}" for v in STATUS_MAP.values()] status_keys = list(STATUS_MAP.keys()) selected_statuses = st.multiselect("Stato", status_options, key="status_filter") with filter_cols[3]: st.write("") # spacer st.write("") if st.button("🔄 Aggiorna", key="refresh_list", use_container_width=True): st.session_state.orders_cache = None st.rerun() st.divider() # Carica ordini if st.session_state.orders_cache is None: with st.spinner("Caricamento ordini..."): st.session_state.orders_cache = fetch_orders() orders = st.session_state.orders_cache if not orders: st.info("Nessun ordine trovato") return # Applica filtri filtered = orders # Filtro data if date_range and len(date_range) == 2: start_date, end_date = date_range filtered = [ o for o in filtered if o.get("data_ordine") and start_date <= datetime.fromisoformat(o["data_ordine"]).date() <= end_date ] # Filtro cliente if client_search: search_lower = client_search.lower() filtered = [ o for o in filtered if o.get("nome_cliente") and search_lower in o["nome_cliente"].lower() ] # Filtro stato if selected_statuses: selected_ids = [] for s in selected_statuses: for k, v in STATUS_MAP.items(): if f"{v[1]} {v[0]}" == s: selected_ids.append(k) filtered = [o for o in filtered if o.get("stato_ordine") in selected_ids] if not filtered: st.warning("Nessun ordine corrisponde ai filtri selezionati") return st.caption(f"{len(filtered)} ordini trovati") # Mostra lista for order in filtered: id_ord = order.get("id_ordine") product = order.get("product_name", "N/D") or "N/D" client = order.get("nome_cliente", "N/D") or "N/D" stato = order.get("stato_ordine", 0) data = order.get("data_ordine", "") note = order.get("note", "") data_display = data[:10] if data else "N/D" stato_display = status_label(stato) note_preview = (note[:60] + "...") if note and len(note) > 60 else (note or "") col_btn, col_info = st.columns([1, 5]) with col_btn: if st.button(f"#{id_ord}", key=f"order_{id_ord}", use_container_width=True): st.session_state.selected_order_id = id_ord st.session_state.confirm_delete = False st.rerun() with col_info: info_cols = st.columns([3, 2, 1.5, 1.5, 3]) info_cols[0].write(f"**{product}**") info_cols[1].write(client) info_cols[2].write(data_display) info_cols[3].write(stato_display) if note_preview: info_cols[4].caption(note_preview) # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if st.session_state.selected_order_id is not None: show_order_detail(st.session_state.selected_order_id) else: show_orders_list()