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()