Spaces:
Running
Running
Updated Summary Tab
Browse files- streamlit_app.py +103 -74
streamlit_app.py
CHANGED
|
@@ -846,20 +846,18 @@ total_iqty = filtered_daily["ItemQty"].sum() if "ItemQty" in filtered_daily
|
|
| 846 |
total_irev = filtered_daily["ItemRevenue"].sum() if "ItemRevenue" in filtered_daily.columns else 0
|
| 847 |
rev_phead = (total_rev / total_cust) if total_cust else 0
|
| 848 |
|
| 849 |
-
c1, c2, c3
|
| 850 |
-
c1.metric("Total Revenue",
|
| 851 |
-
c2.metric("
|
| 852 |
-
c3.metric("
|
| 853 |
-
c4.metric("Revenue / Head", fmt_money(rev_phead))
|
| 854 |
-
c5.metric("Items Sold (Food)", fmt_num(total_iqty))
|
| 855 |
|
| 856 |
st.divider()
|
| 857 |
|
| 858 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 859 |
# Tabs
|
| 860 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 861 |
-
tab_overview,
|
| 862 |
-
["Overview", "
|
| 863 |
)
|
| 864 |
|
| 865 |
# ββ Overview βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -944,63 +942,112 @@ with tab_overview:
|
|
| 944 |
fig.update_yaxes(tickformat=",.0f")
|
| 945 |
st.plotly_chart(style_plotly(fig, height=320), use_container_width=True)
|
| 946 |
|
| 947 |
-
# ββ
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
)
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
st.subheader("Monthly Summary")
|
| 984 |
-
mf = apply_filters(kpi_monthly, use_date=False)
|
| 985 |
-
if not mf.empty:
|
| 986 |
-
mf = mf.sort_values(
|
| 987 |
-
["Year", "Month", "Restaurant", "Branch"],
|
| 988 |
-
ascending=[False, False, True, True],
|
| 989 |
-
)
|
| 990 |
-
display_cols = [c for c in
|
| 991 |
-
["Year", "Month", "Restaurant", "Branch",
|
| 992 |
-
"Revenue", "ItemRevenue", "Customers", "Rev_Per_Head"]
|
| 993 |
-
if c in mf.columns]
|
| 994 |
st.dataframe(
|
| 995 |
-
|
| 996 |
use_container_width=True, hide_index=True,
|
| 997 |
column_config={
|
|
|
|
|
|
|
| 998 |
"Revenue": st.column_config.NumberColumn(format="ΰΈΏ%.0f"),
|
| 999 |
-
"ItemRevenue": st.column_config.NumberColumn("Item Revenue", format="ΰΈΏ%.0f"),
|
| 1000 |
"Customers": st.column_config.NumberColumn(format="%.0f"),
|
| 1001 |
"Rev_Per_Head": st.column_config.NumberColumn("Rev / Head", format="ΰΈΏ%.0f"),
|
|
|
|
| 1002 |
},
|
| 1003 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
|
| 1005 |
# ββ P&L βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1006 |
with tab_pl:
|
|
@@ -1108,21 +1155,3 @@ with tab_inv:
|
|
| 1108 |
fig.update_xaxes(tickformat=",.0f")
|
| 1109 |
st.plotly_chart(style_plotly(fig, height=380), use_container_width=True)
|
| 1110 |
|
| 1111 |
-
# ββ Raw tables (browser) ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1112 |
-
with tab_data:
|
| 1113 |
-
st.markdown("Quick browser for every sheet in the workbook.")
|
| 1114 |
-
sheet_choice = st.selectbox("Sheet", list(sheets.keys()))
|
| 1115 |
-
df = sheets[sheet_choice]
|
| 1116 |
-
apply_to_table = st.checkbox(
|
| 1117 |
-
"Apply current sidebar filters", value=True,
|
| 1118 |
-
help="When checked, the table respects the Restaurant / Branch / date filters above.",
|
| 1119 |
-
)
|
| 1120 |
-
if apply_to_table:
|
| 1121 |
-
df = apply_filters(df)
|
| 1122 |
-
st.caption(f"{len(df):,} rows Γ {len(df.columns)} columns")
|
| 1123 |
-
# Keep the table snappy by capping the rendered length
|
| 1124 |
-
cap = 5000
|
| 1125 |
-
if len(df) > cap:
|
| 1126 |
-
st.caption(f"Showing first {cap:,} rows (use filters to narrow the view).")
|
| 1127 |
-
df = df.head(cap)
|
| 1128 |
-
st.dataframe(df, use_container_width=True, hide_index=True)
|
|
|
|
| 846 |
total_irev = filtered_daily["ItemRevenue"].sum() if "ItemRevenue" in filtered_daily.columns else 0
|
| 847 |
rev_phead = (total_rev / total_cust) if total_cust else 0
|
| 848 |
|
| 849 |
+
c1, c2, c3 = st.columns(3)
|
| 850 |
+
c1.metric("Total Revenue", fmt_money(total_rev))
|
| 851 |
+
c2.metric("Total Customers", fmt_num(total_cust))
|
| 852 |
+
c3.metric("Revenue / Head", fmt_money(rev_phead))
|
|
|
|
|
|
|
| 853 |
|
| 854 |
st.divider()
|
| 855 |
|
| 856 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 857 |
# Tabs
|
| 858 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 859 |
+
tab_overview, tab_summary, tab_pl, tab_inv = st.tabs(
|
| 860 |
+
["Overview", "Summary", "P&L", "Inventory"]
|
| 861 |
)
|
| 862 |
|
| 863 |
# ββ Overview βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 942 |
fig.update_yaxes(tickformat=",.0f")
|
| 943 |
st.plotly_chart(style_plotly(fig, height=320), use_container_width=True)
|
| 944 |
|
| 945 |
+
# ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 946 |
+
# Restaurant-by-restaurant summary tables, mirroring the layout of the
|
| 947 |
+
# original Summary.xlsx (Copper Buffet) and Summary_Tiew.xlsx (Tiew Copper).
|
| 948 |
+
# Restaurant filter is intentionally ignored here so both restaurants are
|
| 949 |
+
# always shown; Branch / date / year filters still apply.
|
| 950 |
+
with tab_summary:
|
| 951 |
+
def _summary_filter(df: pd.DataFrame, *, use_date: bool = True) -> pd.DataFrame:
|
| 952 |
+
"""Like apply_filters() but skips the Restaurant filter."""
|
| 953 |
+
if df.empty:
|
| 954 |
+
return df
|
| 955 |
+
out = df
|
| 956 |
+
if "Branch" in out.columns and sel_branches:
|
| 957 |
+
out = out[out["Branch"].isin(sel_branches)]
|
| 958 |
+
if use_date and "Date" in out.columns:
|
| 959 |
+
if date_from is not None:
|
| 960 |
+
out = out[out["Date"] >= pd.to_datetime(date_from)]
|
| 961 |
+
if date_to is not None:
|
| 962 |
+
out = out[out["Date"] <= pd.to_datetime(date_to)]
|
| 963 |
+
if sel_year != "All years":
|
| 964 |
+
if "Year" in out.columns:
|
| 965 |
+
out = out[out["Year"] == sel_year]
|
| 966 |
+
elif "Date" in out.columns:
|
| 967 |
+
out = out[out["Date"].dt.year == sel_year]
|
| 968 |
+
return out
|
| 969 |
+
|
| 970 |
+
def _render_restaurant_summary(restaurant_name: str) -> None:
|
| 971 |
+
st.subheader(restaurant_name)
|
| 972 |
+
|
| 973 |
+
monthly = _summary_filter(kpi_monthly, use_date=False)
|
| 974 |
+
monthly = monthly[monthly.get("Restaurant", "") == restaurant_name] \
|
| 975 |
+
if "Restaurant" in monthly.columns else monthly.iloc[0:0]
|
| 976 |
+
daily = _summary_filter(kpi_daily)
|
| 977 |
+
daily = daily[daily.get("Restaurant", "") == restaurant_name] \
|
| 978 |
+
if "Restaurant" in daily.columns else daily.iloc[0:0]
|
| 979 |
+
|
| 980 |
+
if monthly.empty and daily.empty:
|
| 981 |
+
st.info(f"No data for {restaurant_name} in the current filters.")
|
| 982 |
+
return
|
| 983 |
+
|
| 984 |
+
# ββ Top-line totals for this restaurant ββββββββββββββββββββββββββ
|
| 985 |
+
tot_rev = daily["Revenue"].sum() if "Revenue" in daily.columns else 0
|
| 986 |
+
tot_cust = daily["Customers"].sum() if "Customers" in daily.columns else 0
|
| 987 |
+
tot_iqty = daily["ItemQty"].sum() if "ItemQty" in daily.columns else 0
|
| 988 |
+
tot_irev = daily["ItemRevenue"].sum() if "ItemRevenue" in daily.columns else 0
|
| 989 |
+
rev_ph = (tot_rev / tot_cust) if tot_cust else 0
|
| 990 |
+
|
| 991 |
+
kc1, kc2, kc3, kc4 = st.columns(4)
|
| 992 |
+
kc1.metric("Revenue", fmt_money(tot_rev))
|
| 993 |
+
kc2.metric("Customers", fmt_num(tot_cust))
|
| 994 |
+
kc3.metric("Rev / Head", fmt_money(rev_ph))
|
| 995 |
+
kc4.metric("Item Revenue (Grand Total)", fmt_money(tot_irev))
|
| 996 |
+
|
| 997 |
+
# ββ Monthly summary table ββββββββββββββββββββββββββββββββββββββββ
|
| 998 |
+
st.markdown("**Monthly summary**")
|
| 999 |
+
if not monthly.empty:
|
| 1000 |
+
m = monthly.copy().sort_values(
|
| 1001 |
+
["Year", "Month", "Branch"], ascending=[False, False, True]
|
| 1002 |
)
|
| 1003 |
+
cols = [c for c in
|
| 1004 |
+
["Year", "Month", "Branch", "Revenue", "Customers",
|
| 1005 |
+
"Rev_Per_Head", "ItemRevenue"]
|
| 1006 |
+
if c in m.columns]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1007 |
st.dataframe(
|
| 1008 |
+
m[cols],
|
| 1009 |
use_container_width=True, hide_index=True,
|
| 1010 |
column_config={
|
| 1011 |
+
"Year": st.column_config.NumberColumn(format="%d"),
|
| 1012 |
+
"Month": st.column_config.NumberColumn(format="%d"),
|
| 1013 |
"Revenue": st.column_config.NumberColumn(format="ΰΈΏ%.0f"),
|
|
|
|
| 1014 |
"Customers": st.column_config.NumberColumn(format="%.0f"),
|
| 1015 |
"Rev_Per_Head": st.column_config.NumberColumn("Rev / Head", format="ΰΈΏ%.0f"),
|
| 1016 |
+
"ItemRevenue": st.column_config.NumberColumn("Item Revenue", format="ΰΈΏ%.0f"),
|
| 1017 |
},
|
| 1018 |
)
|
| 1019 |
+
else:
|
| 1020 |
+
st.caption("No monthly rows in this filter window.")
|
| 1021 |
+
|
| 1022 |
+
# ββ Daily detail (collapsed by default β can be long) ββββββββββββ
|
| 1023 |
+
with st.expander("Daily detail", expanded=False):
|
| 1024 |
+
if not daily.empty:
|
| 1025 |
+
d = daily.copy().sort_values(["Date", "Branch"], ascending=[False, True])
|
| 1026 |
+
# Compute Rev/Head on the fly so daily rows have the same column.
|
| 1027 |
+
if "Revenue" in d.columns and "Customers" in d.columns:
|
| 1028 |
+
d["Rev_Per_Head"] = d["Revenue"] / d["Customers"].replace(0, np.nan)
|
| 1029 |
+
cols = [c for c in
|
| 1030 |
+
["Date", "Branch", "DayType", "Revenue", "Customers",
|
| 1031 |
+
"Rev_Per_Head", "ItemRevenue", "ItemQty"]
|
| 1032 |
+
if c in d.columns]
|
| 1033 |
+
st.dataframe(
|
| 1034 |
+
d[cols],
|
| 1035 |
+
use_container_width=True, hide_index=True,
|
| 1036 |
+
column_config={
|
| 1037 |
+
"Date": st.column_config.DateColumn(format="YYYY-MM-DD"),
|
| 1038 |
+
"Revenue": st.column_config.NumberColumn(format="ΰΈΏ%.0f"),
|
| 1039 |
+
"Customers": st.column_config.NumberColumn(format="%.0f"),
|
| 1040 |
+
"Rev_Per_Head": st.column_config.NumberColumn("Rev / Head", format="ΰΈΏ%.0f"),
|
| 1041 |
+
"ItemRevenue": st.column_config.NumberColumn("Item Revenue", format="ΰΈΏ%.0f"),
|
| 1042 |
+
"ItemQty": st.column_config.NumberColumn("Items Sold", format="%.0f"),
|
| 1043 |
+
},
|
| 1044 |
+
)
|
| 1045 |
+
else:
|
| 1046 |
+
st.caption("No daily rows in this filter window.")
|
| 1047 |
+
|
| 1048 |
+
_render_restaurant_summary("Copper Buffet")
|
| 1049 |
+
st.divider()
|
| 1050 |
+
_render_restaurant_summary("Tiew Copper")
|
| 1051 |
|
| 1052 |
# ββ P&L βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1053 |
with tab_pl:
|
|
|
|
| 1155 |
fig.update_xaxes(tickformat=",.0f")
|
| 1156 |
st.plotly_chart(style_plotly(fig, height=380), use_container_width=True)
|
| 1157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|