Spaces:
Running
Running
Add kpi to Inventory tab and the card at the top of the dashboard
Browse files- 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 |
-
"
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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("
|
| 1205 |
-
|
| 1206 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|