diff --git a/app/data/sql/ora_kostenobjekte.sql b/app/data/sql/ora_kostenobjekte.sql index 671d81a..0625efb 100644 --- a/app/data/sql/ora_kostenobjekte.sql +++ b/app/data/sql/ora_kostenobjekte.sql @@ -6,7 +6,7 @@ select KOSTENOBJEKT as obj, BEZEICHNUNG, VERANTWORTLICHER, - FELD_2_X30 as VORGESETZER, + FELD_2_X30 as vorgesetzter, ZUORDNUNGSGRUPPE_1 as zgrp1, ZUORDNUNGSGRUPPE_2 as zgrp2, ZUORDNUNGSGRUPPE_3 as zgrp3, @@ -14,6 +14,9 @@ select ZUORDNUNGSGRUPPE_5 as zgrp5, ZUORDNUNGSGRUPPE_6 as zgrp6, FELD_1_X30 as fertigung, - OBJEKTGRUPPE as objgrp + OBJEKTGRUPPE as objgrp, + sysdate from pkos +where + objekttyp in ('01', '02') diff --git a/app/pages/costobjects.py b/app/pages/costobjects.py index b239f01..6c8f40a 100644 --- a/app/pages/costobjects.py +++ b/app/pages/costobjects.py @@ -5,41 +5,149 @@ 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 -# hide_sidebar_if_logged_out() st.set_page_config(page_title="Co-App Home", page_icon="🏠", layout="wide") authenticator = require_login() st.session_state["authenticator"] = authenticator + + @st.cache_data -def cache_data(): - return load_data("ora_kostenobjekte","oracle") +def cache_data() -> pd.DataFrame: + """ + Load and cache the base dataset for this page. -def render_report(): - df = cache_data() + 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.sidebar.header("Filter") - zgrp1_options = st.multiselect( - "Zuordnungsgruppe1", - ["SPI", "KULA", "AT"] - ) + if st.sidebar.button("Refresh (Global)"): + cache_data.clear() + st.rerun() + + year = st.sidebar.selectbox(label="Jahr", options=(2025, 2026), index=1) + filter_text = st.sidebar.text_input(label="Textsuche", placeholder="Suche Objekt, Text, Verantwortlicher") + col_s1, col_s2 = st.sidebar.columns(2) + with col_s1: + typ = st.sidebar.selectbox(label="Typ", options=sorted(df["typ"].dropna().unique()), index=1) + with col_s2: + 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} - year = 2026 - df_actual_year = df[df["jahr"] == year] - df_actual_year = df_actual_year.sort_values( - by="obj", - key=lambda s: pd.to_numeric(s, errors="coerce") - ) - if zgrp1_options: - df_view = df_actual_year[df_actual_year["zgrp1"].isin(zgrp1_options)] - else: - df_view = df_actual_year - st.dataframe(df_view, height=600, hide_index=True, width="stretch") +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.dataframe(df, hide_index=True, width="stretch", height="stretch") + + +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 = df.sort_values(by="obj", key=lambda s: pd.to_numeric(s, errors="coerce")) + df_clean = df[df.columns[:-1]] # letzte Spalten entfernen (sysdate) + df_display = df_clean.loc[mask] + 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}") + st.text("Datenquelle: Oracle (PENTA)") + with col2: + st.markdown(f"
Anzahl Zeilen: {row_count}
", unsafe_allow_html=True) + + + if __name__ == "__main__": - df = render_report() \ No newline at end of file + df = page() \ No newline at end of file