taotanapol commited on
Commit
a641f02
·
verified ·
1 Parent(s): 6a3cce2

Add kpi to Inventory tab and the card at the top of the dashboard

Browse files
Files changed (1) hide show
  1. streamlit_app.py +84 -22
streamlit_app.py CHANGED
@@ -63,6 +63,10 @@ LANG = {
63
  "all_dates": "all dates",
64
  "n_restaurants": "{n} restaurant(s)",
65
  "n_branches": "{n} branch(es)",
 
 
 
 
66
  # Overview tab
67
  "ov_monthly_revenue_trend": "Monthly Revenue Trend",
68
  "ov_channel_mix": "Channel Mix",
@@ -120,7 +124,8 @@ LANG = {
120
  "inv_month_picker": "Month",
121
  "inv_sort_by": "Sort by",
122
  "inv_snapshot": "Inventory snapshot ({ym})",
123
- "inv_closing_by_branch": "Closing inventory value by branch",
 
124
  # Sign-in screen
125
  "auth_title": "Copper Group Dashboard",
126
  "auth_intro": "Restricted to members of the <code>CB-Group</code> organization on Hugging Face. "
@@ -171,6 +176,10 @@ LANG = {
171
  "all_dates": "ทุกวันที่",
172
  "n_restaurants": "{n} ร้าน",
173
  "n_branches": "{n} สาขา",
 
 
 
 
174
  # Overview tab
175
  "ov_monthly_revenue_trend": "แนวโน้มรายได้รายเดือน",
176
  "ov_channel_mix": "สัดส่วนช่องทาง",
@@ -228,7 +237,8 @@ LANG = {
228
  "inv_month_picker": "เดือน",
229
  "inv_sort_by": "เรียงตาม",
230
  "inv_snapshot": "ภาพรวมสินค้าคงคลัง ({ym})",
231
- "inv_closing_by_branch": "มูลค่าลือตาสาขา",
 
232
  # Sign-in screen
233
  "auth_title": "แดชบอร์ดคอปเปอร์กรุ๊ป",
234
  "auth_intro": "เฉพาะสมาชิกขององค์กร <code>CB-Group</code> บน Hugging Face เท่านั้น "
@@ -1200,10 +1210,46 @@ total_iqty = filtered_daily["ItemQty"].sum() if "ItemQty" in filtered_daily
1200
  total_irev = filtered_daily["ItemRevenue"].sum() if "ItemRevenue" in filtered_daily.columns else 0
1201
  rev_phead = (total_rev / total_cust) if total_cust else 0
1202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1203
  c1, c2, c3 = st.columns(3)
1204
- c1.metric("Total Revenue", fmt_money(total_rev))
1205
- c2.metric("Total Customers", fmt_num(total_cust))
1206
- c3.metric("Revenue / Head", fmt_money(rev_phead))
 
 
 
1207
 
1208
  st.divider()
1209
 
@@ -2305,6 +2351,39 @@ with tab_inv:
2305
  (inv_filt["Date"].dt.year == _y_sel)
2306
  & (inv_filt["Date"].dt.month == _m_sel)
2307
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2308
  ranked = latest_rows[latest_rows[sort_by] > 0].sort_values(sort_by, ascending=False).head(100)
2309
 
2310
  cols_to_show = [c for c in
@@ -2328,21 +2407,4 @@ with tab_inv:
2328
  use_container_width=True, hide_index=True,
2329
  )
2330
 
2331
- # Per-branch inventory totals
2332
- st.subheader(t("inv_closing_by_branch"))
2333
- totals = (
2334
- latest_rows.groupby(["Restaurant", "Branch"], as_index=False)["Value_Closing"]
2335
- .sum()
2336
- .sort_values("Value_Closing", ascending=True)
2337
- )
2338
- if not totals.empty:
2339
- totals["Label"] = totals["Restaurant"] + " / " + totals["Branch"]
2340
- fig = px.bar(
2341
- totals, x="Value_Closing", y="Label", orientation="h",
2342
- color="Restaurant", color_discrete_map=RESTAURANT_COLOR,
2343
- text=totals["Value_Closing"].apply(fmt_money),
2344
- )
2345
- fig.update_layout(xaxis_title="Closing Value (THB)", yaxis_title=None, showlegend=False)
2346
- fig.update_xaxes(tickformat=",.0f")
2347
- st.plotly_chart(style_plotly(fig, height=380), use_container_width=True)
2348
 
 
63
  "all_dates": "all dates",
64
  "n_restaurants": "{n} restaurant(s)",
65
  "n_branches": "{n} branch(es)",
66
+ "kpi_total_revenue": "Total Revenue",
67
+ "kpi_total_customers": "Total Customers",
68
+ "kpi_rev_per_head": "Revenue / Head",
69
+ "kpi_yoy_suffix": "YoY",
70
  # Overview tab
71
  "ov_monthly_revenue_trend": "Monthly Revenue Trend",
72
  "ov_channel_mix": "Channel Mix",
 
124
  "inv_month_picker": "Month",
125
  "inv_sort_by": "Sort by",
126
  "inv_snapshot": "Inventory snapshot ({ym})",
127
+ "inv_kpi_value_used": "Total Value Used",
128
+ "inv_kpi_value_per_cust": "Value Used / Customer",
129
  # Sign-in screen
130
  "auth_title": "Copper Group Dashboard",
131
  "auth_intro": "Restricted to members of the <code>CB-Group</code> organization on Hugging Face. "
 
176
  "all_dates": "ทุกวันที่",
177
  "n_restaurants": "{n} ร้าน",
178
  "n_branches": "{n} สาขา",
179
+ "kpi_total_revenue": "รายได้รวม",
180
+ "kpi_total_customers": "ลูกค้ารวม",
181
+ "kpi_rev_per_head": "รายได้ต่อหัว",
182
+ "kpi_yoy_suffix": "YoY",
183
  # Overview tab
184
  "ov_monthly_revenue_trend": "แนวโน้มรายได้รายเดือน",
185
  "ov_channel_mix": "สัดส่วนช่องทาง",
 
237
  "inv_month_picker": "เดือน",
238
  "inv_sort_by": "เรียงตาม",
239
  "inv_snapshot": "ภาพรวมสินค้าคงคลัง ({ym})",
240
+ "inv_kpi_value_used": "มูลค่าที่ใช้ทั้งหม",
241
+ "inv_kpi_value_per_cust": "มูลค่าที่ใช้ต่อลูกค้า",
242
  # Sign-in screen
243
  "auth_title": "แดชบอร์ดคอปเปอร์กรุ๊ป",
244
  "auth_intro": "เฉพาะสมาชิกขององค์กร <code>CB-Group</code> บน Hugging Face เท่านั้น "
 
1210
  total_irev = filtered_daily["ItemRevenue"].sum() if "ItemRevenue" in filtered_daily.columns else 0
1211
  rev_phead = (total_rev / total_cust) if total_cust else 0
1212
 
1213
+ # ── Year-on-year comparison ────────────────────────────────────────────
1214
+ # Pull the same date window one calendar year earlier from kpi_daily,
1215
+ # apply the same Restaurant / Branch sidebar filters, and compute the
1216
+ # same three totals. The percent delta is what we show under each tile.
1217
+ # DateOffset(years=1) handles the Feb-29 → Feb-28 edge case for us.
1218
+ yoy_rev = yoy_cust = yoy_rph = None
1219
+ if date_from is not None and date_to is not None and not kpi_daily.empty:
1220
+ try:
1221
+ _yoy_from = (pd.Timestamp(date_from) - pd.DateOffset(years=1))
1222
+ _yoy_to = (pd.Timestamp(date_to) - pd.DateOffset(years=1))
1223
+ _y = kpi_daily.copy()
1224
+ _y["Date"] = pd.to_datetime(_y["Date"], errors="coerce")
1225
+ _y = _y[(_y["Date"] >= _yoy_from) & (_y["Date"] <= _yoy_to)]
1226
+ if sel_restaurants and "Restaurant" in _y.columns:
1227
+ _y = _y[_y["Restaurant"].isin(sel_restaurants)]
1228
+ if sel_branches and "Branch" in _y.columns:
1229
+ _y = _y[_y["Branch"].isin(sel_branches)]
1230
+ if not _y.empty:
1231
+ yoy_rev = float(_y["Revenue"].sum()) if "Revenue" in _y.columns else None
1232
+ yoy_cust = float(_y["Customers"].sum()) if "Customers" in _y.columns else None
1233
+ yoy_rph = (yoy_rev / yoy_cust) if (yoy_rev is not None and yoy_cust) else None
1234
+ except Exception:
1235
+ pass
1236
+
1237
+ def _yoy_delta(current, previous) -> "str | None":
1238
+ """% change vs prior year, formatted with a sign + 'YoY' suffix.
1239
+ Returns None when there's no comparable prior-year value so the
1240
+ delta indicator is hidden instead of misleading."""
1241
+ if previous is None or previous == 0 or pd.isna(previous):
1242
+ return None
1243
+ pct = (current - previous) / previous * 100
1244
+ return f"{pct:+.1f}% {t('kpi_yoy_suffix')}"
1245
+
1246
  c1, c2, c3 = st.columns(3)
1247
+ c1.metric(t("kpi_total_revenue"), fmt_money(total_rev),
1248
+ delta=_yoy_delta(total_rev, yoy_rev))
1249
+ c2.metric(t("kpi_total_customers"), fmt_num(total_cust),
1250
+ delta=_yoy_delta(total_cust, yoy_cust))
1251
+ c3.metric(t("kpi_rev_per_head"), fmt_money(rev_phead),
1252
+ delta=_yoy_delta(rev_phead, yoy_rph))
1253
 
1254
  st.divider()
1255
 
 
2351
  (inv_filt["Date"].dt.year == _y_sel)
2352
  & (inv_filt["Date"].dt.month == _m_sel)
2353
  ]
2354
+
2355
+ # ── KPI tiles — total Value_Used in the month + per-customer rate
2356
+ # Customers for the same month come from kpi_daily, filtered by
2357
+ # the same sidebar Restaurant / Branch filters so the ratio is
2358
+ # consistent with whichever scope the user is viewing.
2359
+ _value_used_total = (
2360
+ float(latest_rows["Value_Used"].sum())
2361
+ if "Value_Used" in latest_rows.columns else 0.0
2362
+ )
2363
+ _cust_in_month = kpi_daily.copy() if not kpi_daily.empty else pd.DataFrame()
2364
+ if not _cust_in_month.empty and "Date" in _cust_in_month.columns:
2365
+ _cust_in_month["Date"] = pd.to_datetime(_cust_in_month["Date"], errors="coerce")
2366
+ _cust_in_month = _cust_in_month[
2367
+ (_cust_in_month["Date"].dt.year == _y_sel)
2368
+ & (_cust_in_month["Date"].dt.month == _m_sel)
2369
+ ]
2370
+ if sel_restaurants and "Restaurant" in _cust_in_month.columns:
2371
+ _cust_in_month = _cust_in_month[_cust_in_month["Restaurant"].isin(sel_restaurants)]
2372
+ if sel_branches and "Branch" in _cust_in_month.columns:
2373
+ _cust_in_month = _cust_in_month[_cust_in_month["Branch"].isin(sel_branches)]
2374
+ _cust_total = (
2375
+ float(_cust_in_month["Customers"].sum())
2376
+ if "Customers" in _cust_in_month.columns and not _cust_in_month.empty else 0.0
2377
+ )
2378
+ _value_per_cust = (_value_used_total / _cust_total) if _cust_total > 0 else None
2379
+
2380
+ ik1, ik2 = st.columns(2)
2381
+ ik1.metric(t("inv_kpi_value_used"), fmt_money(_value_used_total))
2382
+ ik2.metric(
2383
+ t("inv_kpi_value_per_cust"),
2384
+ fmt_money(_value_per_cust) if _value_per_cust is not None else "—",
2385
+ )
2386
+
2387
  ranked = latest_rows[latest_rows[sort_by] > 0].sort_values(sort_by, ascending=False).head(100)
2388
 
2389
  cols_to_show = [c for c in
 
2407
  use_container_width=True, hide_index=True,
2408
  )
2409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2410