diff --git a/.gitignore b/.gitignore index 21e6a91..bb96667 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # own -prototypes/ +# prototypes/ data/ reports/ *.code-workspace diff --git a/prototypes/recursive_menu.py b/prototypes/recursive_menu.py new file mode 100644 index 0000000..5e7958f --- /dev/null +++ b/prototypes/recursive_menu.py @@ -0,0 +1,49 @@ +from typing import Any + +from nicegui import ui + +nested_menu = { + "Notiz": None, + "Beratungsgespräch": None, + "Neu": {"Unternehmen": None, "Individualperson": None}, +} + + +def build_menu( + title: str, + structure: dict[str, Any], +): + def _recursive(structure: dict[str, Any]): + with ui.menu(): + for label, children in structure.items(): + if children: + with ui.menu_item(label, auto_close=False).props("icon=arrow_right"): + with ui.menu().props('anchor="top end" self="top start"'): + build_menu(children) # Rekursion für Untermenüs + else: + ui.menu_item(label, on_click=lambda: ui.notify(f"{label} geklickt")) + + with ui.button(title, icon="add"): + _recursive(structure) + + # ui.menu_item("Direkte Aktion", on_click=lambda: ui.notify("Aktion 1")) + + # ui.separator() + + # # Der Trick: @click.stop verhindert, dass das Hauptmenü den Klick bemerkt + # with ui.menu_item("Untermenü öffnen...", auto_close=False).props("icon=arrow_right"): + # with ui.item_section().props("side"): + # ui.icon("keyboard_arrow_right") + # with ui.menu().props('anchor="top end" self="top start"'): + # ui.menu_item("Unterpunkt A", on_click=lambda: ui.notify("A")) + # ui.menu_item("Unterpunkt B", on_click=lambda: ui.notify("B")) + + +# def build_menu(data): +# for label, children in data.items(): +# if children: +# with ui.menu_item(label): +# with ui.menu().props('anchor="top end" self="top start"'): +# build_menu(children) # Rekursion für Untermenüs +# else: +# ui.menu_item(label, on_click=lambda l=label: ui.notify(f"{l} geklickt")) diff --git a/prototypes/t_nice_gui.py b/prototypes/t_nice_gui.py new file mode 100644 index 0000000..977e183 --- /dev/null +++ b/prototypes/t_nice_gui.py @@ -0,0 +1,378 @@ +from typing import Any + +from nicegui import ui + + +def menu(): + with ui.column().classes("w-full items-center"): + ui.label("Mein Dashboard 2").classes("text-h4") + + +# Eine Hauptspalte für die gesamte Seite +# @ui.page("/") +# def main_page(): +# menu() +# # Erste Sektion: Zwei Karten nebeneinander +# with ui.row().classes("w-full justify-center"): +# with ui.card(): +# ui.label("Statistik A") +# ui.number(value=42) + +# with ui.card(): +# ui.label("Statistik B") +# ui.switch("Aktivieren") + +# # Zweite Sektion: Eine breite Spalte darunter +# with ui.card().classes("w-full items-left"): +# ui.label("Details") +# ui.markdown("Hier stehen weitere Informationen in einer eigenen Sektion.") + +# with ui.expansion("Erweiterte Einstellungen", icon="settings").classes("w-full"): +# ui.label("Hier kannst du tiefergehende Konfigurationen vornehmen.") +# # ui.input("API Schlüssel") +# ui.link("Gehe zur Einstellungs-Seite", "/settings") + + +# @ui.page("/settings") +# def settings_page(): +# menu() +# ui.label("Einstellungen").classes("text-h4") +# ui.button("Zurück", on_click=lambda: ui.navigate.to("/")) + +# Beispieldaten für die Kacheln +menu_items = [ + {"title": "Dashboard", "icon": "dashboard", "route": "/dashboard", "color": "#e3f2fd"}, + {"title": "Benutzer", "icon": "people", "route": "/users", "color": "#f1f8e9"}, + {"title": "Einstellungen", "icon": "settings", "route": "/settings", "color": "#fff3e0"}, + {"title": "Berichte", "icon": "assessment", "route": "/reports", "color": "#fce4ec"}, + {"title": "Hilfe", "icon": "help", "route": "/help", "color": "#f3e5f5"}, + {"title": "Abmelden", "icon": "logout", "route": "/logout", "color": "#efebe9"}, +] + + +# @ui.page("/") +# def main_page(): +# ui.label("Hauptmenü").classes("text-h4 mb-4") + +# # Erstellt ein Raster mit 3 Spalten (auf kleinen Bildschirmen 1, auf mittleren 2, auf großen 3) +# with ui.grid(columns="1fr 1fr 1fr").classes("w-full gap-4"): +# for item in menu_items: +# # Erstelle eine Karte als Kachel +# # 'cursor-pointer' macht den Mauszeiger zur Hand +# with ( +# ui.card() +# .tight() +# .classes("cursor-pointer hover:shadow-lg transition-shadow") +# .style(f"background-color: {item['color']}; height: 150px;") +# .on("click", lambda i=item: ui.navigate.to(i["route"])) +# ): +# with ui.column().classes("w-full h-full items-center justify-center p-4"): +# ui.icon(item["icon"]).classes("text-5xl mb-2") +# ui.label(item["title"]).classes("text-lg font-bold") + + +# # Dummy-Zielseiten +# @ui.page("/dashboard") +# def dashboard(): +# ui.label("Hier ist das Dashboard") +# ui.button("Zurück", on_click=lambda: ui.navigate.to("/")) + +# Beispieldaten: Eine Liste von Einträgen +# data = [ +# {"id": "001", "name": "Projekt Alpha", "status": "Aktiv", "date": "2023-10-01"}, +# {"id": "002", "name": "Kundenmeeting", "status": "Geplant", "date": "2023-10-05"}, +# {"id": "003", "name": "System-Update", "status": "Erledigt", "date": "2023-09-28"}, +# {"id": "004", "name": "Budget-Review", "status": "Aktiv", "date": "2023-10-12"}, +# ] + + +# @ui.page("/") +# def table_view(): +# with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-2"): +# # --- TABELLEN-KOPF --- +# # Wir nutzen ein Grid, um die Spaltenbreiten festzulegen (z.B. 10% ID, 50% Name, 20% Status, 20% Datum) +# with ui.row().classes( +# "w-full bg-slate-200 p-4 rounded-t-lg shadow-sm font-bold text-lg items-center" +# ): +# ui.label("ID").classes("w-12") +# ui.label("Bezeichnung").classes("flex-grow") +# ui.label("Status").classes("w-24") +# ui.label("Datum").classes("w-24 text-right") + +# # --- TABELLEN-EINTRÄGE --- +# for entry in data: +# # Jede Zeile ist eine klickbare Karte/Reihe +# with ( +# ui.card() +# .tight() +# .classes( +# "w-full hover:bg-blue-50 cursor-pointer transition-all hover:scale-[1.01]" +# ) +# .on("click", lambda e=entry: ui.notify(f"Gehe zu: {e['name']}")) +# ): +# with ui.row().classes("w-full p-4 items-center"): +# # Geringfügig kleinerer Text als im Header (text-base vs text-lg) +# ui.label(entry["id"]).classes("w-12 text-slate-500 font-mono") +# ui.label(entry["name"]).classes("flex-grow font-medium") + +# # Ein kleiner Chip für den Status +# ui.badge( +# entry["status"], +# color="green" if entry["status"] == "Aktiv" else "grey", +# ).classes("w-20") + +# ui.label(entry["date"]).classes("w-24 text-right text-sm text-slate-400") + + +# 1. Unsere Datenquelle (könnte später auch eine Datenbank sein) +data = [ + { + "UN/ NWP/ Kontaktperson": "Unternehmen 1", + "Individualberatung": "Max Mustermann", + "Pauschalberatung": "", + "VerlaufsprotokollDatum": "30.04.2023", + }, + { + "UN/ NWP/ Kontaktperson": "Maxi Musterfrau", + "Individualberatung": "", + "Pauschalberatung": "Ausbildung", + "Datum": "30.06.2023", + }, +] + + +nested_menu = { + "Notiz": None, + "Beratungsgespräch": None, + "Neu": {"Unternehmen": None, "Individualperson": None}, +} + + +# nested_menu = { +# "Notiz": None, +# "Beratungsgespräch": None, +# "Neu": {"Unternehmen": None, "Individualperson": None}, +# "Weitere": { +# "Sub 1": {"Sub-1-1": None, "Sub-1-2": None}, +# "Sub 2": {"Sub 2-1": None, "Sub-2-2": None}, +# "Sub 3 deutlich länger als andere": {"Sub 3-1": None, "Sub-3-2": None}, +# }, +# } + + +def build_menu( + title: str, + icon: str, + structure: dict[str, Any], +): + # Diese Funktion baut NUR die Einträge (Items), nicht die Container (Menus) + def _recursive(current_structure: dict[str, Any]): + for label, children in current_structure.items(): + if children: + # --- AST (Verzweigung) --- + with ui.menu_item(auto_close=False): + with ui.item_section(): + ui.label(label) + with ui.item_section().props("side"): + ui.icon("keyboard_arrow_right") # Das korrekte Pfeil-Icon + + # Hier bauen wir EINEN neuen Container für die nächste Ebene + with ui.menu().props('anchor="top end" self="top start"'): + _recursive(children) + else: + # --- BLATT (Endpunkt) --- + # Beachte das l=label für das Closure/Late-Binding-Problem + ui.menu_item(label, on_click=lambda l=label: ui.notify(f"{l} geklickt!")) + + with ui.button(title, icon=icon): + with ui.menu(): + _recursive(structure) + + +@ui.page("/") +def table_view(): + # Header-Bereich + with ui.column().classes("w-full max-w-5xl mx-auto p-2 gap-2"): + ui.label("Projekt-Verwaltung").classes("text-h4 mb-2") + + with ui.row().classes("w-full justify-left p-2"): + build_menu("Hinzufügen", "add", nested_menu) + # with ui.button("Neu", icon="add"): + # with ui.menu() as main_menu: + # ui.menu_item("Direkte Aktion", on_click=lambda: ui.notify("Aktion 1")) + + # ui.separator() + + # with ui.menu_item("Untermenü öffnen...", auto_close=False).props( + # "icon=arrow_right" + # ): + # with ui.item_section().props("side"): + # ui.icon("keyboard_arrow_right") + # with ui.menu().props('anchor="top end" self="top start"'): + # ui.menu_item("Unterpunkt A", on_click=lambda: ui.notify("A")) + # ui.menu_item("Unterpunkt B", on_click=lambda: ui.notify("B")) + + # --- EINGABE-BEREICH --- + with ui.row().classes("w-full items-center mb-4 gap-2"): + name_input = ui.input(placeholder="Name des Eintrags").classes("flex-grow") + status_input = ui.select(["Aktiv", "Geplant", "Erledigt"], value="Aktiv").classes( + "w-40" + ) + + def add_entry(): + if not name_input.value: + ui.notify("Bitte einen Namen eingeben!", type="negative") + return + + # Neuen Eintrag zum Datenmodell hinzufügen + new_id = f"{len(data) + 1:03d}" + data.append( + { + "id": new_id, + "name": name_input.value, + "status": status_input.value, + "date": "Heute", + } + ) + + # UI aktualisieren + name_input.value = "" # Feld leeren + # render_table.refresh() # Die Tabelle neu zeichnen + ui.notify(f"Eintrag {new_id} hinzugefügt") + + ui.button(on_click=add_entry, icon="add").classes("rounded-full") + + # Beispieldaten: Bei Projekt Gamma fehlt 'c3' komplett, bei Delta ist es leer/None + data = [ + { + "c1": "Projekt Alpha", + "c2": "Ganz normaler Eintrag mit allen Daten.", + "c3": "Teamleitung", + "c4": "Priorität Hoch", + "date": "17.04.2024", + }, + { + "c1": "Projekt Gamma", + "c2": "Hier fehlt die Abteilung (c3) komplett im Datenmodell.", + # 'c3' existiert hier nicht! + "c4": "Wartend", + "date": "21.04.2024", + }, + { + "c1": "Projekt Delta", + "c2": "Hier ist die Abteilung (c3) explizit None.", + "c3": None, + "c4": "Pausiert", + "date": "22.04.2024", + }, + ] + + # Äußerer Container mit grauem Hintergrund, damit die weißen Zellen besser wirken + # KORREKTUR: 'min-h-screen' entfernt. + # NEU: 'rounded-xl' hinzugefügt, damit der Hintergrund-Kasten saubere Ecken hat. + with ui.column().classes("w-full max-w-6xl mx-auto p-4 gap-2 bg-slate-50 rounded-xl"): + # --- TABELLEN-HEADER --- + # Auch hier min-w-0 hinzufügen, damit der Header nicht vom Text weggedrückt wird + with ui.row().classes( + "w-full p-2 font-bold text-slate-500 items-center no-wrap gap-2" + ): + ui.label("Name").classes("flex-1 min-w-0 text-center") + ui.label("Beschreibung").classes("flex-1 min-w-0 text-center") + ui.label("Abteilung").classes("flex-1 min-w-0 text-center") + ui.label("Status").classes("flex-1 min-w-0 text-center") + ui.label("Datum").classes("w-32 text-center") + + # --- TABELLEN-ZEILEN --- + for entry in data: + with ui.row().classes("w-full items-stretch no-wrap gap-2 mb-2"): + # WICHTIG: 'min-w-0' zwingt die Box, in ihrem Flex-Anteil zu bleiben! + cell_classes = ( + "p-3 bg-white border border-slate-200 rounded-lg shadow-sm " + "cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-all " + "flex items-center justify-center text-center min-w-0" + ) + text_logic = "whitespace-normal break-words" + + with ui.element("div").classes(f"flex-1 {cell_classes}"): + ui.label(entry.get("c1", "")).classes(f"{text_logic} font-bold") + + with ui.element("div").classes(f"flex-1 {cell_classes}"): + ui.label(entry.get("c2", "")).classes( + f"{text_logic} text-sm text-slate-600" + ) + + # --- ZELLE 3: PLATZHALTER --- + c3_value = entry.get("c3") + + if c3_value: + with ui.element("div").classes(f"flex-1 {cell_classes}"): + ui.label(c3_value).classes(text_logic) + else: + # WICHTIG: Auch der unsichtbare Platzhalter braucht 'min-w-0' + ui.element("div").classes("flex-1 min-w-0") + + with ui.element("div").classes(f"flex-1 {cell_classes}"): + ui.badge(entry.get("c4", "")).classes(text_logic) + + with ui.element("div").classes(f"w-32 {cell_classes}"): + ui.label(entry.get("date", "")).classes( + f"{text_logic} text-slate-400 text-xs font-mono" + ) + # # --- TABELLEN-HEADER --- + # with ui.row().classes("w-full bg-slate-200 p-4 rounded-t-lg font-bold items-center"): + # ui.label("UN/ NWP/ Kontaktperson").classes("w-12") + # ui.label("Individualberatung").classes("flex-grow") + # ui.label("Pauschalberatung").classes("w-24 text-center") + # ui.label("Aktion").classes("w-12 text-right") + + # # --- ZEILEN MIT EINZELN KLICKBAREN SPALTEN --- + # for entry in data: + # with ui.card().tight().classes("w-full mb-1"): + # with ui.row().classes("w-full p-4 items-center no-wrap"): + # # 1. Spalte: ID (Klickbar) + # ui.label(entry["UN/ NWP/ Kontaktperson"]).classes( + # "flex-grow text-slate-500 font-mono cursor-pointer hover:text-blue-600" + # ).on("click", lambda e=entry: ui.notify(f"ID {e['id']} Details öffnen")) + + # # 2. Spalte: Name (Klickbar, nimmt den meisten Platz ein) + # ui.label(entry["Individualberatung"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # # 3. Spalte: Status (Klickbar, als Badge) + # ui.label(entry["Pauschalberatung"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # # 4. Spalte: Einzelnes Icon (Klickbare Aktion am Rand) + # # 3. Spalte: Status (Klickbar, als Badge) + # ui.label(entry["Datum"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # --- DYNAMISCHER TABELLEN-INHALT --- + # Wir definieren eine Funktion mit dem @ui.refreshable Dekorator + # @ui.refreshable + # def render_table(): + # if not data: + # ui.label("Keine Einträge vorhanden").classes("p-4 text-slate-400") + # return + + # for entry in data: + # with ui.card().tight().classes("w-full hover:bg-blue-50 cursor-pointer mb-1"): + # with ui.row().classes("w-full p-4 items-center"): + # ui.label(entry["id"]).classes("w-12 text-slate-500 font-mono") + # ui.label(entry["name"]).classes("flex-grow font-medium") + # ui.badge( + # entry["status"], + # color="green" if entry["status"] == "Aktiv" else "grey", + # ).classes("w-20") + # ui.label(entry["date"]).classes( + # "w-24 text-right text-sm text-slate-400" + # ) + + # Initiales Rendern der Tabelle + # render_table() + + +ui.run(native=False) diff --git a/prototypes/t_nice_gui_2.py b/prototypes/t_nice_gui_2.py new file mode 100644 index 0000000..72ed2db --- /dev/null +++ b/prototypes/t_nice_gui_2.py @@ -0,0 +1,384 @@ +from typing import Any + +from nicegui import ui + + +def menu(): + with ui.column().classes("w-full items-center"): + ui.label("Mein Dashboard 2").classes("text-h4") + + +# Eine Hauptspalte für die gesamte Seite +# @ui.page("/") +# def main_page(): +# menu() +# # Erste Sektion: Zwei Karten nebeneinander +# with ui.row().classes("w-full justify-center"): +# with ui.card(): +# ui.label("Statistik A") +# ui.number(value=42) + +# with ui.card(): +# ui.label("Statistik B") +# ui.switch("Aktivieren") + +# # Zweite Sektion: Eine breite Spalte darunter +# with ui.card().classes("w-full items-left"): +# ui.label("Details") +# ui.markdown("Hier stehen weitere Informationen in einer eigenen Sektion.") + +# with ui.expansion("Erweiterte Einstellungen", icon="settings").classes("w-full"): +# ui.label("Hier kannst du tiefergehende Konfigurationen vornehmen.") +# # ui.input("API Schlüssel") +# ui.link("Gehe zur Einstellungs-Seite", "/settings") + + +# @ui.page("/settings") +# def settings_page(): +# menu() +# ui.label("Einstellungen").classes("text-h4") +# ui.button("Zurück", on_click=lambda: ui.navigate.to("/")) + +# Beispieldaten für die Kacheln +menu_items = [ + {"title": "Dashboard", "icon": "dashboard", "route": "/dashboard", "color": "#e3f2fd"}, + {"title": "Benutzer", "icon": "people", "route": "/users", "color": "#f1f8e9"}, + {"title": "Einstellungen", "icon": "settings", "route": "/settings", "color": "#fff3e0"}, + {"title": "Berichte", "icon": "assessment", "route": "/reports", "color": "#fce4ec"}, + {"title": "Hilfe", "icon": "help", "route": "/help", "color": "#f3e5f5"}, + {"title": "Abmelden", "icon": "logout", "route": "/logout", "color": "#efebe9"}, +] + + +# @ui.page("/") +# def main_page(): +# ui.label("Hauptmenü").classes("text-h4 mb-4") + +# # Erstellt ein Raster mit 3 Spalten (auf kleinen Bildschirmen 1, auf mittleren 2, auf großen 3) +# with ui.grid(columns="1fr 1fr 1fr").classes("w-full gap-4"): +# for item in menu_items: +# # Erstelle eine Karte als Kachel +# # 'cursor-pointer' macht den Mauszeiger zur Hand +# with ( +# ui.card() +# .tight() +# .classes("cursor-pointer hover:shadow-lg transition-shadow") +# .style(f"background-color: {item['color']}; height: 150px;") +# .on("click", lambda i=item: ui.navigate.to(i["route"])) +# ): +# with ui.column().classes("w-full h-full items-center justify-center p-4"): +# ui.icon(item["icon"]).classes("text-5xl mb-2") +# ui.label(item["title"]).classes("text-lg font-bold") + + +# # Dummy-Zielseiten +# @ui.page("/dashboard") +# def dashboard(): +# ui.label("Hier ist das Dashboard") +# ui.button("Zurück", on_click=lambda: ui.navigate.to("/")) + +# Beispieldaten: Eine Liste von Einträgen +# data = [ +# {"id": "001", "name": "Projekt Alpha", "status": "Aktiv", "date": "2023-10-01"}, +# {"id": "002", "name": "Kundenmeeting", "status": "Geplant", "date": "2023-10-05"}, +# {"id": "003", "name": "System-Update", "status": "Erledigt", "date": "2023-09-28"}, +# {"id": "004", "name": "Budget-Review", "status": "Aktiv", "date": "2023-10-12"}, +# ] + + +# @ui.page("/") +# def table_view(): +# with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-2"): +# # --- TABELLEN-KOPF --- +# # Wir nutzen ein Grid, um die Spaltenbreiten festzulegen (z.B. 10% ID, 50% Name, 20% Status, 20% Datum) +# with ui.row().classes( +# "w-full bg-slate-200 p-4 rounded-t-lg shadow-sm font-bold text-lg items-center" +# ): +# ui.label("ID").classes("w-12") +# ui.label("Bezeichnung").classes("flex-grow") +# ui.label("Status").classes("w-24") +# ui.label("Datum").classes("w-24 text-right") + +# # --- TABELLEN-EINTRÄGE --- +# for entry in data: +# # Jede Zeile ist eine klickbare Karte/Reihe +# with ( +# ui.card() +# .tight() +# .classes( +# "w-full hover:bg-blue-50 cursor-pointer transition-all hover:scale-[1.01]" +# ) +# .on("click", lambda e=entry: ui.notify(f"Gehe zu: {e['name']}")) +# ): +# with ui.row().classes("w-full p-4 items-center"): +# # Geringfügig kleinerer Text als im Header (text-base vs text-lg) +# ui.label(entry["id"]).classes("w-12 text-slate-500 font-mono") +# ui.label(entry["name"]).classes("flex-grow font-medium") + +# # Ein kleiner Chip für den Status +# ui.badge( +# entry["status"], +# color="green" if entry["status"] == "Aktiv" else "grey", +# ).classes("w-20") + +# ui.label(entry["date"]).classes("w-24 text-right text-sm text-slate-400") + + +# 1. Unsere Datenquelle (könnte später auch eine Datenbank sein) +data = [ + { + "UN/ NWP/ Kontaktperson": "Unternehmen 1", + "Individualberatung": "Max Mustermann", + "Pauschalberatung": "", + "VerlaufsprotokollDatum": "30.04.2023", + }, + { + "UN/ NWP/ Kontaktperson": "Maxi Musterfrau", + "Individualberatung": "", + "Pauschalberatung": "Ausbildung", + "Datum": "30.06.2023", + }, +] + + +nested_menu = { + "Notiz": None, + "Beratungsgespräch": None, + "Neu": {"Unternehmen": None, "Individualperson": None}, +} + + +# nested_menu = { +# "Notiz": None, +# "Beratungsgespräch": None, +# "Neu": {"Unternehmen": None, "Individualperson": None}, +# "Weitere": { +# "Sub 1": {"Sub-1-1": None, "Sub-1-2": None}, +# "Sub 2": {"Sub 2-1": None, "Sub-2-2": None}, +# "Sub 3 deutlich länger als andere": {"Sub 3-1": None, "Sub-3-2": None}, +# }, +# } + + +def build_menu( + title: str, + icon: str, + structure: dict[str, Any], +): + # Diese Funktion baut NUR die Einträge (Items), nicht die Container (Menus) + def _recursive(current_structure: dict[str, Any]): + for label, children in current_structure.items(): + if children: + # --- AST (Verzweigung) --- + with ui.menu_item(auto_close=False): + with ui.item_section(): + ui.label(label) + with ui.item_section().props("side"): + ui.icon("keyboard_arrow_right") # Das korrekte Pfeil-Icon + + # Hier bauen wir EINEN neuen Container für die nächste Ebene + with ui.menu().props('anchor="top end" self="top start"'): + _recursive(children) + else: + # --- BLATT (Endpunkt) --- + # Beachte das l=label für das Closure/Late-Binding-Problem + ui.menu_item(label, on_click=lambda l=label: ui.notify(f"{l} geklickt!")) + + with ui.button(title, icon=icon): + with ui.menu(): + _recursive(structure) + + +@ui.page("/") +def table_view(): + # Header-Bereich + with ui.column().classes("w-full max-w-5xl mx-auto p-2 gap-2"): + ui.label("Projekt-Verwaltung").classes("text-h4 mb-2") + + with ui.row().classes("w-full justify-left p-2"): + build_menu("Hinzufügen", "add", nested_menu) + # with ui.button("Neu", icon="add"): + # with ui.menu() as main_menu: + # ui.menu_item("Direkte Aktion", on_click=lambda: ui.notify("Aktion 1")) + + # ui.separator() + + # with ui.menu_item("Untermenü öffnen...", auto_close=False).props( + # "icon=arrow_right" + # ): + # with ui.item_section().props("side"): + # ui.icon("keyboard_arrow_right") + # with ui.menu().props('anchor="top end" self="top start"'): + # ui.menu_item("Unterpunkt A", on_click=lambda: ui.notify("A")) + # ui.menu_item("Unterpunkt B", on_click=lambda: ui.notify("B")) + + # --- EINGABE-BEREICH --- + with ui.row().classes("w-full items-center mb-4 gap-2"): + name_input = ui.input(placeholder="Name des Eintrags").classes("flex-grow") + status_input = ui.select(["Aktiv", "Geplant", "Erledigt"], value="Aktiv").classes( + "w-40" + ) + + def add_entry(): + if not name_input.value: + ui.notify("Bitte einen Namen eingeben!", type="negative") + return + + # Neuen Eintrag zum Datenmodell hinzufügen + new_id = f"{len(data) + 1:03d}" + data.append( + { + "id": new_id, + "name": name_input.value, + "status": status_input.value, + "date": "Heute", + } + ) + + # UI aktualisieren + name_input.value = "" # Feld leeren + # render_table.refresh() # Die Tabelle neu zeichnen + ui.notify(f"Eintrag {new_id} hinzugefügt") + + ui.button(on_click=add_entry, icon="add").classes("rounded-full") + + # Beispieldaten: Bei Projekt Gamma fehlt 'c3' komplett, bei Delta ist es leer/None + data = [ + { + "c1": "Projekt Alpha", + "c2": "Ganz normaler Eintrag mit allen Daten.", + "c3": "Teamleitung", + "c4": "Priorität Hoch", + "date": "17.04.2024", + }, + { + "c1": "Projekt Gamma", + "c2": "Hier fehlt die Abteilung (c3) komplett im Datenmodell.", + # 'c3' existiert hier nicht! + "c4": "Wartend", + "date": "21.04.2024", + }, + { + "c1": "Projekt Delta", + "c2": "Hier ist die Abteilung (c3) explizit None.", + "c3": None, + "c4": "Pausiert", + "date": "22.04.2024", + }, + ] + + # Äußerer Container mit grauem Hintergrund, damit die weißen Zellen besser wirken + # KORREKTUR: 'min-h-screen' entfernt. + # NEU: 'rounded-xl' hinzugefügt, damit der Hintergrund-Kasten saubere Ecken hat. + with ui.column().classes("w-full max-w-6xl mx-auto p-4 gap-2 bg-slate-50 rounded-xl"): + # --- DIE GRID-DEFINITION --- + # 4 gleich große Spalten (1fr) und eine feste (8rem = ca. 128px, entspricht w-32) + grid_classes = "w-full grid grid-cols-[1fr_1fr_1fr_1fr_8rem] gap-2" + + # --- TABELLEN-HEADER --- + # Wir nutzen ein normales div mit unseren grid_classes statt ui.row() + with ui.element("div").classes( + f"{grid_classes} p-2 font-bold text-slate-500 items-center" + ): + ui.label("Name").classes("text-center") + ui.label("Beschreibung").classes("text-center") + ui.label("Abteilung").classes("text-center") + ui.label("Status").classes("text-center") + ui.label("Datum").classes("text-center") + + # --- TABELLEN-ZEILEN --- + for entry in data: + # Auch die Zeile wird zu einem Grid-Container. 'items-stretch' funktioniert hier ebenfalls! + with ui.element("div").classes(f"{grid_classes} items-stretch mb-2"): + # Wir brauchen hier KEIN flex-1 oder min-w-0 mehr! Grid regelt die Breite. + # Wir behalten flex nur im Inneren der Box, um den Text zu zentrieren. + cell_classes = ( + "p-3 bg-white border border-slate-200 rounded-lg shadow-sm " + "cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-all " + "flex items-center justify-center text-center" + ) + text_logic = "whitespace-normal break-words" + + with ui.element("div").classes(cell_classes): + ui.label(entry.get("c1", "")).classes(f"{text_logic} font-bold") + + with ui.element("div").classes(cell_classes): + ui.label(entry.get("c2", "")).classes( + f"{text_logic} text-sm text-slate-600" + ) + + # --- ZELLE 3: PLATZHALTER --- + c3_value = entry.get("c3") + + if c3_value: + with ui.element("div").classes(cell_classes): + ui.label(c3_value).classes(text_logic) + else: + # Der Platzhalter braucht gar nichts mehr. Grid reserviert den Platz ohnehin! + ui.element("div") + + with ui.element("div").classes(cell_classes): + ui.badge(entry.get("c4", "")).classes(text_logic) + + with ui.element("div").classes(cell_classes): + ui.label(entry.get("date", "")).classes( + f"{text_logic} text-slate-400 text-xs font-mono" + ) + # # --- TABELLEN-HEADER --- + # with ui.row().classes("w-full bg-slate-200 p-4 rounded-t-lg font-bold items-center"): + # ui.label("UN/ NWP/ Kontaktperson").classes("w-12") + # ui.label("Individualberatung").classes("flex-grow") + # ui.label("Pauschalberatung").classes("w-24 text-center") + # ui.label("Aktion").classes("w-12 text-right") + + # # --- ZEILEN MIT EINZELN KLICKBAREN SPALTEN --- + # for entry in data: + # with ui.card().tight().classes("w-full mb-1"): + # with ui.row().classes("w-full p-4 items-center no-wrap"): + # # 1. Spalte: ID (Klickbar) + # ui.label(entry["UN/ NWP/ Kontaktperson"]).classes( + # "flex-grow text-slate-500 font-mono cursor-pointer hover:text-blue-600" + # ).on("click", lambda e=entry: ui.notify(f"ID {e['id']} Details öffnen")) + + # # 2. Spalte: Name (Klickbar, nimmt den meisten Platz ein) + # ui.label(entry["Individualberatung"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # # 3. Spalte: Status (Klickbar, als Badge) + # ui.label(entry["Pauschalberatung"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # # 4. Spalte: Einzelnes Icon (Klickbare Aktion am Rand) + # # 3. Spalte: Status (Klickbar, als Badge) + # ui.label(entry["Datum"]).classes( + # "flex-grow font-medium cursor-pointer hover:underline" + # ).on("click", lambda e=entry: ui.notify(f"Editor für {e['name']}")) + + # --- DYNAMISCHER TABELLEN-INHALT --- + # Wir definieren eine Funktion mit dem @ui.refreshable Dekorator + # @ui.refreshable + # def render_table(): + # if not data: + # ui.label("Keine Einträge vorhanden").classes("p-4 text-slate-400") + # return + + # for entry in data: + # with ui.card().tight().classes("w-full hover:bg-blue-50 cursor-pointer mb-1"): + # with ui.row().classes("w-full p-4 items-center"): + # ui.label(entry["id"]).classes("w-12 text-slate-500 font-mono") + # ui.label(entry["name"]).classes("flex-grow font-medium") + # ui.badge( + # entry["status"], + # color="green" if entry["status"] == "Aktiv" else "grey", + # ).classes("w-20") + # ui.label(entry["date"]).classes( + # "w-24 text-right text-sm text-slate-400" + # ) + + # Initiales Rendern der Tabelle + # render_table() + + +ui.run(native=False) diff --git a/prototypes/t_qt.py b/prototypes/t_qt.py new file mode 100644 index 0000000..ff8cbe6 --- /dev/null +++ b/prototypes/t_qt.py @@ -0,0 +1,285 @@ +import sys + +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction # WICHTIG: QAction wird für Menüeinträge gebraucht! +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QDialogButtonBox, + QFormLayout, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QScrollArea, + QSizePolicy, + QVBoxLayout, + QWidget, +) + + +# 1. Wir definieren unsere eigene klickbare "Zelle" +class ClickableCell(QFrame): + def __init__(self, text): + super().__init__() + + # Qt Style Sheets (QSS) für das Design (Rahmen, Hintergrund, Hover) + self.setStyleSheet(""" + ClickableCell { + background-color: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + } + ClickableCell:hover { + background-color: #eff6ff; + border: 1px solid #60a5fa; + } + """) + + # Layout in der Zelle, um den Text zu zentrieren + layout = QVBoxLayout(self) + + label = QLabel(text) + label.setWordWrap(True) # Textumbruch erzwingen! + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("border: none; background: transparent;") + + layout.addWidget(label) + + # Klick-Event abfangen + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + print("Zelle wurde geklickt!") + + +class HeaderCell(QLabel): + def __init__(self, text): + super().__init__(text) + + # Textausrichtung zentrieren + self.setAlignment(Qt.AlignCenter) + + # Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet + self.setStyleSheet(""" + HeaderCell { + background-color: #e2e8f0; + color: #475569; + font-weight: bold; + padding: 10px; + border-radius: 6px; + } + """) + + +class NewEntryDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Neuer Eintrag") + self.setMinimumWidth(350) + + # Ein FormLayout richtet Labels und Eingabefelder automatisch sauber aus + layout = QFormLayout(self) + + # Eingabefelder erstellen + self.input_c1 = QLineEdit() + self.input_c2 = QLineEdit() + self.input_c3 = QLineEdit() + self.input_c4 = QLineEdit() + self.input_date = QLineEdit() + + # Felder zum Layout hinzufügen + layout.addRow("Name:", self.input_c1) + layout.addRow("Beschreibung:", self.input_c2) + layout.addRow("Abteilung (optional):", self.input_c3) + layout.addRow("Status:", self.input_c4) + layout.addRow("Datum:", self.input_date) + + # Standard-Buttons (OK und Abbrechen) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) # Schließt Dialog und meldet "Erfolg" + buttons.rejected.connect(self.reject) # Schließt Dialog und meldet "Abbruch" + layout.addWidget(buttons) + + def get_data(self): + # Liest die Textfelder aus und gibt sie als Dictionary zurück + return { + "c1": self.input_c1.text(), + "c2": self.input_c2.text(), + # Wenn das Feld leer ist, speichern wir None (für unseren Platzhalter-Effekt) + "c3": self.input_c3.text() if self.input_c3.text().strip() else None, + "c4": self.input_c4.text(), + "date": self.input_date.text(), + } + + +# 2. Das Hauptfenster mit dem Grid-Layout +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Native PySide Tabelle") + self.resize(1800, 200) + + # --- 1. DAS MENÜ ERSTELLEN --- + self.create_menu() + + # --- 2. DAS ZENTRALE WIDGET --- + # Da das QMainWindow den Rahmen vorgibt, brauchen wir ein Container-Widget für die Mitte + central_widget = QWidget() + self.setCentralWidget(central_widget) + # Das Haupt-Layout des Fensters (Horizontal) + outer_layout = QHBoxLayout(central_widget) + + # Ein Container-Widget für deine Tabelle/Grid + container = QWidget() + # 2. NEU: Dem Container sagen: "Dehne dich so weit aus, wie du darfst!" + # container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + # --- NEU: DER SCROLL-BEREICH --- + scroll_area = QScrollArea() + scroll_area.setWidgetResizable( + True + ) # WICHTIG: Erlaubt dem Grid im Inneren, sich an die Breite anzupassen + scroll_area.setMinimumWidth(700) + scroll_area.setMaximumWidth( + 1500 + ) # Die Breiten-Begrenzung wandert nun auf die ScrollArea + scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht + scroll_area.setFrameShape(QFrame.NoFrame) + # --- WICHTIG: VERKNÜPFUNG --- + # 1. Den Container in die ScrollArea stecken + scroll_area.setWidget(container) + + # 3. Zentrierung durch "Stretches" (wie mx-auto) + # Wir fügen links und rechts vom Container Platzhalter ein + outer_layout.addStretch(1) + outer_layout.addWidget(scroll_area, stretch=100) + outer_layout.addStretch(1) + # Optional: Damit der Container oben am Rand klebt + outer_layout.setAlignment(Qt.AlignTop) + + # Wir geben dem Container ein vertikales Layout + container_layout = QVBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) # Entfernt unnötige Ränder + # Das Grid für unsere Tabelle + # Das Grid-Layout kommt in den Container + # self.grid = QGridLayout(container) + # Das Grid wird nun OHNE direkten Container erstellt + self.grid = QGridLayout() + self.grid.setSpacing(10) # Entspricht gap-2 + self.grid.setColumnStretch(0, 1) + self.grid.setColumnStretch(1, 1) + self.grid.setColumnStretch(2, 1) + self.grid.setColumnStretch(3, 1) + + # 3. Wir fügen das Grid oben in das vertikale Layout ein + container_layout.addLayout(self.grid) + # 4. DER ZAUBERTRICK: Wir setzen eine Feder unter das Grid. + # Diese Feder drückt das gesamte Grid nach oben und absorbiert den leeren Raum! + container_layout.addStretch() + + # Zeilen-Zähler (0 ist für die Überschriften) + self.current_row = 0 + # Beispieldaten (Projekt Gamma hat kein 'c3') + headers = ["Name", "Beschreibung", "Abteilung", "Status", "Datum"] + for col_idx, title in enumerate(headers): + self.grid.addWidget(HeaderCell(title), self.current_row, col_idx) + + self.current_row += 1 + + data = [ + { + "c1": "Projekt Alpha", + "c2": "Alles komplett.", + "c3": "Teamleitung", + "c4": "Hoch", + "date": "17.04.", + }, + { + "c1": "Projekt Beta", + "c2": "Abteilung fehlt.", + "c4": "Wartend", + "date": "21.04.", + }, + { + "c1": "Projekt Gamma", + "c2": "Alles komplett.", + "c3": "Teamleitung", + "c4": "Mittel", + "date": "30.04.", + }, + { + "c1": "Projekt Delta", + "c2": "Abteilung fehlt.", + "c4": "Wartend", + "date": "05.05.", + }, + ] + + # Wir gehen die Datenliste mit enumerate durch, um den row_index für das Grid zu bekommen + for entry in data: + self.add_row_to_grid(entry) + + # --- HILFSMETHODE UM EINE ZEILE EINZUFÜGEN --- + def add_row_to_grid(self, entry): + row = self.current_row + + self.grid.addWidget(ClickableCell(entry.get("c1", "")), row, 0) + self.grid.addWidget(ClickableCell(entry.get("c2", "")), row, 1) + + c3_value = entry.get("c3") + if c3_value: + self.grid.addWidget(ClickableCell(c3_value), row, 2) + else: + empty_box = QFrame() + empty_box.setStyleSheet( + "QFrame { background-color: #f8fafc; border: 2px dashed #e2e8f0; border-radius: 8px; }" + ) + self.grid.addWidget(empty_box, row, 2) + + self.grid.addWidget(ClickableCell(entry.get("c4", "")), row, 3) + self.grid.addWidget(ClickableCell(entry.get("date", "")), row, 4) + + self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen + + # --- MENÜ LOGIK --- + def create_menu(self): + menu_bar = self.menuBar() + file_menu = menu_bar.addMenu("Datei") + + new_action = QAction("Neuer Eintrag...", self) + new_action.setShortcut("Ctrl+N") + # VERKNÜPFUNG: Wenn geklickt, rufe die Methode zum Öffnen des Dialogs auf + new_action.triggered.connect(self.open_new_entry_dialog) + file_menu.addAction(new_action) + + file_menu.addSeparator() + + exit_action = QAction("Beenden", self) + exit_action.setShortcut("Ctrl+Q") + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 2. Hauptmenü-Punkt: "Hilfe" + help_menu = menu_bar.addMenu("Hilfe") + about_action = QAction("Über", self) + help_menu.addAction(about_action) + + # --- DIALOG AUFRUFEN & DATEN ÜBERNEHMEN --- + def open_new_entry_dialog(self): + dialog = NewEntryDialog(self) + + # .exec() pausiert das Programm, bis der Dialog geschlossen wird. + # Es gibt True zurück, wenn der Nutzer "OK" geklickt hat. + if dialog.exec(): + new_data = dialog.get_data() # Dictionary aus dem Dialog holen + self.add_row_to_grid(new_data) # Ins Grid zeichnen + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py new file mode 100644 index 0000000..3dbd37d --- /dev/null +++ b/prototypes/t_qt_2.py @@ -0,0 +1,966 @@ +from __future__ import annotations + +import dataclasses as dc +import sys + +from PySide6.QtCore import Qt, Signal # Signal ist wichtig! +from PySide6.QtGui import QAction +from PySide6.QtWidgets import ( + QApplication, + QComboBox, + QCompleter, + QDialog, + QDialogButtonBox, + QFormLayout, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPlainTextEdit, + QPushButton, + QScrollArea, + QSizePolicy, + QStackedWidget, + QVBoxLayout, + QWidget, +) + + +@dc.dataclass(slots=True) +class Address: + name: str + street: str + number: int + zip_code: str + city: str + + def export(self): + data = {} + for f in dc.fields(self): + data[f.name] = str(getattr(self, f.name)) + + return data + + +# added search field +class AddressForm(QWidget): + def __init__(self): + super().__init__() + + main_layout = QVBoxLayout(self) + form_layout = QFormLayout() + form_layout.setSpacing(15) + + # --- 1. Einfaches Feld: Firmenname --- + self.company_input = QLineEdit(placeholderText="Name des Partners") + form_layout.addRow("Name Unternehmen/Netzwerkpartner:", self.company_input) + + # --- 2. Kombinierte Zeile: Straße & Hausnummer --- + street_layout = QHBoxLayout() + street_layout.setContentsMargins(0, 0, 0, 0) # Wichtig: Verhindert doppelte Abstände! + street_layout.setSpacing(10) + + self.street_input = QLineEdit(placeholderText="Straße") + self.number_input = QLineEdit(placeholderText="Nr.") + + # Optik-Trick: Hausnummern-Feld begrenzen, damit es nicht so breit wie die Straße wird + self.number_input.setMaximumWidth(80) + + # Mit "stretch" definieren wir das Breitenverhältnis (Straße nimmt restlichen Platz) + street_layout.addWidget(self.street_input, stretch=3) + street_layout.addWidget(self.number_input, stretch=1) + + form_layout.addRow("Straße / Nr.:", street_layout) + + # --- 3. Kombinierte Zeile: PLZ & Ort --- + city_layout = QHBoxLayout() + city_layout.setContentsMargins(0, 0, 0, 0) + city_layout.setSpacing(10) + + self.zip_input = QLineEdit(placeholderText="PLZ") + self.city_input = QLineEdit(placeholderText="Ort") + + self.zip_input.setMaximumWidth(100) # PLZ ist immer relativ kurz + + city_layout.addWidget(self.zip_input, stretch=1) + city_layout.addWidget(self.city_input, stretch=3) + + form_layout.addRow("PLZ / Ort:", city_layout) + + main_layout.addLayout(form_layout) + + self.autofilled_fields = ( + self.street_input, + self.number_input, + self.zip_input, + self.city_input, + ) + for field in self.autofilled_fields: + field.setReadOnly(True) + field.setStyleSheet(""" + QLineEdit { + background-color: #f1f5f9; /* Helles System-Grau */ + color: #333D4B; /* Etwas blassere Schrift */ + border: 1px dashed #cbd5e1; /* Ein gestrichelter Rand wirkt oft wie ein "Stempel" */ + border-radius: 4px; + padding: 5px; + } + /* Wenn das Feld fokussiert wird, keinen blauen Rand anzeigen */ + QLineEdit:focus { + border: 1px dashed #cbd5e1; + } + """) + + def fill_out(self, address: Address): + addr_ = address.export() + + for field, value in zip(self.autofilled_fields, addr_.values()): + field.setText(value) + + +ADDRESSES = [ + Address("Test UG", "Teststraße", 1, "09111", "Chemnitz"), + Address("Max Mustermann GmbH", "Teststraße", 2, "09112", "Chemnitz"), + Address("Mustergruppe GbR", "Teststraße", 3, "09113", "Chemnitz"), + Address("Lorem Ipsum AG", "Teststraße", 4, "09114", "Chemnitz"), +] + + +class AddressForm_Search(QWidget): + def __init__(self): + super().__init__() + + main_layout = QVBoxLayout(self) + form_layout = QFormLayout() + form_layout.setSpacing(15) + + self.search_input = QLineEdit(placeholderText="Tippen zum Suchen...") + form_layout.addRow("Suche:", self.search_input) + search_data = [addr.name for addr in ADDRESSES] + self.SEARCH_MAP = {addr.name: addr for addr in ADDRESSES} + self.completer = QCompleter(search_data) + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + self.completer.setFilterMode(Qt.MatchContains) + self.search_input.setCompleter(self.completer) + self.completer.activated.connect(self.search_result_selected) + + # --- 1. Einfaches Feld: Firmenname --- + self.company_input = QLineEdit(placeholderText="Name des Partners") + form_layout.addRow("Name Unternehmen/Netzwerkpartner:", self.company_input) + + street_layout = QHBoxLayout() + street_layout.setContentsMargins(0, 0, 0, 0) # Wichtig: Verhindert doppelte Abstände! + street_layout.setSpacing(10) + + self.street_input = QLineEdit(placeholderText="Straße") + self.number_input = QLineEdit(placeholderText="Nr.") + + # Optik-Trick: Hausnummern-Feld begrenzen, damit es nicht so breit wie die Straße wird + self.number_input.setMaximumWidth(80) + + # Mit "stretch" definieren wir das Breitenverhältnis (Straße nimmt restlichen Platz) + street_layout.addWidget(self.street_input, stretch=3) + street_layout.addWidget(self.number_input, stretch=1) + + form_layout.addRow("Straße / Nr.:", street_layout) + + # --- 3. Kombinierte Zeile: PLZ & Ort --- + city_layout = QHBoxLayout() + city_layout.setContentsMargins(0, 0, 0, 0) + city_layout.setSpacing(10) + + self.zip_input = QLineEdit(placeholderText="PLZ") + self.city_input = QLineEdit(placeholderText="Ort") + + self.zip_input.setMaximumWidth(100) # PLZ ist immer relativ kurz + + city_layout.addWidget(self.zip_input, stretch=1) + city_layout.addWidget(self.city_input, stretch=3) + + form_layout.addRow("PLZ / Ort:", city_layout) + + main_layout.addLayout(form_layout) + + self.autofilled_fields = ( + self.company_input, + self.street_input, + self.number_input, + self.zip_input, + self.city_input, + ) + for field in self.autofilled_fields: + field.setReadOnly(True) + field.setStyleSheet(""" + QLineEdit { + background-color: #f1f5f9; /* Helles System-Grau */ + color: #333D4B; /* Etwas blassere Schrift */ + border: 1px dashed #cbd5e1; /* Ein gestrichelter Rand wirkt oft wie ein "Stempel" */ + border-radius: 4px; + padding: 5px; + } + /* Wenn das Feld fokussiert wird, keinen blauen Rand anzeigen */ + QLineEdit:focus { + border: 1px dashed #cbd5e1; + } + """) + + def fill_out(self, address: Address): + addr_ = address.export() + + for field, value in zip(self.autofilled_fields, addr_.values()): + field.setText(value) + + def search_result_selected(self, name): + address = self.SEARCH_MAP[name] + self.fill_out(address) + + +class DropdownSearch(QWidget): + def __init__(self): + super().__init__() + layout = QVBoxLayout(self) + + # 1. Das normale Eingabefeld + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Tippe zum Suchen (z.B. 'Pro')...") + self.search_input.setMinimumWidth(300) + + # 2. Deine Datenbank / Liste an Suchbegriffen + search_data = [ + "Projekt Alpha", + "Projekt Beta", + "Personalakte Müller", + "Protokoll April 2026", + "Abrechnung", + ] + + # 3. Den Completer erstellen und mit Daten füttern + self.completer = QCompleter(search_data) + + # --- WICHTIGE EINSTELLUNGEN --- + # Ignoriert Groß-/Kleinschreibung (sehr wichtig für eine gute Suche!) + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + + # 'MatchContains' sorgt dafür, dass "pha" auch "Projekt Alpha" findet. + # Standard ist 'MatchStartsWith' (findet nur Worte am Anfang). + self.completer.setFilterMode(Qt.MatchContains) + + # 4. Den Completer an das Eingabefeld binden + self.search_input.setCompleter(self.completer) + + layout.addWidget(self.search_input) + + # Optional: Aktion auslösen, wenn ein Element im Dropdown angeklickt wird + self.completer.activated.connect(self.on_item_selected) + + def on_item_selected(self, text): + print(f"Nutzer hat '{text}' aus dem Dropdown ausgewählt!") + # Hier könntest du z.B. deine Detail-Seite für dieses Projekt öffnen + + +class MyForm(QWidget): + def __init__(self): + super().__init__() + + # Das Herzstück: Das Form-Layout + # Es kümmert sich automatisch darum, dass alle Labels links + # und alle Felder rechts perfekt bündig untereinander stehen. + self.form_layout = QFormLayout(self) + self.form_layout.setSpacing(15) # Abstand zwischen den Zeilen + + # Definition deiner Felder + # 'key' ist der Name, unter dem du die Daten später abrufst + # 'label' ist der Text, der angezeigt wird + # 'type' bestimmt, welches Widget erstellt wird + self.field_definitions = [ + {"key": "name", "label": "Projektname:", "type": "text"}, + {"key": "date", "label": "Datum:", "type": "date", "value": "22.04.2026"}, + { + "key": "status", + "label": "Status:", + "type": "text", + "placeholder": "z.B. Aktiv", + }, + {"key": "desc", "label": "Beschreibung:", "type": "longtext"}, + {"key": "notes", "label": "Interne Notizen:", "type": "longtext"}, + ] + + # Dictionary, um die erstellten Widgets zu speichern (für späteren Zugriff) + self.widgets = {} + + # Automatischer Aufbau des Formulars + self.create_form_fields() + + def create_form_fields(self): + for field in self.field_definitions: + widget = None + + # Entscheidung: Welches Widget wird benötigt? + if field["type"] == "text": + widget = QLineEdit() + if "placeholder" in field: + widget.setPlaceholderText(field["placeholder"]) + if "value" in field: + widget.setText(field["value"]) + widget.setReadOnly(True) # Falls es ein Fixwert ist + + elif field["type"] == "longtext": + widget = QPlainTextEdit() + widget.setMaximumHeight(80) # Kompakte Höhe für Formulare + + elif field["type"] == "date": + widget = QLineEdit() # Oder QDateEdit + widget.setText(field.get("value", "")) + widget.setReadOnly(True) + widget.setStyleSheet("background-color: #f1f5f9; border: 1px dashed #cbd5e1;") + + if widget: + # Widget im Dictionary speichern, um später darauf zuzugreifen + self.widgets[field["key"]] = widget + # Dem Form-Layout hinzufügen (Label links, Widget rechts) + self.form_layout.addRow(field["label"], widget) + + def get_form_data(self): + """Liest alle Felder automatisch aus""" + data = {} + for key, widget in self.widgets.items(): + if isinstance(widget, QLineEdit): + data[key] = widget.text() + elif isinstance(widget, QPlainTextEdit): + data[key] = widget.toPlainText() + return data + + +class ClickableCell(QFrame): + # Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt + clicked = Signal(dict) + + def __init__(self, text, data_record): + super().__init__() + self.data_record = data_record # Wir merken uns den ganzen Datensatz + self.setStyleSheet(""" + ClickableCell { + background-color: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + } + ClickableCell:hover { + background-color: #eff6ff; + border: 1px solid #60a5fa; + } + """) + layout = QVBoxLayout(self) + label = QLabel(text) + label.setWordWrap(True) + label.setAlignment(Qt.AlignCenter) + layout.addWidget(label) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + # Wenn geklickt wird, senden wir die Daten aus + self.clicked.emit(self.data_record) + + +class HeaderCell(QLabel): + def __init__(self, text): + super().__init__(text) + + # Textausrichtung zentrieren + self.setAlignment(Qt.AlignCenter) + + # Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet + self.setStyleSheet(""" + HeaderCell { + background-color: #e2e8f0; + color: #475569; + font-weight: bold; + padding: 10px; + border-radius: 6px; + } + """) + + +class NewEntryDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Neuer Eintrag") + self.setMinimumWidth(350) + + # Ein FormLayout richtet Labels und Eingabefelder automatisch sauber aus + layout = QFormLayout(self) + + # Eingabefelder erstellen + self.input_c1 = QLineEdit() + self.input_c2 = QLineEdit() + self.input_c3 = QLineEdit() + self.input_c4 = QLineEdit() + self.input_date = QLineEdit() + + # Felder zum Layout hinzufügen + layout.addRow("Name:", self.input_c1) + layout.addRow("Beschreibung:", self.input_c2) + layout.addRow("Abteilung (optional):", self.input_c3) + layout.addRow("Status:", self.input_c4) + layout.addRow("Datum:", self.input_date) + + # Standard-Buttons (OK und Abbrechen) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) # Schließt Dialog und meldet "Erfolg" + buttons.rejected.connect(self.reject) # Schließt Dialog und meldet "Abbruch" + layout.addWidget(buttons) + + def get_data(self): + # Liest die Textfelder aus und gibt sie als Dictionary zurück + return { + "c1": self.input_c1.text(), + "c2": self.input_c2.text(), + # Wenn das Feld leer ist, speichern wir None (für unseren Platzhalter-Effekt) + "c3": self.input_c3.text() if self.input_c3.text().strip() else None, + "c4": self.input_c4.text(), + "date": self.input_date.text(), + } + + +# --- 2. DIE DETAIL-ANSICHT (SEITE 2) --- +class DetailView(QWidget): + back_requested = Signal() # Signal für den Zurück-Button + + def __init__(self): + super().__init__() + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) + + # Zurück-Button + back_btn = QPushButton("← Zurück zur Tabelle") + back_btn.setFixedWidth(200) + back_btn.clicked.connect(lambda: self.back_requested.emit()) + layout.addWidget(back_btn) + + # Platzhalter für die Details + self.title_label = QLabel("Titel") + self.title_label.setStyleSheet( + "font-size: 24px; font-weight: bold; margin-top: 20px;" + ) + layout.addWidget(self.title_label) + + self.info_label = QLabel("Zusatzinfos...") + self.info_label.setWordWrap(True) + self.info_label.setStyleSheet("font-size: 16px; color: #475569; margin-top: 10px;") + layout.addWidget(self.info_label) + + def update_content(self, data): + # Diese Methode füllt die Seite mit den echten Daten + self.title_label.setText(f"Details für: {data.get('c1', 'Unbekannt')}") + self.info_label.setText( + f"Beschreibung: {data.get('c2')}\n\n" + f"Abteilung: {data.get('c3', 'Keine Angabe')}\n" + f"Status: {data.get('c4')}\n" + f"Datum: {data.get('date')}" + ) + + +class NewEntrySelect_view(QWidget): + back_requested = Signal() # Signal für den Zurück-Button + company_requested = Signal() # Signal Unternehmen + person_requested = Signal() # Signal Unternehmen + + def __init__(self): + super().__init__() + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) + + # Zurück-Button + back_btn = QPushButton("← Zurück zur Übersicht") + back_btn.setFixedWidth(200) + back_btn.clicked.connect(lambda: self.back_requested.emit()) + + # Platzhalter für die Details + self.title_label = QLabel("Wählen Sie den Typ der Grunderfassung") + self.title_label.setStyleSheet( + "font-size: 24px; font-weight: bold; margin-top: 20px;" + ) + + btn_company = QPushButton("Unternehmen →") + btn_company.setFixedWidth(300) + btn_company.setFixedHeight(40) + btn_company.clicked.connect(lambda: self.company_requested.emit()) + # btn_company.clicked.connect(lambda: print("Unternehmen gewählt")) + + btn_person = QPushButton("Individualperson →") + btn_person.setFixedWidth(300) + btn_person.setFixedHeight(40) + btn_person.clicked.connect(lambda: self.person_requested.emit()) + btn_person.clicked.connect(lambda: print("Person gewählt")) + + layout.addWidget(back_btn) + layout.addSpacing(15) + layout.addWidget(self.title_label) + layout.addWidget(btn_company) + layout.addWidget(btn_person) + + # self.info_label = QLabel("Zusatzinfos...") + # self.info_label.setWordWrap(True) + # self.info_label.setStyleSheet("font-size: 16px; color: #475569; margin-top: 10px;") + # layout.addWidget(self.info_label) + + # def update_content(self, data): + # # Diese Methode füllt die Seite mit den echten Daten + # self.title_label.setText(f"Details für: {data.get('c1', 'Unbekannt')}") + # self.info_label.setText( + # f"Beschreibung: {data.get('c2')}\n\n" + # f"Abteilung: {data.get('c3', 'Keine Angabe')}\n" + # f"Status: {data.get('c4')}\n" + # f"Datum: {data.get('date')}" + # ) + + +class SearchFormPage(QWidget): + back_main_requested = Signal() # Signal für den Zurück-Button + back_requested = Signal() # Signal für den Zurück-Button + + def __init__(self): + super().__init__() + # Hauptlayout der Seite + main_layout = QVBoxLayout(self) + # main_layout.setContentsMargins(0, 0, 0, 0) + + # --- 1. HEADER --- + header_layout = QVBoxLayout() + upper_button_group_v = QVBoxLayout() + upper_button_group = QHBoxLayout() + upper_button_group.addLayout(upper_button_group_v) + upper_button_group.addStretch() + # header_layout = QGridLayout() + # header_layout.setColumnStretch(0, 1) + # header_layout.setColumnStretch(1, 1) + + back_btn_main = QPushButton("← Zurück zur Übersicht") + back_btn_main.clicked.connect(lambda: self.back_main_requested.emit()) + back_btn_step = QPushButton("← Zurück") + back_btn_step.clicked.connect(lambda: self.back_requested.emit()) + + title = QLabel("Grunderfassung Unternehmen") + title.setStyleSheet("font-size: 20px; font-weight: bold;") + + upper_button_group_v.addWidget(back_btn_step) + upper_button_group_v.addWidget(back_btn_main) + + header_layout.addLayout(upper_button_group) + header_layout.addSpacing(15) + # header_layout.addWidget(back_btn_step) + header_layout.addWidget(title) + # header_layout.addStretch() # Drückt den Titel nach links + main_layout.addLayout(header_layout) + + # --- KOPF Unternehmen --- + inf_block_1 = QHBoxLayout() + inhalte = [ + "Fall-Nr.:", + "Ersteintrag Datum:", + "Aktualisierung Datum:", + "Aktualisierung Nutzer:", + ] + for entry in inhalte: + label = QLabel(entry) + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + field = QLineEdit(placeholderText="...") + field.setText("22.04.2026") + field.setReadOnly(True) + field.setStyleSheet(""" + QLineEdit { + background-color: #f1f5f9; /* Helles System-Grau */ + color: #333D4B; /* Etwas blassere Schrift */ + border: 1px dashed #cbd5e1; /* Ein gestrichelter Rand wirkt oft wie ein "Stempel" */ + border-radius: 4px; + padding: 5px; + } + /* Wenn das Feld fokussiert wird, keinen blauen Rand anzeigen */ + QLineEdit:focus { + border: 1px #cbd5e1; + } + """) + field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + inf_block_1.addWidget(label) + inf_block_1.addWidget(field) + + inf_block_1.addStretch() + main_layout.addLayout(inf_block_1) + + # --- NOTIZEN Unternehmen --- + # eventuell später verknüpft + inf_block_2 = QHBoxLayout() + label = QLabel("Notizen:") + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + inf_block_2.addWidget(label, alignment=Qt.AlignmentFlag.AlignTop) + inf_block_2.addWidget(QPlainTextEdit(placeholderText="Notizen ergänzen...")) + + main_layout.addLayout(inf_block_2) + main_layout.addSpacing(10) + + # --- Suche mit Namen + # inf_block_3 = ( + # QVBoxLayout() + # ) # Horizontal, damit Suchen und Filtern nebeneinander stehen + # name_input = QLineEdit(placeholderText="Tippe zum Suchen...") + # name_input.setMinimumWidth(150) + # name_input.setMaximumWidth(600) + # inf_block_3_1 = QHBoxLayout() + # inf_block_3_1.addWidget(QLabel("Name")) + # inf_block_3_1.addWidget(name_input, stretch=100) + # inf_block_3_1.addStretch() + + # inf_block_3_2 = QHBoxLayout() + # inf_block_3_3 = QHBoxLayout() + # demo_data = { + # "name": "Test UG", + # "Straße": "Teststraße", + # "Hausnummer": "12", + # "PLZ": "09111", + # "Ort": "Chemnitz", + # } + + # current_block = inf_block_3_2 + # for entry in ("Straße", "Hausnummer", "PLZ", "Ort"): + # if entry == "PLZ": + # current_block = inf_block_3_3 + + # label = QLabel(entry) + # label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + # field = QLineEdit() + # field.setText(demo_data[entry]) + # field.setReadOnly(True) + # field.setStyleSheet(""" + # QLineEdit { + # background-color: #f1f5f9; /* Helles System-Grau */ + # color: #333D4B; /* Etwas blassere Schrift */ + # border: 1px dashed #cbd5e1; /* Ein gestrichelter Rand wirkt oft wie ein "Stempel" */ + # border-radius: 4px; + # padding: 5px; + # } + # /* Wenn das Feld fokussiert wird, keinen blauen Rand anzeigen */ + # QLineEdit:focus { + # border: 1px dashed #cbd5e1; + # } + # """) + # if entry in ("Hausnummer", "PLZ"): + # field.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + # field.setMinimumWidth(50) + # field.setMaximumWidth(50) + # else: + # field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + # field.setMinimumWidth(100) + # field.setMaximumWidth(300) + # current_block.addWidget(label) + # current_block.addWidget(field, stretch=100) + + # inf_block_3_2.addStretch() + # inf_block_3_3.addStretch() + + # inf_block_3.addLayout(inf_block_3_1) + # inf_block_3.addLayout(inf_block_3_2) + # inf_block_3.addLayout(inf_block_3_3) + # main_layout.addLayout(inf_block_3) + + main_layout.addSpacing(30) + + main_layout.addWidget(MyForm()) + + main_layout.addSpacing(30) + + # addr = Address("Test UG", "Teststraße", 202, "09111", "Chemnitz") + # addr_widget = AddressForm() + # addr_widget.fill_out(addr) + addr_widget = AddressForm_Search() + main_layout.addWidget(addr_widget) + + main_layout.addSpacing(30) + + main_layout.addWidget(DropdownSearch()) + + main_layout.addSpacing(30) + + # --- 2. SUCH-FORMULAR --- + form_layout = ( + QHBoxLayout() + ) # Horizontal, damit Suchen und Filtern nebeneinander stehen + + # Texteingabe für die Suche + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Tippe zum Suchen...") + self.search_input.textChanged.connect(self.perform_search) # LIVE-SUCHE! + + # Ein Dropdown als Filter-Beispiel + self.status_filter = QComboBox() + self.status_filter.addItems(["Alle", "Aktiv", "Wartend", "Abgeschlossen"]) + self.status_filter.currentTextChanged.connect(self.perform_search) + + form_layout.addWidget(QLabel("Suchbegriff:")) + form_layout.addWidget(self.search_input, stretch=2) + form_layout.addWidget(QLabel("Status:")) + form_layout.addWidget(self.status_filter, stretch=1) + + main_layout.addLayout(form_layout) + + # --- 3. ERGEBNIS-BEREICH --- + # Für den Anfang ein einfaches Listen-Widget + self.results_list = QListWidget() + main_layout.addWidget( + self.results_list, stretch=100 + ) # Nimmt den restlichen Platz ein + + # Dummy-Datenbank für das Beispiel + self.database = [ + "Projekt Alpha (Aktiv)", + "Projekt Beta (Abgeschlossen)", + "Projekt Gamma (Wartend)", + ] + self.perform_search() # Initiale Ansicht laden + + def perform_search(self): + # 1. Eingaben auslesen + query = self.search_input.text().lower() + status = self.status_filter.currentText() + + # 2. Alte Ergebnisse löschen + self.results_list.clear() + + # 3. Daten filtern und anzeigen + for item in self.database: + # Einfache Filter-Logik + if query in item.lower(): + if status == "Alle" or status in item: + self.results_list.addItem(item) + + +# 2. Das Hauptfenster mit dem Grid-Layout +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Master") + self.resize(1800, 200) + + # --- 1. DAS MENÜ ERSTELLEN --- + self.create_menu() + + # DER STACK (Stapel) + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + + # SEITE 1: Die Tabellen-Ansicht (unser bisheriger Code) + self.main_page = self.setup_main_page() + self.stack.addWidget(self.main_page) + + # SEITE 2: Die Detail-Ansicht + self.detail_page = DetailView() + self.detail_page.back_requested.connect(self.show_main_page) + self.stack.addWidget(self.detail_page) + + # SEITE: Neue Einträge hinzufügen + self.new_entry_select = NewEntrySelect_view() + self.new_entry_select.back_requested.connect(self.show_main_page) + self.new_entry_select.company_requested.connect(self.show_company_page) + self.stack.addWidget(self.new_entry_select) + # SEITE: Bsp. + self.company_page = SearchFormPage() + self.company_page.back_main_requested.connect(self.show_main_page) + self.company_page.back_requested.connect(self.show_new_entry_select) + self.stack.addWidget(self.company_page) + + def setup_main_page(self): + # --- 2. DAS ZENTRALE WIDGET --- + # Da das QMainWindow den Rahmen vorgibt, brauchen wir ein Container-Widget für die Mitte + main_widget = QWidget() + # self.setCentralWidget(central_widget) + # Das Haupt-Layout des Fensters (Horizontal) + outer_layout = QHBoxLayout(main_widget) + + vert_layout = QVBoxLayout() + + # add button + new_btn = QPushButton("Neu →") + new_btn.setFixedWidth(100) + new_btn.setFixedHeight(40) + new_btn.clicked.connect(self.show_new_entry_select) + # back_btn.clicked.connect(lambda: self.back_requested.emit()) + # layout.addWidget(back_btn) + + # Ein Container-Widget für deine Tabelle/Grid + container = QWidget() + # 2. NEU: Dem Container sagen: "Dehne dich so weit aus, wie du darfst!" + # container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + # SCROLL-BEREICH + scroll_area = QScrollArea() + scroll_area.setWidgetResizable( + True + ) # WICHTIG: Erlaubt dem Grid im Inneren, sich an die Breite anzupassen + scroll_area.setMinimumWidth(700) + scroll_area.setMaximumWidth( + 1500 + ) # Die Breiten-Begrenzung wandert nun auf die ScrollArea + scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht + scroll_area.setFrameShape(QFrame.NoFrame) + scroll_area.setWidget(container) + + vert_layout.addWidget(new_btn) + vert_layout.addSpacing(20) + vert_layout.addWidget(scroll_area) + + # Zentrierung durch "Stretches" (wie mx-auto) + # Wir fügen links und rechts vom Container Platzhalter ein + outer_layout.addStretch(1) + outer_layout.addLayout(vert_layout, stretch=100) + # outer_layout.addWidget(scroll_area, stretch=100) + outer_layout.addStretch(1) + # Optional: Damit der Container oben am Rand klebt + outer_layout.setAlignment(Qt.AlignTop) + + # Wir geben dem Container ein vertikales Layout + container_layout = QVBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) # Entfernt unnötige Ränder + # Das Grid für unsere Tabelle + # Das Grid-Layout kommt in den Container + # self.grid = QGridLayout(container) + # Das Grid wird nun OHNE direkten Container erstellt + self.grid = QGridLayout() + self.grid.setSpacing(10) # Entspricht gap-2 + self.grid.setColumnStretch(0, 1) + self.grid.setColumnStretch(1, 1) + self.grid.setColumnStretch(2, 1) + self.grid.setColumnStretch(3, 1) + + # 3. Wir fügen das Grid oben in das vertikale Layout ein + container_layout.addLayout(self.grid) + # 4. DER ZAUBERTRICK: Wir setzen eine Feder unter das Grid. + # Diese Feder drückt das gesamte Grid nach oben und absorbiert den leeren Raum! + container_layout.addStretch() + + # Zeilen-Zähler (0 ist für die Überschriften) + self.current_row = 0 + # Beispieldaten (Projekt Gamma hat kein 'c3') + headers = ["Name", "Beschreibung", "Abteilung", "Status", "Datum"] + for col_idx, title in enumerate(headers): + self.grid.addWidget(HeaderCell(title), self.current_row, col_idx) + + self.current_row += 1 + + data = [ + { + "c1": "Projekt Alpha", + "c2": "Alles komplett.", + "c3": "Teamleitung", + "c4": "Hoch", + "date": "17.04.", + }, + { + "c1": "Projekt Beta", + "c2": "Abteilung fehlt.", + "c4": "Wartend", + "date": "21.04.", + }, + { + "c1": "Projekt Gamma", + "c2": "Alles komplett.", + "c3": "Teamleitung", + "c4": "Mittel", + "date": "30.04.", + }, + { + "c1": "Projekt Delta", + "c2": "Abteilung fehlt.", + "c4": "Wartend", + "date": "05.05.", + }, + ] + + # Wir gehen die Datenliste mit enumerate durch, um den row_index für das Grid zu bekommen + for entry in data: + self.add_row_to_grid(entry) + + return main_widget + + # --- HILFSMETHODE UM EINE ZEILE EINZUFÜGEN --- + def add_row_to_grid(self, entry): + row = self.current_row + + # Beim Erstellen der Zelle übergeben wir den kompletten Datensatz + cell = ClickableCell(entry["c1"], entry) + cell.clicked.connect(self.show_details) + self.grid.addWidget(cell, row, 0) + # Wir verbinden das Klick-Signal der Zelle mit unserer Wechsel-Funktion + self.grid.addWidget(ClickableCell(entry["c2"], entry), row, 1) + + c3_value = entry.get("c3") + if c3_value: + self.grid.addWidget(ClickableCell(c3_value, entry), row, 2) + else: + empty_box = QFrame() + empty_box.setStyleSheet( + "QFrame { background-color: #f8fafc; border: 2px dashed #e2e8f0; border-radius: 8px; }" + ) + self.grid.addWidget(empty_box, row, 2) + + self.grid.addWidget(ClickableCell(entry.get("c4", ""), entry), row, 3) + self.grid.addWidget(ClickableCell(entry.get("date", ""), entry), row, 4) + + self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen + + def show_details(self, data): + # 1. Daten an die Detail-Seite übergeben + self.detail_page.update_content(data) + # 2. Auf die Detail-Seite umblättern + self.stack.setCurrentWidget(self.detail_page) + + def show_main_page(self): + # Zurück zur Tabelle blättern + self.stack.setCurrentWidget(self.main_page) + + def show_new_entry_select(self): + self.stack.setCurrentWidget(self.new_entry_select) + + def show_company_page(self): + self.stack.setCurrentWidget(self.company_page) + + # --- MENÜ LOGIK --- + def create_menu(self): + menu_bar = self.menuBar() + file_menu = menu_bar.addMenu("Datei") + + new_action = QAction("Neuer Eintrag...", self) + new_action.setShortcut("Ctrl+N") + # VERKNÜPFUNG: Wenn geklickt, rufe die Methode zum Öffnen des Dialogs auf + new_action.triggered.connect(self.open_new_entry_dialog) + file_menu.addAction(new_action) + + file_menu.addSeparator() + + exit_action = QAction("Beenden", self) + exit_action.setShortcut("Ctrl+Q") + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 2. Hauptmenü-Punkt: "Hilfe" + help_menu = menu_bar.addMenu("Hilfe") + about_action = QAction("Über", self) + help_menu.addAction(about_action) + + # --- DIALOG AUFRUFEN & DATEN ÜBERNEHMEN --- + def open_new_entry_dialog(self): + dialog = NewEntryDialog(self) + + # .exec() pausiert das Programm, bis der Dialog geschlossen wird. + # Es gibt True zurück, wenn der Nutzer "OK" geklickt hat. + if dialog.exec(): + new_data = dialog.get_data() # Dictionary aus dem Dialog holen + self.add_row_to_grid(new_data) # Ins Grid zeichnen + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/prototypes/tests.py b/prototypes/tests.py new file mode 100644 index 0000000..e8fbb86 --- /dev/null +++ b/prototypes/tests.py @@ -0,0 +1,28 @@ +# %% +import dataclasses as dc + + +# %% +@dc.dataclass(slots=True) +class Address: + street: str + number: int + postal_code: str + city: str + + def export(self): + data = {} + for f in dc.fields(self): + val = getattr(self, f.name) + if f.type is int: + val = str(val) + data[f.name] = val + + return data + + +# %% +addr = Address("Teststraße", 202, "09111", "Chemnitz") +# %% +addr.export() +# %%