import pandas as pd import streamlit as st from datetime import datetime, date from functions import ( STATUS_MAP, api_delete_order, api_retry_order, download_excel, download_sources, fetch_order_detail, fetch_orders, status_label, ) # --------------------------------------------------------------------------- # 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.""" 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) 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, width="stretch", hide_index=True) else: st.info("Nessun ingrediente disponibile") st.divider() # Action buttons st.subheader("Azioni") btn_cols = st.columns(5) with btn_cols[0]: if st.button("🔄 Aggiorna", key="refresh_btn", width="stretch"): st.rerun() with btn_cols[1]: if stato == 9: if st.button("🔁 Retry", key="retry_btn", type="primary", width="stretch"): 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, width="stretch") with btn_cols[2]: if has_project: excel_data = None if st.button("📊 Scarica Excel", key="excel_prep_btn", width="stretch"): 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, width="stretch", help="Progetto non ancora disponibile") with btn_cols[3]: if has_project: if st.button("📄 Scarica Fonti PDF", key="pdf_prep_btn", width="stretch"): 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, width="stretch", help="Progetto non ancora disponibile") with btn_cols[4]: if not st.session_state.confirm_delete: if st.button("🗑️ Elimina", key="delete_btn", width="stretch"): 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", width="stretch"): 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", width="stretch"): st.session_state.confirm_delete = False st.rerun() # --------------------------------------------------------------------------- # List View # --------------------------------------------------------------------------- def show_orders_list(): """Mostra la lista degli ordini con 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()] selected_statuses = st.multiselect("Stato", status_options, key="status_filter") with filter_cols[3]: st.write("") st.write("") if st.button("🔄 Aggiorna", key="refresh_list", width="stretch"): st.session_state.orders_cache = None st.rerun() st.divider() 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 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 ] 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() ] if selected_statuses: selected_ids = [ k for k, v in STATUS_MAP.items() if f"{v[1]} {v[0]}" in selected_statuses ] 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") 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}", width="stretch"): 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()