Spaces:
Running
Running
Add filter mechanic to Inventory tab
Browse files- streamlit_app.py +62 -28
streamlit_app.py
CHANGED
|
@@ -127,6 +127,8 @@ LANG = {
|
|
| 127 |
"inv_kpi_value_used": "Total Value Used",
|
| 128 |
"inv_kpi_value_per_cust": "Value Used / Customer",
|
| 129 |
"inv_chart_vpc_trend": "Value Used / Customer — monthly trend",
|
|
|
|
|
|
|
| 130 |
"inv_store_filter": "Store",
|
| 131 |
# Sign-in screen
|
| 132 |
"auth_title": "Copper Group Dashboard",
|
|
@@ -242,6 +244,8 @@ LANG = {
|
|
| 242 |
"inv_kpi_value_used": "มูลค่าที่ใช้ทั้งหมด",
|
| 243 |
"inv_kpi_value_per_cust": "มูลค่าที่ใช้ต่อลูกค้า",
|
| 244 |
"inv_chart_vpc_trend": "มูลค่าที่ใช้ต่อลูกค้า — แนวโน้มรายเดือน",
|
|
|
|
|
|
|
| 245 |
"inv_store_filter": "สโตร์",
|
| 246 |
# Sign-in screen
|
| 247 |
"auth_title": "แดชบอร์ดคอปเปอร์กรุ๊ป",
|
|
@@ -2405,14 +2409,62 @@ with tab_inv:
|
|
| 2405 |
fmt_money(_value_per_cust) if _value_per_cust is not None else "—",
|
| 2406 |
)
|
| 2407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2408 |
# ── Monthly trend: Value Used / Customer ─────────────────────
|
| 2409 |
-
#
|
| 2410 |
-
#
|
| 2411 |
-
#
|
| 2412 |
-
#
|
| 2413 |
_inv_for_trend = inv_filt.copy()
|
| 2414 |
_inv_for_trend["Date"] = pd.to_datetime(_inv_for_trend["Date"], errors="coerce")
|
| 2415 |
_inv_for_trend = _inv_for_trend.dropna(subset=["Date"])
|
|
|
|
|
|
|
|
|
|
| 2416 |
if not _inv_for_trend.empty and "Value_Used" in _inv_for_trend.columns:
|
| 2417 |
_inv_for_trend["YearMonth"] = _inv_for_trend["Date"].dt.to_period("M").astype(str)
|
| 2418 |
_inv_for_trend["Year"] = _inv_for_trend["Date"].dt.year
|
|
@@ -2471,34 +2523,16 @@ with tab_inv:
|
|
| 2471 |
textfont=dict(size=9),
|
| 2472 |
)
|
| 2473 |
fig.update_yaxes(tickformat=",.0f")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2474 |
fig.update_layout(
|
| 2475 |
-
title=
|
| 2476 |
xaxis_title=None, yaxis_title=None,
|
| 2477 |
showlegend=False,
|
| 2478 |
)
|
| 2479 |
st.plotly_chart(style_plotly(fig, height=320), use_container_width=True)
|
| 2480 |
|
| 2481 |
-
ranked = latest_rows[latest_rows[sort_by] > 0].sort_values(sort_by, ascending=False).head(100)
|
| 2482 |
-
|
| 2483 |
-
cols_to_show = [c for c in
|
| 2484 |
-
["Item", "Restaurant", "Branch", "Store Name",
|
| 2485 |
-
"Unit", "Qty_Closing", "Value_Closing",
|
| 2486 |
-
"Qty_Used", "Value_Used"]
|
| 2487 |
-
if c in ranked.columns]
|
| 2488 |
-
disp = ranked[cols_to_show].copy()
|
| 2489 |
-
for c in ("Qty_Closing", "Qty_Used"):
|
| 2490 |
-
if c in disp.columns:
|
| 2491 |
-
disp[c] = disp[c].map(fmt_qty)
|
| 2492 |
-
for c in ("Value_Closing", "Value_Used"):
|
| 2493 |
-
if c in disp.columns:
|
| 2494 |
-
disp[c] = disp[c].map(fmt_money)
|
| 2495 |
-
disp = disp.rename(columns={
|
| 2496 |
-
"Value_Closing": "Value Closing (THB)",
|
| 2497 |
-
"Value_Used": "Value Used (THB)",
|
| 2498 |
-
})
|
| 2499 |
-
st.dataframe(
|
| 2500 |
-
disp,
|
| 2501 |
-
use_container_width=True, hide_index=True,
|
| 2502 |
-
)
|
| 2503 |
-
|
| 2504 |
|
|
|
|
| 127 |
"inv_kpi_value_used": "Total Value Used",
|
| 128 |
"inv_kpi_value_per_cust": "Value Used / Customer",
|
| 129 |
"inv_chart_vpc_trend": "Value Used / Customer — monthly trend",
|
| 130 |
+
"inv_chart_vpc_item": "Value Used / Customer — {item} (monthly)",
|
| 131 |
+
"inv_table_hint": "Click any row to filter the chart below to that item. Click the same row again to clear.",
|
| 132 |
"inv_store_filter": "Store",
|
| 133 |
# Sign-in screen
|
| 134 |
"auth_title": "Copper Group Dashboard",
|
|
|
|
| 244 |
"inv_kpi_value_used": "มูลค่าที่ใช้ทั้งหมด",
|
| 245 |
"inv_kpi_value_per_cust": "มูลค่าที่ใช้ต่อลูกค้า",
|
| 246 |
"inv_chart_vpc_trend": "มูลค่าที่ใช้ต่อลูกค้า — แนวโน้มรายเดือน",
|
| 247 |
+
"inv_chart_vpc_item": "มูลค่าที่ใช้ต่อลูกค้า — {item} (รายเดือน)",
|
| 248 |
+
"inv_table_hint": "คลิกแถวใดก็ได้เพื่อกรองกราฟด้านล่างเฉพาะรายการนั้น คลิกแถวเดิมอีกครั้งเพื่อล้าง",
|
| 249 |
"inv_store_filter": "สโตร์",
|
| 250 |
# Sign-in screen
|
| 251 |
"auth_title": "แดชบอร์ดคอปเปอร์กรุ๊ป",
|
|
|
|
| 2409 |
fmt_money(_value_per_cust) if _value_per_cust is not None else "—",
|
| 2410 |
)
|
| 2411 |
|
| 2412 |
+
# ── Snapshot table (with row selection) ──────────────────────
|
| 2413 |
+
# Render the table FIRST. A single-row selection here drives the
|
| 2414 |
+
# chart below — clicking an item filters its monthly Value Used
|
| 2415 |
+
# / Customer trend; clicking again clears.
|
| 2416 |
+
ranked = latest_rows[latest_rows[sort_by] > 0].sort_values(sort_by, ascending=False).head(100)
|
| 2417 |
+
|
| 2418 |
+
cols_to_show = [c for c in
|
| 2419 |
+
["Item", "Restaurant", "Branch", "Store Name",
|
| 2420 |
+
"Unit", "Qty_Closing", "Value_Closing",
|
| 2421 |
+
"Qty_Used", "Value_Used"]
|
| 2422 |
+
if c in ranked.columns]
|
| 2423 |
+
disp = ranked[cols_to_show].copy()
|
| 2424 |
+
for c in ("Qty_Closing", "Qty_Used"):
|
| 2425 |
+
if c in disp.columns:
|
| 2426 |
+
disp[c] = disp[c].map(fmt_qty)
|
| 2427 |
+
for c in ("Value_Closing", "Value_Used"):
|
| 2428 |
+
if c in disp.columns:
|
| 2429 |
+
disp[c] = disp[c].map(fmt_money)
|
| 2430 |
+
disp = disp.rename(columns={
|
| 2431 |
+
"Value_Closing": "Value Closing (THB)",
|
| 2432 |
+
"Value_Used": "Value Used (THB)",
|
| 2433 |
+
})
|
| 2434 |
+
st.caption(t("inv_table_hint"))
|
| 2435 |
+
_table_event = st.dataframe(
|
| 2436 |
+
disp,
|
| 2437 |
+
use_container_width=True, hide_index=True,
|
| 2438 |
+
on_select="rerun",
|
| 2439 |
+
selection_mode="single-row",
|
| 2440 |
+
key="inv_table",
|
| 2441 |
+
)
|
| 2442 |
+
|
| 2443 |
+
# Translate the picked row index back to the underlying Item name.
|
| 2444 |
+
# The picker indexes into the displayed (formatted) DataFrame,
|
| 2445 |
+
# which has the same row order as `ranked`, so we can look it up
|
| 2446 |
+
# there to recover the original (unformatted) Item value.
|
| 2447 |
+
_selected_item: "str | None" = None
|
| 2448 |
+
try:
|
| 2449 |
+
_sel_rows = _table_event.selection.rows # list[int]
|
| 2450 |
+
if _sel_rows and "Item" in ranked.columns:
|
| 2451 |
+
_idx = _sel_rows[0]
|
| 2452 |
+
if 0 <= _idx < len(ranked):
|
| 2453 |
+
_selected_item = str(ranked.iloc[_idx]["Item"])
|
| 2454 |
+
except Exception:
|
| 2455 |
+
_selected_item = None
|
| 2456 |
+
|
| 2457 |
# ── Monthly trend: Value Used / Customer ─────────────────────
|
| 2458 |
+
# If a row was selected above, filter the numerator (Value_Used)
|
| 2459 |
+
# to that item only — the denominator (Customers) stays as the
|
| 2460 |
+
# period total because we're asking "for this item, how much
|
| 2461 |
+
# value per customer did we burn each month?".
|
| 2462 |
_inv_for_trend = inv_filt.copy()
|
| 2463 |
_inv_for_trend["Date"] = pd.to_datetime(_inv_for_trend["Date"], errors="coerce")
|
| 2464 |
_inv_for_trend = _inv_for_trend.dropna(subset=["Date"])
|
| 2465 |
+
if _selected_item is not None and "Item" in _inv_for_trend.columns:
|
| 2466 |
+
_inv_for_trend = _inv_for_trend[_inv_for_trend["Item"] == _selected_item]
|
| 2467 |
+
|
| 2468 |
if not _inv_for_trend.empty and "Value_Used" in _inv_for_trend.columns:
|
| 2469 |
_inv_for_trend["YearMonth"] = _inv_for_trend["Date"].dt.to_period("M").astype(str)
|
| 2470 |
_inv_for_trend["Year"] = _inv_for_trend["Date"].dt.year
|
|
|
|
| 2523 |
textfont=dict(size=9),
|
| 2524 |
)
|
| 2525 |
fig.update_yaxes(tickformat=",.0f")
|
| 2526 |
+
_chart_title = (
|
| 2527 |
+
t("inv_chart_vpc_item", item=_selected_item)
|
| 2528 |
+
if _selected_item is not None
|
| 2529 |
+
else t("inv_chart_vpc_trend")
|
| 2530 |
+
)
|
| 2531 |
fig.update_layout(
|
| 2532 |
+
title=_chart_title,
|
| 2533 |
xaxis_title=None, yaxis_title=None,
|
| 2534 |
showlegend=False,
|
| 2535 |
)
|
| 2536 |
st.plotly_chart(style_plotly(fig, height=320), use_container_width=True)
|
| 2537 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2538 |
|