From 3c0e58dfc1e834825d366ecea10c335a16a416f0 Mon Sep 17 00:00:00 2001 From: handi Date: Thu, 4 Dec 2025 22:08:15 +0100 Subject: [PATCH] Add sidebar builder --- app/auth.py | 2 +- app/db.py | 5 + app/pages/{Benutzer.py => user.py} | 2 +- app/ui/sidebar.py | 121 +++++++++++++++++- data/app.db | Bin 69632 -> 69632 bytes ...0251204_210700_add_col_page_dashboards.sql | 11 ++ 6 files changed, 138 insertions(+), 3 deletions(-) rename app/pages/{Benutzer.py => user.py} (98%) create mode 100644 migrations/20251204_210700_add_col_page_dashboards.sql diff --git a/app/auth.py b/app/auth.py index de09f6b..7a82f02 100644 --- a/app/auth.py +++ b/app/auth.py @@ -67,7 +67,7 @@ def verify_user(username: str, password: str): def get_role_for_user(username: str) -> str: with closing(get_conn()) as conn: row = conn.execute( - "SELECT role_id FROM users WHERE username = ?", + "SELECT r.role_text from users u left join roles r on u.role_id = r.role_id WHERE username = ?", (username,), ).fetchone() return row[0] if row else "user" diff --git a/app/db.py b/app/db.py index 5c7b5c2..abadd2f 100644 --- a/app/db.py +++ b/app/db.py @@ -1,6 +1,7 @@ import sqlite3 from pathlib import Path import bcrypt +import pandas as pd BASE_DIR = Path(__file__).resolve().parent.parent DB_PATH = BASE_DIR / "data" / "app.db" @@ -10,3 +11,7 @@ def get_conn(): # check_same_thread=False, damit Streamlit mehrere Threads nutzen kann return sqlite3.connect(DB_PATH, check_same_thread=False) +def get_list(sql, params=None): + conn = get_conn() + df = pd.read_sql_query(sql, conn, params=params) + return df \ No newline at end of file diff --git a/app/pages/Benutzer.py b/app/pages/user.py similarity index 98% rename from app/pages/Benutzer.py rename to app/pages/user.py index 450f099..104caa5 100644 --- a/app/pages/Benutzer.py +++ b/app/pages/user.py @@ -11,7 +11,7 @@ authenticator = require_login() st.session_state["authenticator"] = authenticator username = st.session_state.get("username") -build_sidebar() +# build_sidebar() st.title("Benutzerverwaltung") diff --git a/app/ui/sidebar.py b/app/ui/sidebar.py index 5ad6f03..92b09fa 100644 --- a/app/ui/sidebar.py +++ b/app/ui/sidebar.py @@ -1,5 +1,9 @@ +from contextlib import closing import streamlit as st from auth import get_fullname_for_user, get_role_for_user +from db import get_conn, get_list +import sqlite3 + def build_sidebar(): @@ -15,6 +19,53 @@ def build_sidebar(): role = get_role_for_user(username) fullname = get_fullname_for_user(username) + if role == "admin": + sql = """ + select + g.group_id, + g.group_text, + d.dash_id, + d.dash_text, + d.page_file, + d.dash_type + from + groups g + left join dashboards d + on g.group_id = d.group_id + where + g.active = 1 + and d.active = 1 + """ + else: + sql = """ + SELECT + d.group_id, + g.group_text, + p.dash_id, + d.dash_text, + d.page_file, + d.dash_type + FROM + users u + left join permissions p + on u.role_id = p.role_id + left join dashboards d + on p.dash_id = d.dash_id + left join groups g + on d.group_id = g.group_id + where + u.active = 1 + and g.active = 1 + and d.active = 1 + and p.active = 1 + and u.username = ? + order by + g.order_no, + d.order_no + """ + params = (username,) if "?" in sql else None + df = get_list(sql, params) + with st.sidebar: st.logo("app/images/GMN_Logo_neu_rgb.png", size="small") st.write(f"**{fullname}** ({role})") @@ -30,11 +81,79 @@ def build_sidebar(): st.divider() st.markdown("## Menü") - st.page_link("pages/Benutzer.py", label="Benutzer anlegen") + # for group_text, df_group in df.groupby("group_text"): + # with st.expander(group_text, expanded=False): + # for _, row in df_group.iterrows(): + # dash_type = row.get("dash_type") + # page_file = row.get("page_file") + # label = row.get("dash_text", "") + # print(dash_type, page_file, label) + # # 1) echte Streamlit-Page + # if dash_type == "page" and isinstance(page_file, str) and page_file.strip(): + # st.page_link( + # page_file, # z.B. "pages/umsatz.py" + # label=label, + # ) + + # # 2) externer Link (oder interner HTTP-Link) + # elif dash_type == "url" and isinstance(page_file, str) and page_file.strip(): + # st.markdown( + # f"[{label}]({page_file})", + # unsafe_allow_html=False, + # ) + + # # 3) Platzhalter / noch nicht implementiert + # else: + # st.write(f"▫️ {label} (in Vorbereitung)") + # --- Suchfeld --- + query = st.text_input("Menü-Suche", "", placeholder="z.B. Umsatz, Kosten, User ...") + query = query.strip() + # Aktive Seite ermitteln (für Expander-Status / Highlight) + current_page = st.session_state.get("_page_path") + # --- DF filtern, falls Suchbegriff gesetzt --- + if query: + mask = ( + df["dash_text"].str.contains(query, case=False, na=False) + | df["group_text"].str.contains(query, case=False, na=False) + ) + df_view = df[mask].copy() + else: + df_view = df + + if df_view.empty: + st.info("Keine Einträge zum Suchbegriff gefunden.") + return + + # --- Gruppiert durchlaufen --- + for group_text, df_group in df_view.groupby("group_text"): + # Expander offen, wenn: + # - aktuelle Seite in dieser Gruppe liegt + group_open = any( + (row["page_file"] == current_page) + for _, row in df_group.iterrows() + ) + + with st.expander(group_text, expanded=(group_open or bool(query))): + for _, row in df_group.iterrows(): + dash_type = row.get("dash_type") + page_file = row.get("page_file") + label = row.get("dash_text", "") + + # Streamlit-Page + if dash_type == "page" and isinstance(page_file, str) and page_file.strip(): + st.page_link(page_file, label=label) + + # Externer Link + elif dash_type == "url" and isinstance(page_file, str) and page_file.strip(): + st.markdown(f"[{label}]({page_file})") + + # Platzhalter / Sonstiges + else: + st.write(f"▫️ {label}") # Damit die leere Sidebar komplett verschwindet nach dem Logout def hide_sidebar_if_logged_out(): diff --git a/data/app.db b/data/app.db index 5382b6ff3c1865231507c8397fe197200dcc39c8..f5f86da8cf33356ddde8fd922676e7accfad4f3c 100644 GIT binary patch delta 862 zcma))O=uHA6vtq$-L9R0cAq>jMcvFPGT-(wg|JgZ+M=vTSP@I*zg@YYGsMW5 z92TbvMa-Ceg_vf)>YUutL4DGmv|G^+5Z52WG|}UtM*(_^?xD-57k-Ab@HFh8U(t8q zBYM=BW{@5^PLmT6ffELKfs2X)&kb>$n2{tgTP})CH`C%;IipAw6ASY~n2Rz37mIK) z?nrDGnT}{z*k1jK|DvVG1zCVTqL=6{x`EE3{qQGjz^AYV$04f+7zjg$$qC{=?Pz`6 zNV2^_>*$^CIr$K2>!P3N(BF&wjJprm;0k~Zw1E~;60R7)0(76~fJ7G{sEF~u0L6Q? z?z*opuc~q-HZU+VGsEVtmskl`m12T`M6gW~2KlVs+x1Tp&4QrjuJ>sl77vXZW%u~D z({?A6sx0G@Ktkeow9~sCZ%vF3M>vKbjztG!(P6S$!HV&KV-`sG+m)#PPOVt??=uHp i888V}o->8hY{U&7*q3CAFpj3CbkEhV$Yv!8O85ombm*`E delta 565 zcmYjNO=uHg5dFUFZZ@0j&LkIq8e=yQTUMH|$u=OtgR~bx>_sXiv`wX1lZb5Mx{=a@ z6oOC>nuZVRO$(wI10UUiYFBVf*=(2&>nkG+%&B?@R-NEH_T&}*2JYXaU~v` zB80^DK>dE8Gh(HuNaog#7Mvp}R0+oMrBH<}?Vfr~bHpxicRD+K zMxKyG*d!tT1SaV~L|Z2K0q@{6reG5mU>t(lTWuL0^Q3-BlL`@G(qglj6n~mH7Z`{@ zk>GcHg{yc4Pvao`fi66Q78H5RJPEv*;YsN;zQYY3JM{?JMkzrB{-rdmNApC2^#DG^ zSy+c}sQQKn0EVE_IH8K=KblZ>x1C@U?c?HbP^HXh9|OEejKll7!lv8t^F~0WiGQQ; znC&#ZMcd8g%v?U5&896=&z>lZ<_o5UpGd;kwH_U_U3