import streamlit as st import pandas as pd from sqlalchemy import text from app_core.db.database import engine from app_core.ui.layout import render_store_selector @st.cache_data(ttl=300) # Cache for 5 minutes def _load_tenant_data(tenant_id: int, limit: int = 10000): """Load data for a specific tenant with caching.""" with engine.connect() as conn: df = pd.read_sql( text('SELECT * FROM "tenantpostings" WHERE "tenant_id" = :t ORDER BY "id" DESC LIMIT :limit'), conn, params={"t": tenant_id, "limit": limit}, ) return df def _detect_status_column(df: pd.DataFrame) -> str | None: candidates = ["status", "state", "result", "triumph_status"] lower_map = {c.lower(): c for c in df.columns} for key in candidates: if key in lower_map: return lower_map[key] for c in df.columns: if "status" in c.lower(): return c return None def _normalize_name(name: str) -> str: return "".join(ch for ch in name.lower() if ch.isalnum()) def _build_display_map(df: pd.DataFrame) -> dict[str, str]: overrides = { "triumph_status": "Status", "triumph_event": "Event", "outlet_name": "Outlet Name", "tenant_id": "Tenant ID", "processing_type": "Processing Type", "total_amount": "Total Amount", "created_at": "Date", "updated_at": "Updated At", "id": "SNo", } display_map: dict[str, str] = {} used: set[str] = set() for col in df.columns: key = col.lower() if key in overrides: label = overrides[key] else: label = col.replace("_", " ").title() base = label suffix = 2 while label in used: label = f"{base} {suffix}" suffix += 1 used.add(label) display_map[col] = label return display_map def _format_status_with_emoji(styler: "pd.io.formats.style.Styler", df: pd.DataFrame, status_col: str | None) -> "pd.io.formats.style.Styler": if status_col is None or status_col not in df.columns: return styler def fmt(val): v = str(val) v_lower = v.lower() if any(k in v_lower for k in ["success", "ok", "completed", "done", "active"]): return f"✅ {v}" if any(k in v_lower for k in ["fail", "error", "dead", "invalid"]): return f"❌ {v}" if any(k in v_lower for k in ["pending", "queue", "waiting", "processing"]): return f"⏳ {v}" return v return styler.format({status_col: fmt}) def _badge_status_cells(styler: "pd.io.formats.style.Styler", df: pd.DataFrame, status_col: str | None) -> "pd.io.formats.style.Styler": if status_col is None or status_col not in df.columns: return styler def badge(val): v = str(val).lower() bg = "#E2E8F0"; color = "#0F172A" if any(k in v for k in ["success", "ok", "completed", "done", "active"]): bg = "#E6F7EE"; color = "#166534" elif any(k in v for k in ["fail", "error", "dead", "invalid"]): bg = "#FDECEC"; color = "#991B1B" elif any(k in v for k in ["pending", "queue", "waiting", "processing"]): bg = "#FEF5E6"; color = "#92400E" return f"background-color: {bg}; color:{color}; border-radius: 999px; padding: 4px 8px;" return styler.map(badge, subset=pd.IndexSlice[:, [status_col]]) def _zebra_style(df: pd.DataFrame) -> "pd.io.formats.style.Styler": df2 = df.reset_index(drop=True) def zebra(row: pd.Series): return ["background-color: rgba(2,6,23,0.03);" if (row.name % 2 == 0) else ""] * len(row) styler = df2.style.apply(zebra, axis=1) styler = styler.set_table_styles([ {"selector": "th", "props": "position: sticky; top: 0; background: #F0F6FF; color:#0F172A; font-weight:700;"}, {"selector": "tbody td", "props": "border-top: 1px solid rgba(15,23,42,0.06);"}, {"selector": "table", "props": "border-collapse: separate; border-spacing: 0;"}, ]) styler = styler.hide(axis="index") return styler def _format_two_decimals_for_amounts(styler: "pd.io.formats.style.Styler", df: pd.DataFrame) -> "pd.io.formats.style.Styler": candidates_norm = {"totalamount", "total_amount", "amount", "totalamounts", "totalamounttotals"} targets = [] for c in df.columns: if _normalize_name(c) in candidates_norm and pd.api.types.is_numeric_dtype(df[c]): targets.append(c) if targets: styler = styler.format(formatter="{:.2f}", subset=pd.IndexSlice[:, targets]) return styler def _format_date_columns(df: pd.DataFrame) -> pd.DataFrame: """Format date columns to show only date part""" df_formatted = df.copy() for col in df_formatted.columns: if 'created_at' in col.lower() or 'date' in col.lower(): if pd.api.types.is_datetime64_any_dtype(df_formatted[col]): df_formatted[col] = df_formatted[col].dt.date else: # Try to convert to datetime first try: df_formatted[col] = pd.to_datetime(df_formatted[col]).dt.date except: pass return df_formatted def _pick_existing_columns(df: pd.DataFrame, names: list[str]) -> list[str]: lower_map = {c.lower(): c for c in df.columns} picked = [] for n in names: if n.lower() in lower_map: picked.append(lower_map[n.lower()]) return picked def _stat_card(title: str, value: int | str, color: str, emoji: str) -> str: return f"""