add
This commit is contained in:
189
pages/mappings.py
Normal file
189
pages/mappings.py
Normal file
@@ -0,0 +1,189 @@
|
||||
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("""
|
||||
<style>
|
||||
.stApp { font-size: 1.05rem; }
|
||||
[data-testid="stDataEditor"] { font-size: 1.05rem !important; }
|
||||
h2 { font-weight: 700 !important; letter-spacing: -0.02em !important; }
|
||||
h3 { font-weight: 600 !important; color: #6366f1 !important; margin-top: 1.2rem !important; }
|
||||
.store-pill {
|
||||
display: inline-block;
|
||||
padding: 4px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin: 3px 4px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
""", 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("<br>", 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()
|
||||
Reference in New Issue
Block a user