Files
co_app/app/pages/costobjects.py
2026-03-01 13:11:28 +01:00

186 lines
5.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import streamlit as st
import pandas as pd
#from data.scriptloader import get_sql
from data.db import load_data
from auth_runtime import require_login
#from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out
#from auth import get_fullname_for_user
import numpy as np
from tools.excel_export import df_to_excel_bytes
from tools.help_text import get_help
st.set_page_config(page_title="Co-App Home", page_icon="🏠", layout="wide")
authenticator = require_login()
st.session_state["authenticator"] = authenticator
st.markdown("""
<style>
.block-container {
padding-top: 0.2rem;
}
</style>
""", unsafe_allow_html=True)
description = get_help("costobjects")
with st.expander(label=" Hilfe / Hinweise", expanded=False): # ❓
st.markdown(description)
@st.cache_data
def cache_data() -> pd.DataFrame:
"""
Load and cache the base dataset for this page.
Streamlit reruns the script on every interaction; caching avoids
repeated I/O and makes filtering feel instant.
"""
try:
df = load_data("ora_kostenobjekte", "oracle")
return df
except:
st.warning("Fehler beim Laden der Daten", icon="⚠️")
def sidebar_filters(df) -> dict:
"""
Render the sidebar UI and return the current filter selections.
This function should contain UI concerns only (widgets, layout),
and not data filtering logic, to keep the code maintainable.
"""
st.markdown("""
<style>
section[data-testid="stSidebar"] .block-container {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
section[data-testid="stSidebar"] div[data-testid="stVerticalBlock"] > div {
gap: 0.2rem;
}
</style>
""", unsafe_allow_html=True)
st.sidebar.header("Filter")
if st.sidebar.button("Refresh (Global)"):
cache_data.clear()
st.rerun()
filter_text = st.sidebar.text_input(label="Textsuche", placeholder="Suche Objekt, Text, Verantwortlicher")
col_s1, col_s2 = st.sidebar.columns(2)
with col_s1:
year = st.selectbox(label="Jahr", options=(2025, 2026), index=1)
with col_s2:
typ = st.selectbox(label="Typ", options=sorted(df["typ"].dropna().unique()), index=1)
obj = st.sidebar.multiselect("obj", sorted(df["obj"].dropna().unique()))
zgrp1 = st.sidebar.multiselect("ZGrp1", sorted(df["zgrp1"].dropna().unique()))
zgrp2 = st.sidebar.multiselect("ZGrp2", sorted(df["zgrp2"].dropna().unique()))
zgrp3 = st.sidebar.multiselect("ZGrp3", sorted(df["zgrp3"].dropna().unique()))
return {"year": year, "filter_text": filter_text, "typ": typ, "obj": obj, "zgrp1": zgrp1, "zgrp2": zgrp2, "zgrp3": zgrp3}
def build_mask(df: pd.DataFrame, sidebar_filter: dict) -> np.ndarray:
"""
Build a boolean mask based on filter selections.
The mask approach keeps the logic readable and makes it easy
to add more conditions later.
"""
mask = np.ones(len(df), dtype=bool)
filter_text = (sidebar_filter.get("filter_text") or "").strip()
if filter_text:
m1 = df["bezeichnung"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
m2 = df["verantwortlicher"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
m3 = df["vorgesetzter"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
mask &= (m1 | m2 | m3)
if sidebar_filter["year"]:
mask &= df["jahr"].eq(sidebar_filter["year"])
if sidebar_filter["typ"]:
mask &= df["typ"].eq(sidebar_filter["typ"])
if sidebar_filter["obj"]:
mask &= df["obj"].isin(sidebar_filter["obj"])
if sidebar_filter["zgrp1"]:
mask &= df["zgrp1"].isin(sidebar_filter["zgrp1"])
if sidebar_filter["zgrp2"]:
mask &= df["zgrp2"].isin(sidebar_filter["zgrp2"])
if sidebar_filter["zgrp3"]:
mask &= df["zgrp3"].isin(sidebar_filter["zgrp3"])
return mask
def render_table(df):
"""
Render the result table.
Keep this function presentation-only: it should not modify data.
"""
st.markdown("### Übersicht Kostenobjekte")
st.dataframe(df, hide_index=True, width="stretch", height="stretch")
def download_data(df):
df.to_excel(path)
def page():
"""
Page entry point: orchestrates data loading, UI, filtering, and rendering.
"""
# -----------------------------
# Data loading (cached)
# -----------------------------
df = cache_data()
# -----------------------------
# UI (Sidebar)
# -----------------------------
sidebar_filter = sidebar_filters(df)
# -----------------------------
# Business logic (filtering)
# -----------------------------
mask = build_mask(df, sidebar_filter)
# -----------------------------
# Presentation
# -----------------------------
df_clean = df[df.columns[:-1]] # letzte Spalten entfernen (sysdate)
df_display = df_clean.loc[mask]
df_display = df_display.sort_values(by="obj", key=lambda s: pd.to_numeric(s, errors="coerce"))
render_table(df_display)
col1, col2 = st.columns(2)
data_as_of = df["sysdate"].max()
row_count = len(df_display)
with col1:
st.text(f"Datenstand: {data_as_of}")
with col2:
st.markdown(f"<div style='text-align: right;'>Anzahl Zeilen: {row_count}</div>", unsafe_allow_html=True)
st.download_button(
"📊 Excel-Datei herunterladen",
data=df_to_excel_bytes(df_display),
file_name="kostenobjekte",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
if __name__ == "__main__":
df = page()