import streamlit as st import pandas as pd from app_core.services.mappings_service import MappingsService from app_core.config.settings import STORES def render_page(): if st.session_state.get("auth_user") is None: st.warning("Please login to continue.") st.stop() st.markdown(""" """, unsafe_allow_html=True) st.markdown("## πŸ“‹ Triumph Debtor Mappings") st.caption("Manage POS account sale mappings to Triumph debtor codes β€” filtered by store.") service = MappingsService() all_mappings = service.get_all_mappings() # Store labels from config β€” used only for the "Add New" dropdown store_labels = [s["label"] for s in STORES] tab1, tab2 = st.tabs(["πŸ” View & Search", "βž• Add New Mapping"]) # ── TAB 1: View & Edit ──────────────────────────────────────────────────── with tab1: st.markdown("### πŸ” Current Mappings") if not all_mappings: st.info("No mappings found. Use the 'βž• Add New Mapping' tab to create one.") else: # Build dataframe from raw DB values data = [ { "ID": m.id, "POS Code": m.code or "", "Account Name": m.name or "", "Triumph Code": m.dbmacc or "", "Outlet": (m.outlet or "").strip(), "Created At": m.created_at.strftime("%Y-%m-%d %H:%M") if m.created_at else "β€”", "Updated At": m.updated_at.strftime("%Y-%m-%d %H:%M") if m.updated_at else "β€”", } for m in all_mappings ] df_full = pd.DataFrame(data) # Distinct outlet names actually in DB distinct_outlets = sorted([ o for o in df_full["Outlet"].dropna().unique().tolist() if o.strip() ]) f1, f2 = st.columns([1, 2]) with f1: selected_store = st.selectbox( "πŸͺ Filter by Store", options=["All Stores"] + distinct_outlets, index=0, ) with f2: search_query = st.text_input( "πŸ”Ž Search", placeholder="POS Code, Account Name, or Triumph Code…", ) df = df_full.copy() if selected_store != "All Stores": df = df[df["Outlet"] == selected_store] if search_query: q = search_query df = df[ df["POS Code"].str.contains(q, case=False, na=False) | df["Account Name"].str.contains(q, case=False, na=False) | df["Triumph Code"].str.contains(q, case=False, na=False) ] store_label = selected_store if selected_store != "All Stores" else "all stores" st.caption(f"Showing **{len(df)}** mapping(s) for **{store_label}**.") st.markdown("#### πŸ“ Edit Mappings") st.caption("Double-click any editable cell to modify. Changes are saved when you press Enter.") st.data_editor( df, hide_index=True, use_container_width=True, num_rows="dynamic", disabled=["ID", "Created At", "Updated At"], column_config={ "ID": st.column_config.NumberColumn(format="%d", width="small"), "POS Code": st.column_config.TextColumn(max_chars=50, width="medium"), "Account Name": st.column_config.TextColumn(max_chars=255, width="large"), "Triumph Code": st.column_config.TextColumn(max_chars=50, width="medium"), "Outlet": st.column_config.TextColumn(max_chars=255, width="large"), "Created At": st.column_config.TextColumn(width="medium"), "Updated At": st.column_config.TextColumn(width="medium"), }, key="mapping_editor_v2", ) if st.session_state.get("mapping_editor_v2"): edited_rows = st.session_state.mapping_editor_v2.get("edited_rows", {}) deleted_rows = st.session_state.mapping_editor_v2.get("deleted_rows", []) if edited_rows or deleted_rows: changes_made = False for idx, patch in edited_rows.items(): mapping_id = df.iloc[idx]["ID"] row = df.iloc[idx] new_code = patch.get("POS Code", row["POS Code"]) new_name = patch.get("Account Name", row["Account Name"]) new_triumph = patch.get("Triumph Code", row["Triumph Code"]) new_outlet = patch.get("Outlet", row["Outlet"]) if service.update_mapping(mapping_id, new_code, new_name, new_triumph, new_outlet): changes_made = True for idx in deleted_rows: if service.delete_mapping(df.iloc[idx]["ID"]): changes_made = True if changes_made: st.toast("βœ… Mappings updated and synced!", icon="πŸš€") st.rerun() # ── TAB 2: Add New ──────────────────────────────────────────────────────── with tab2: st.markdown("### βž• Create New Mapping") st.caption("All fields are mandatory.") with st.form("new_mapping_form", clear_on_submit=True): c1, c2 = st.columns(2) with c1: new_code = st.text_input("POS Code", placeholder="e.g. 0273", help="Unique identifier from your POS system.") new_name = st.text_input("Account Sale Name", placeholder="e.g. Suriya", help="The name as it appears on account invoices.") with c2: new_triumph = st.text_input("Triumph Debtor Code (DBMACC#)", placeholder="e.g. SURI0273", help="The debtor code in Triumph ERP.") new_outlet = st.selectbox( "Store / Outlet", options=["Select a Store"] + store_labels, index=0, help="Select the store this mapping belongs to.", ) st.markdown("
", unsafe_allow_html=True) if st.form_submit_button("Create Mapping", type="primary", use_container_width=True): if not all([new_code.strip(), new_name.strip(), new_triumph.strip()]) or new_outlet == "β€” Select a Store β€”": st.error("⚠️ All fields are required β€” including selecting a store.") else: service.create_mapping(new_code.strip(), new_name.strip(), new_triumph.strip(), new_outlet) st.success(f"βœ… Mapping for **{new_name}** created under **{new_outlet}**!") st.balloons() st.rerun() st.markdown("---") with st.expander("πŸ“– Field definitions"): st.write(""" - **POS Code** β€” Unique identifier from your POS system. - **Account Name** β€” Name used on account sales invoices. - **Triumph Code (DBMACC#)** β€” Corresponding debtor code in Triumph ERP. - **Store / Outlet** β€” Store this mapping is assigned to. *Any change here is immediately picked up by the background event processor.* """) if __name__ == "__main__": render_page()