Spaces:
Running
Running
| """ | |
| pages/data_entry.py | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SPJIMR Waste Analytics β Daily Data Entry Form | |
| Replaces the Excel upload workflow entirely. | |
| Staff enter wet & dry waste kg per block directly in this page. | |
| Data is persisted in SQLite (data/waste_log.db). | |
| After saving, the shared session state key "waste_all_df" is | |
| refreshed so the Analytics and Gamification pages immediately | |
| reflect the new data β no re-upload needed. | |
| Add to app.py: | |
| from pages.data_entry import render_data_entry | |
| # in your navigation block: | |
| render_data_entry() | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import logging | |
| from datetime import date, timedelta | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import streamlit as st | |
| from core.waste_db import WasteDB | |
| from core.waste_parser import LOCATIONS, LOCATION_LABELS, LOCATION_GROUPS | |
| logger = logging.getLogger(__name__) | |
| # ββ Session state key shared across all pages ββββββββββββββββββββββββββββββββββ | |
| _ALL_DF_KEY = "waste_all_df" | |
| _DB_KEY = "waste_db_instance" | |
| # ββ Block display order β group them visually ββββββββββββββββββββββββββββββββββ | |
| DISPLAY_ORDER = [ | |
| # Academic | |
| "A&B Block", "C&D Block", | |
| # Hostels | |
| "L H Hostel", "Hostel no -25", "Hostel no -26", "Hostel no -27", | |
| "Hostel no -28", "Hostel no -29", "Hostel no -30", | |
| # Dining | |
| "cantean", "MESS", | |
| ] | |
| GROUP_ICONS = {"Academic": "π«", "Hostels": "π ", "Dining": "π½οΈ"} | |
| def _loc_group(loc: str) -> str: | |
| return next((g for g, locs in LOCATION_GROUPS.items() if loc in locs), "Other") | |
| # ββ CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _CSS = """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;700&family=Space+Grotesk:wght@400;600;700;800&display=swap'); | |
| [data-testid="stAppViewContainer"] { background: #0B1622; } | |
| [data-testid="stSidebar"] { background: #07111D; } | |
| h1,h2,h3,h4 { font-family:'Space Grotesk',sans-serif !important; } | |
| p,div,span,label,input { font-family:'DM Sans',sans-serif !important; } | |
| /* ββ Block input card ββ */ | |
| .block-card { | |
| background: linear-gradient(145deg,#111E2E,#172840); | |
| border: 1px solid rgba(0,201,167,0.15); | |
| border-radius: 14px; | |
| padding: 16px 18px 14px; | |
| margin-bottom: 12px; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| } | |
| .block-card:hover { | |
| border-color: rgba(0,201,167,0.4); | |
| box-shadow: 0 4px 20px rgba(0,201,167,0.08); | |
| } | |
| .block-card.filled { | |
| border-color: rgba(0,201,167,0.35); | |
| background: linear-gradient(145deg,#0E2220,#14352E); | |
| } | |
| .block-card.warning { | |
| border-color: rgba(245,166,35,0.4); | |
| background: linear-gradient(145deg,#1E1800,#2A2000); | |
| } | |
| .block-title { | |
| font-family:'Space Grotesk',sans-serif; | |
| font-size: 0.95rem; font-weight: 700; | |
| color: #E8F4F8; margin-bottom: 2px; | |
| } | |
| .block-group { | |
| font-size: 0.72rem; color: #7A9BB5; | |
| text-transform: uppercase; letter-spacing: 0.08em; | |
| margin-bottom: 10px; | |
| } | |
| .block-total { | |
| font-family:'Space Grotesk',sans-serif; | |
| font-size: 1.05rem; font-weight: 700; color: #00C9A7; | |
| text-align: right; margin-top: 6px; | |
| } | |
| /* ββ Section headers ββ */ | |
| .section-hdr { | |
| font-family:'Space Grotesk',sans-serif; | |
| font-size: 1.1rem; font-weight: 700; color: #E8F4F8; | |
| border-left: 3px solid #00C9A7; padding-left: 12px; | |
| margin: 28px 0 14px; | |
| } | |
| .group-hdr { | |
| font-family:'Space Grotesk',sans-serif; | |
| font-size: 0.82rem; font-weight: 600; color: #7A9BB5; | |
| text-transform: uppercase; letter-spacing: 0.12em; | |
| margin: 20px 0 8px; padding-bottom: 4px; | |
| border-bottom: 1px solid rgba(255,255,255,0.06); | |
| } | |
| /* ββ Summary bar ββ */ | |
| .summary-bar { | |
| background: linear-gradient(135deg,#0D2B3E,#112840); | |
| border: 1px solid rgba(0,201,167,0.25); | |
| border-radius: 12px; padding: 16px 24px; | |
| display: flex; justify-content: space-between; | |
| align-items: center; margin-bottom: 20px; | |
| } | |
| .summary-val { | |
| font-family:'Space Grotesk',sans-serif; | |
| font-size: 1.6rem; font-weight: 800; color: #00C9A7; | |
| } | |
| .summary-lbl { font-size: 0.76rem; color: #7A9BB5; text-transform: uppercase; letter-spacing: 0.1em; } | |
| /* ββ Status chips ββ */ | |
| .chip { | |
| display: inline-block; padding: 3px 10px; border-radius: 20px; | |
| font-size: 0.74rem; font-weight: 600; margin: 2px; | |
| } | |
| .chip-ok { background: rgba(0,201,167,0.15); color: #00C9A7; border: 1px solid rgba(0,201,167,0.3); } | |
| .chip-warn { background: rgba(245,166,35,0.15); color: #F5A623; border: 1px solid rgba(245,166,35,0.3); } | |
| .chip-err { background: rgba(255,107,107,0.15); color: #FF6B6B; border: 1px solid rgba(255,107,107,0.3); } | |
| /* ββ Calendar completion dots ββ */ | |
| .cal-cell { | |
| display: inline-block; width: 28px; height: 28px; | |
| border-radius: 6px; line-height: 28px; text-align: center; | |
| font-size: 0.72rem; font-weight: 600; margin: 2px; | |
| } | |
| .cal-full { background: #00C9A7; color: #0B1622; } | |
| .cal-partial { background: #F5A623; color: #0B1622; } | |
| .cal-empty { background: rgba(255,255,255,0.06); color: #7A9BB5; } | |
| /* ββ History table rows ββ */ | |
| .hist-row { | |
| display: flex; align-items: center; justify-content: space-between; | |
| background: rgba(17,30,46,0.9); border: 1px solid rgba(0,201,167,0.1); | |
| border-radius: 10px; padding: 12px 18px; margin-bottom: 8px; | |
| } | |
| .hist-date { font-family:'Space Grotesk',sans-serif; font-size:0.9rem; font-weight:700; color:#E8F4F8; width:110px; } | |
| .hist-stat { font-size:0.82rem; color:#7A9BB5; } | |
| .hist-total { font-family:'Space Grotesk',sans-serif; font-size:1rem; font-weight:700; color:#00C9A7; width:100px; text-align:right; } | |
| </style> | |
| """ | |
| CHART_LAYOUT = dict( | |
| paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", | |
| font=dict(family="DM Sans", color="#E8F4F8", size=12), | |
| margin=dict(l=10, r=10, t=36, b=10), | |
| legend=dict(bgcolor="rgba(17,30,46,0.8)", bordercolor="rgba(0,201,167,0.2)", borderwidth=1), | |
| xaxis=dict(gridcolor="rgba(255,255,255,0.05)", linecolor="rgba(255,255,255,0.08)"), | |
| yaxis=dict(gridcolor="rgba(255,255,255,0.05)", linecolor="rgba(255,255,255,0.08)"), | |
| ) | |
| # ββ DB singleton via session state βββββββββββββββββββββββββββββββββββββββββββββ | |
| def _get_db() -> WasteDB: | |
| if _DB_KEY not in st.session_state: | |
| st.session_state[_DB_KEY] = WasteDB() | |
| return st.session_state[_DB_KEY] | |
| def _refresh_session_df(db: WasteDB) -> None: | |
| """Rebuild the shared DataFrame so analytics/gamification pages update instantly.""" | |
| df = db.to_dataframe() | |
| st.session_state[_ALL_DF_KEY] = df if not df.empty else None | |
| # ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _group_header(group: str) -> str: | |
| icon = GROUP_ICONS.get(group, "π¦") | |
| return f'<div class="group-hdr">{icon} {group}</div>' | |
| def _completion_chip(pct: float) -> str: | |
| if pct == 100: | |
| return '<span class="chip chip-ok">β Complete</span>' | |
| elif pct > 0: | |
| return f'<span class="chip chip-warn">β‘ {pct:.0f}%</span>' | |
| return '<span class="chip chip-err">β No data</span>' | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 1 β Daily Entry Form | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _render_entry_form(db: WasteDB) -> None: | |
| st.markdown('<div class="section-hdr">π Select Date</div>', unsafe_allow_html=True) | |
| col_date, col_nav = st.columns([2, 3]) | |
| with col_date: | |
| selected_date = st.date_input( | |
| "Entry Date", | |
| value=date.today(), | |
| max_value=date.today(), | |
| key="entry_date_picker", | |
| label_visibility="collapsed", | |
| ) | |
| with col_nav: | |
| recorded = db.all_dates() | |
| if recorded: | |
| b1, b2, b3 = st.columns(3) | |
| with b1: | |
| if st.button("β¬ Previous day", use_container_width=True, key="nav_prev"): | |
| idx = recorded.index(selected_date) if selected_date in recorded else -1 | |
| if idx > 0: | |
| st.session_state["entry_date_picker"] = recorded[idx - 1] | |
| st.rerun() | |
| with b2: | |
| if st.button("Today", use_container_width=True, key="nav_today"): | |
| st.session_state["entry_date_picker"] = date.today() | |
| st.rerun() | |
| with b3: | |
| if st.button("Next day β‘", use_container_width=True, key="nav_next"): | |
| idx = recorded.index(selected_date) if selected_date in recorded else -1 | |
| if idx >= 0 and idx < len(recorded) - 1: | |
| st.session_state["entry_date_picker"] = recorded[idx + 1] | |
| st.rerun() | |
| # Load existing values for this date | |
| existing = db.get_day(selected_date) | |
| comp = db.date_completion(selected_date) | |
| # Status summary bar | |
| status_chip = _completion_chip(comp["pct"]) | |
| st.markdown( | |
| f"""<div class="summary-bar"> | |
| <div> | |
| <div class="summary-lbl">Selected Date</div> | |
| <div class="summary-val">{selected_date.strftime("%d %b %Y")}</div> | |
| </div> | |
| <div style="text-align:center"> | |
| <div class="summary-lbl">Blocks filled</div> | |
| <div class="summary-val">{comp["filled"]} / {comp["total"]}</div> | |
| </div> | |
| <div style="text-align:right"> | |
| {status_chip} | |
| </div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown('<div class="section-hdr">βοΈ Enter Waste Data (kg)</div>', unsafe_allow_html=True) | |
| st.caption("Enter 0 for blocks with no data collected that day.") | |
| # ββ Build form inputs grouped by category βββββββββββββββββββββββββββββββββ | |
| form_values: dict[str, dict[str, float]] = {} | |
| prev_group = None | |
| # Use a single st.form so all 11 blocks save atomically | |
| with st.form(key=f"waste_entry_{selected_date}", clear_on_submit=False): | |
| for loc in DISPLAY_ORDER: | |
| group = _loc_group(loc) | |
| label = LOCATION_LABELS.get(loc, loc) | |
| ex = existing.get(loc, {"wet": 0.0, "dry": 0.0}) | |
| # Group separator header | |
| if group != prev_group: | |
| st.markdown(_group_header(group), unsafe_allow_html=True) | |
| prev_group = group | |
| # Card header with location name | |
| is_filled = ex["wet"] > 0 or ex["dry"] > 0 | |
| card_class = "block-card filled" if is_filled else "block-card" | |
| last_edit = f"Last saved: {ex['updated_at'][:16].replace('T',' ')}" if ex.get("updated_at") else "" | |
| st.markdown( | |
| f"""<div class="{card_class}"> | |
| <div class="block-title">{label}</div> | |
| <div class="block-group">{group} Β· {last_edit}</div> | |
| </div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| wet = st.number_input( | |
| f"π’ Wet waste (kg) β {label}", | |
| min_value=0.0, max_value=5000.0, | |
| value=float(ex["wet"]), | |
| step=0.5, format="%.1f", | |
| key=f"wet_{loc}", | |
| label_visibility="visible", | |
| ) | |
| with c2: | |
| dry = st.number_input( | |
| f"π‘ Dry waste (kg) β {label}", | |
| min_value=0.0, max_value=5000.0, | |
| value=float(ex["dry"]), | |
| step=0.5, format="%.1f", | |
| key=f"dry_{loc}", | |
| label_visibility="visible", | |
| ) | |
| total_now = wet + dry | |
| if total_now > 0: | |
| st.markdown( | |
| f'<div class="block-total">Total: {total_now:.1f} kg</div>', | |
| unsafe_allow_html=True, | |
| ) | |
| form_values[loc] = {"wet": wet, "dry": dry} | |
| st.markdown("---") | |
| # Live preview inside form | |
| grand_wet = sum(v["wet"] for v in form_values.values()) | |
| grand_dry = sum(v["dry"] for v in form_values.values()) | |
| grand_total = grand_wet + grand_dry | |
| pcol1, pcol2, pcol3, pcol4 = st.columns(4) | |
| pcol1.metric("Total Wet", f"{grand_wet:.1f} kg") | |
| pcol2.metric("Total Dry", f"{grand_dry:.1f} kg") | |
| pcol3.metric("Grand Total", f"{grand_total:.1f} kg") | |
| pcol4.metric("Blocks with data", sum(1 for v in form_values.values() if v["wet"]+v["dry"] > 0)) | |
| submitted = st.form_submit_button( | |
| "πΎ Save Entry", | |
| use_container_width=True, | |
| type="primary", | |
| ) | |
| if submitted: | |
| with st.spinner("Savingβ¦"): | |
| db.upsert_day(selected_date, form_values) | |
| _refresh_session_df(db) | |
| filled = sum(1 for v in form_values.values() if v["wet"] + v["dry"] > 0) | |
| st.success(f"β Saved {filled} block(s) for **{selected_date.strftime('%d %b %Y')}**. " | |
| f"Analytics and Gamification pages are now updated.") | |
| st.rerun() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 2 β History & Edit | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _render_history(db: WasteDB) -> None: | |
| recorded = db.all_dates() | |
| if not recorded: | |
| st.info("No entries yet. Use the **Enter Data** tab to add your first record.") | |
| return | |
| st.markdown('<div class="section-hdr">π Recorded Days</div>', unsafe_allow_html=True) | |
| # Completion calendar | |
| df_all = db.to_dataframe() | |
| if not df_all.empty: | |
| months = sorted(df_all["month"].unique(), | |
| key=lambda m: pd.to_datetime("01 " + m, format="%d %b %Y")) | |
| sel_cal_month = st.selectbox("Month", months, index=len(months)-1, key="hist_cal_month") | |
| mdf = df_all[df_all["month"] == sel_cal_month] | |
| st.markdown("**Completeness calendar** (π’ full Β· π‘ partial Β· β¬ missing):") | |
| cal_dates = sorted(mdf["date"].dt.date.unique()) | |
| dt_comp = {d: db.date_completion(d) for d in cal_dates} | |
| cells_html = "" | |
| for d in cal_dates: | |
| c = dt_comp[d] | |
| if c["pct"] == 100: | |
| css = "cal-full" | |
| elif c["pct"] > 0: | |
| css = "cal-partial" | |
| else: | |
| css = "cal-empty" | |
| cells_html += f'<span class="cal-cell {css}" title="{d}: {c["filled"]}/{c["total"]}">{d.day}</span>' | |
| st.markdown(cells_html, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown('<div class="section-hdr">ποΈ All Entries</div>', unsafe_allow_html=True) | |
| # Search / filter | |
| fc1, fc2 = st.columns([2, 3]) | |
| with fc1: | |
| filter_loc = st.selectbox( | |
| "Filter by block", | |
| ["All blocks"] + [LOCATION_LABELS[l] for l in DISPLAY_ORDER], | |
| key="hist_filter_loc", | |
| ) | |
| with fc2: | |
| sort_by = st.radio( | |
| "Sort by", | |
| ["Date (newest first)", "Date (oldest first)", "Total waste (highest)", "Total waste (lowest)"], | |
| horizontal=True, key="hist_sort", | |
| ) | |
| if df_all.empty: | |
| st.info("No data recorded yet.") | |
| return | |
| disp = df_all.copy() | |
| if filter_loc != "All blocks": | |
| disp = disp[disp["label"] == filter_loc] | |
| sort_map = { | |
| "Date (newest first)": ("date", False), | |
| "Date (oldest first)": ("date", True), | |
| "Total waste (highest)": ("total_kg", False), | |
| "Total waste (lowest)": ("total_kg", True), | |
| } | |
| col, asc = sort_map[sort_by] | |
| disp = disp.sort_values(col, ascending=asc) | |
| # Render as styled rows grouped by date | |
| grouped = disp.groupby("date") | |
| for dt, grp in grouped: | |
| day_str = pd.Timestamp(dt).strftime("%d %b %Y (%A)") | |
| day_total = grp["total_kg"].sum() | |
| filled = (grp["total_kg"] > 0).sum() | |
| chip = _completion_chip(filled / len(LOCATIONS) * 100) | |
| with st.expander(f"π {day_str} β {day_total:,.1f} kg total {chip}", expanded=False): | |
| # Mini table | |
| t_df = grp[["label","group","wet_kg","dry_kg","total_kg"]].rename(columns={ | |
| "label":"Block","group":"Category", | |
| "wet_kg":"Wet (kg)","dry_kg":"Dry (kg)","total_kg":"Total (kg)" | |
| }) | |
| st.dataframe(t_df, use_container_width=True, hide_index=True) | |
| # Quick bar chart for this day | |
| fig = go.Figure() | |
| fig.add_bar(name="Wet", x=grp["label"], y=grp["wet_kg"], | |
| marker_color="#2E9E6B", text=grp["wet_kg"].round(1), textposition="inside") | |
| fig.add_bar(name="Dry", x=grp["label"], y=grp["dry_kg"], | |
| marker_color="#F5A623", text=grp["dry_kg"].round(1), textposition="inside") | |
| fig.update_layout(barmode="stack", height=260, | |
| title=f"Waste breakdown β {day_str}", **CHART_LAYOUT) | |
| fig.update_xaxes(tickangle=-30) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Delete this day | |
| if st.button(f"ποΈ Delete all entries for {pd.Timestamp(dt).strftime('%d %b %Y')}", | |
| key=f"del_day_{dt}", type="secondary"): | |
| deleted = db.delete_day(pd.Timestamp(dt).date()) | |
| _refresh_session_df(db) | |
| st.warning(f"Deleted {deleted} record(s) for {pd.Timestamp(dt).strftime('%d %b %Y')}.") | |
| st.rerun() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 3 β Database Health & Export | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _render_db_health(db: WasteDB) -> None: | |
| st.markdown('<div class="section-hdr">ποΈ Database Status</div>', unsafe_allow_html=True) | |
| row_count = db.row_count() | |
| all_dates = db.all_dates() | |
| months_df = db.monthly_completion() | |
| mc1, mc2, mc3 = st.columns(3) | |
| mc1.metric("Total Records", f"{row_count:,}") | |
| mc2.metric("Days Recorded", f"{len(all_dates)}") | |
| mc3.metric("Months Covered", f"{len(months_df)}" if not months_df.empty else "0") | |
| if not months_df.empty: | |
| st.markdown('<div class="section-hdr">π Monthly Completeness</div>', unsafe_allow_html=True) | |
| fig_comp = go.Figure(go.Bar( | |
| x=months_df["month"], | |
| y=months_df["completeness"], | |
| marker=dict( | |
| color=months_df["completeness"], | |
| colorscale=[[0,"#FF6B6B"],[0.5,"#F5A623"],[1.0,"#00C9A7"]], | |
| showscale=False, | |
| ), | |
| text=months_df["completeness"].apply(lambda x: f"{x:.0f}%"), | |
| textposition="outside", | |
| )) | |
| fig_comp.add_hline(y=100, line_dash="dot", line_color="rgba(0,201,167,0.4)", | |
| annotation_text="100% target") | |
| fig_comp.update_layout( | |
| title="Data Completeness by Month (%)", | |
| **CHART_LAYOUT, | |
| ) | |
| # yaxis=dict(range=[0, 115], title="%"), | |
| fig_comp.update_yaxes(range=[0, 115], title="%") | |
| st.plotly_chart(fig_comp, use_container_width=True) | |
| st.dataframe( | |
| months_df.rename(columns={ | |
| "month":"Month","days":"Days with data", | |
| "location_days":"Block-days recorded","completeness":"Completeness %" | |
| }), | |
| use_container_width=True, hide_index=True, | |
| ) | |
| st.markdown('<div class="section-hdr">π₯ Export Data</div>', unsafe_allow_html=True) | |
| ec1, ec2 = st.columns(2) | |
| with ec1: | |
| if st.button("π Refresh Analytics & Gamification", use_container_width=True, type="primary"): | |
| _refresh_session_df(db) | |
| st.success("β Analytics and Gamification pages updated with latest data.") | |
| with ec2: | |
| df_export = db.to_dataframe() | |
| if not df_export.empty: | |
| csv_bytes = df_export.to_csv(index=False).encode() | |
| st.download_button( | |
| label="β¬οΈ Download Full Dataset (CSV)", | |
| data=csv_bytes, | |
| file_name="spjimr_waste_data.csv", | |
| mime="text/csv", | |
| use_container_width=True, | |
| ) | |
| else: | |
| st.info("No data to export yet.") | |
| # Optional: import from Excel on this tab as a one-time migration | |
| st.markdown('<div class="section-hdr">π€ One-time Excel Import</div>', unsafe_allow_html=True) | |
| st.caption("Already have historical Excel files? Import them once β data is saved to the database and you won't need Excel files again.") | |
| uploaded_files = st.file_uploader( | |
| "Upload historical Excel files (one per month)", | |
| type=["xlsx"], accept_multiple_files=True, key="db_import_xlsx", | |
| ) | |
| if uploaded_files and st.button("β¬οΈ Import into Database", type="primary", key="do_import"): | |
| from core.waste_parser import parse_waste_excel | |
| total_imported = 0 | |
| for f in uploaded_files: | |
| try: | |
| df_xl = parse_waste_excel(io.BytesIO(f.read())) | |
| if df_xl.empty: | |
| st.warning(f"β οΈ No data found in {f.name}") | |
| continue | |
| for _, row in df_xl.iterrows(): | |
| db.upsert( | |
| row["date"].date(), | |
| row["location"], | |
| row["wet_kg"], | |
| row["dry_kg"], | |
| ) | |
| total_imported += len(df_xl) | |
| st.success(f"β Imported {len(df_xl)} records from **{f.name}**") | |
| except Exception as exc: | |
| st.error(f"β Failed to import {f.name}: {exc}") | |
| if total_imported: | |
| _refresh_session_df(db) | |
| st.success(f"π Total {total_imported} records imported. Analytics pages are now live.") | |
| st.rerun() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main entry point | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_data_entry() -> None: | |
| st.markdown(_CSS, unsafe_allow_html=True) | |
| st.markdown("## π Daily Waste Data Entry") | |
| st.markdown( | |
| "<p style='color:#7A9BB5;margin-top:-10px;'>" | |
| "Enter daily block-wise waste directly β no Excel file needed</p>", | |
| unsafe_allow_html=True, | |
| ) | |
| db = _get_db() | |
| # Auto-refresh shared DataFrame on page load so analytics stay in sync | |
| if st.session_state.get(_ALL_DF_KEY) is None and db.row_count() > 0: | |
| _refresh_session_df(db) | |
| # Quick status banner at the top | |
| row_count = db.row_count() | |
| all_dates = db.all_dates() | |
| if row_count > 0: | |
| last_date = max(all_dates).strftime("%d %b %Y") | |
| st.markdown( | |
| f'<div style="background:rgba(0,201,167,0.08);border:1px solid rgba(0,201,167,0.2);' | |
| f'border-radius:10px;padding:10px 18px;margin-bottom:16px;font-size:0.88rem;color:#7A9BB5;">' | |
| f'ποΈ Database contains <strong style="color:#00C9A7">{row_count:,} records</strong> across ' | |
| f'<strong style="color:#00C9A7">{len(all_dates)} days</strong>. ' | |
| f'Last entry: <strong style="color:#E8F4F8">{last_date}</strong></div>', | |
| unsafe_allow_html=True, | |
| ) | |
| tab1, tab2, tab3 = st.tabs(["βοΈ Enter Data", "π History & Edit", "ποΈ Database & Export"]) | |
| with tab1: | |
| _render_entry_form(db) | |
| with tab2: | |
| _render_history(db) | |
| with tab3: | |
| _render_db_health(db) | |