Files
Tuckerfresh-site/pages/mappings.py
2026-04-07 12:44:06 +05:30

190 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()