generated from dopt-python/py311
Compare commits
No commits in common. "dev" and "main" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# own
|
# own
|
||||||
# prototypes/
|
prototypes/
|
||||||
data/
|
data/
|
||||||
reports/
|
reports/
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|||||||
@ -1,153 +0,0 @@
|
|||||||
# %%
|
|
||||||
import importlib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import polars as pl
|
|
||||||
import sqlalchemy as sql
|
|
||||||
|
|
||||||
from wce_crm import db
|
|
||||||
|
|
||||||
importlib.reload(db)
|
|
||||||
|
|
||||||
# %%
|
|
||||||
PTH_DATA_DB = Path.cwd().parent / "data/db"
|
|
||||||
assert PTH_DATA_DB.exists()
|
|
||||||
assert PTH_DATA_DB.is_dir()
|
|
||||||
# %%
|
|
||||||
DB_KL = PTH_DATA_DB / "wce_kontaktliste.db"
|
|
||||||
DB_CRM = PTH_DATA_DB / "wce_crm.db"
|
|
||||||
assert DB_KL.exists()
|
|
||||||
assert DB_CRM.exists()
|
|
||||||
# %%
|
|
||||||
engine = sql.create_engine(f"sqlite:///{DB_CRM}")
|
|
||||||
# %%
|
|
||||||
db.df_crm_master
|
|
||||||
# %%
|
|
||||||
stmt = sql.select(db.ext_crm_master)
|
|
||||||
str(stmt.compile(engine))
|
|
||||||
df = pl.read_database(stmt, engine, schema_overrides=db.ext_crm_master_schema)
|
|
||||||
# df = pl.concat([df, df[:2]])
|
|
||||||
# %%
|
|
||||||
df.select("ma_unternehmensname").is_duplicated().sum()
|
|
||||||
# %%
|
|
||||||
q = df.lazy()
|
|
||||||
counter = pl.int_range(0, pl.len()).over(pl.col.ma_unternehmensname)
|
|
||||||
q = q.with_columns(
|
|
||||||
ma_unternehmensname_dedupl=pl.when(counter == 0)
|
|
||||||
.then(pl.col.ma_unternehmensname)
|
|
||||||
.otherwise(pl.format("{} ({})", pl.col.ma_unternehmensname, counter))
|
|
||||||
)
|
|
||||||
df = q.collect()
|
|
||||||
df.select("ma_unternehmensname_dedupl").is_duplicated().sum()
|
|
||||||
|
|
||||||
# %%
|
|
||||||
# mapping dedupl text to idx
|
|
||||||
df.head()
|
|
||||||
|
|
||||||
# dict(zip(df["ma_unternehmensname_dedupl"], df["ma_id"]))
|
|
||||||
|
|
||||||
# %%
|
|
||||||
sub = df[0]
|
|
||||||
sub
|
|
||||||
# sub.with_columns(
|
|
||||||
# # pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein"))
|
|
||||||
# pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein")).name.keep()
|
|
||||||
# )
|
|
||||||
# %%
|
|
||||||
q = (
|
|
||||||
sub.lazy()
|
|
||||||
.with_columns(
|
|
||||||
pl.col(pl.Datetime).dt.to_string("%d.%m.%Y"),
|
|
||||||
pl.col(pl.Date).dt.to_string("%d.%m.%Y"),
|
|
||||||
pl.when(pl.col(pl.Boolean)).then(pl.lit("Ja")).otherwise(pl.lit("Nein")).name.keep(),
|
|
||||||
)
|
|
||||||
.with_columns(pl.all().cast(pl.String))
|
|
||||||
)
|
|
||||||
sub = q.collect()
|
|
||||||
sub
|
|
||||||
# %%
|
|
||||||
df.row(0, named=True)
|
|
||||||
|
|
||||||
# %%
|
|
||||||
db.df_crm_master.estimated_size("mb")
|
|
||||||
|
|
||||||
|
|
||||||
# %%
|
|
||||||
# // CRM Ansprechpartner
|
|
||||||
df = db.df_contact_person.filter(pl.col.ma_id == 410)
|
|
||||||
df = df.with_columns(pl.col(pl.String).str.replace_all(r"[\r\t\n]", " ").str.strip_chars(" "))
|
|
||||||
df
|
|
||||||
|
|
||||||
# %%
|
|
||||||
tuple(zip(df["ma_id"], df["an_id"]))
|
|
||||||
|
|
||||||
# %%
|
|
||||||
db.df_crm_master
|
|
||||||
# %%
|
|
||||||
# // CRM Nutzer
|
|
||||||
stmt = sql.select(db.ext_crm_nutzer).limit(20)
|
|
||||||
str(stmt.compile(engine))
|
|
||||||
df = pl.read_database(stmt, engine, schema_overrides=db.ext_crm_nutzer_schema)
|
|
||||||
# %%
|
|
||||||
stmt = sql.text("""SELECT ma_unternehmensname, ma_ersteintrag_datum, ma_aktualisierung_datum
|
|
||||||
FROM Master
|
|
||||||
WHERE ma_ersteintrag_datum LIKE '%ff'
|
|
||||||
LIMIT 10;""")
|
|
||||||
|
|
||||||
with engine.connect() as con:
|
|
||||||
res = con.execute(stmt)
|
|
||||||
|
|
||||||
print(res.fetchall())
|
|
||||||
|
|
||||||
# %%
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
engine = sql.create_engine(f"sqlite:///{DB_KL}")
|
|
||||||
stmt = sql.select(db.ext_kl_unternehmen.c.u_firmenname).limit(20)
|
|
||||||
|
|
||||||
with engine.connect() as con:
|
|
||||||
res = con.execute(stmt)
|
|
||||||
|
|
||||||
res.scalars().all()
|
|
||||||
# %%
|
|
||||||
for _ in res.mappings():
|
|
||||||
print(_)
|
|
||||||
# %%
|
|
||||||
# %%
|
|
||||||
stmt = sql.select(db.ext_kl_unternehmen)
|
|
||||||
df = pl.read_database(stmt, engine, schema_overrides=db.ext_kl_unternehmen_schema)
|
|
||||||
# %%
|
|
||||||
df
|
|
||||||
# %%
|
|
||||||
df.estimated_size("mb")
|
|
||||||
# %%
|
|
||||||
df.height
|
|
||||||
# %%
|
|
||||||
db.df_kontaktliste
|
|
||||||
# %%
|
|
||||||
sub = db.df_kontaktliste.select(["u_id", "u_firmenname"]).lazy()
|
|
||||||
# %%
|
|
||||||
counter = pl.int_range(0, pl.len()).over(pl.col.u_firmenname)
|
|
||||||
sub = sub.with_columns(
|
|
||||||
t=pl.when(counter == 0)
|
|
||||||
.then(pl.col.u_firmenname)
|
|
||||||
.otherwise(pl.format("{} ({})", pl.col.u_firmenname, counter))
|
|
||||||
)
|
|
||||||
# %%
|
|
||||||
sub.collect()
|
|
||||||
|
|
||||||
# %%
|
|
||||||
# 1. Create a sample DataFrame
|
|
||||||
df = pl.DataFrame({"text_col": ["TEST", "APPLE", "TEST", "TEST", "BANANA", "APPLE"]})
|
|
||||||
|
|
||||||
# 2. Define the window function to count occurrences
|
|
||||||
# This generates a sequence [0, 1, 2...] for each unique string
|
|
||||||
counter = pl.int_range(0, pl.len()).over("text_col")
|
|
||||||
|
|
||||||
# 3. Apply the conditional formatting
|
|
||||||
df = df.with_columns(
|
|
||||||
updated_col=pl.when(counter == 0)
|
|
||||||
.then(pl.col("text_col")) # Keep original for the first occurrence
|
|
||||||
.otherwise(pl.format("{} ({})", pl.col("text_col"), counter)) # Format duplicates
|
|
||||||
)
|
|
||||||
# %%
|
|
||||||
df
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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"))
|
|
||||||
@ -1,378 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,384 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,285 +0,0 @@
|
|||||||
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())
|
|
||||||
1590
prototypes/t_qt_2.py
1590
prototypes/t_qt_2.py
File diff suppressed because it is too large
Load Diff
@ -1,45 +0,0 @@
|
|||||||
# %%
|
|
||||||
import dataclasses as dc
|
|
||||||
import enum
|
|
||||||
|
|
||||||
from PySide6.QtCore import QDate, Qt
|
|
||||||
|
|
||||||
|
|
||||||
class FormFieldType(enum.StrEnum):
|
|
||||||
TEXT = enum.auto()
|
|
||||||
LONGTEXT = enum.auto()
|
|
||||||
DATE = enum.auto()
|
|
||||||
DATETIME = enum.auto()
|
|
||||||
|
|
||||||
|
|
||||||
@dc.dataclass(slots=True)
|
|
||||||
class FormField:
|
|
||||||
key: str
|
|
||||||
label: str
|
|
||||||
type: FormFieldType
|
|
||||||
required: bool
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
self.label = self.label.strip()
|
|
||||||
if not self.label.endswith(":"):
|
|
||||||
self.label += ":"
|
|
||||||
if self.required:
|
|
||||||
self.label += "*"
|
|
||||||
|
|
||||||
|
|
||||||
# %%
|
|
||||||
FormField("name", "Projektbeschreibung", FormFieldType.LONGTEXT, required=True)
|
|
||||||
# %%
|
|
||||||
FormField("name", "Projektbeschreibung:", FormFieldType.LONGTEXT, required=True)
|
|
||||||
|
|
||||||
# %%
|
|
||||||
FormField("name", "Projektbeschreibung", FormFieldType.LONGTEXT, required=False)
|
|
||||||
# %%
|
|
||||||
FormField("name", "Projektbeschreibung:", FormFieldType.LONGTEXT, required=False)
|
|
||||||
# %%
|
|
||||||
addr.export()
|
|
||||||
# %%
|
|
||||||
set_date = QDate.fromString("26.07.2026", "dd.MM.yyyy")
|
|
||||||
|
|
||||||
# %%
|
|
||||||
Qt.Tet
|
|
||||||
@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "wce-crm"
|
name = "wce-gui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "GUI for CRM of NAFKA project with WCE"
|
description = "GUI for NAFKA project with WCE"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
|
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
|
||||||
]
|
]
|
||||||
dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4"]
|
dependencies = []
|
||||||
requires-python = "<3.14,>=3.11"
|
requires-python = ">=3.11"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "LicenseRef-Proprietary"}
|
license = {text = "LicenseRef-Proprietary"}
|
||||||
|
|
||||||
@ -143,8 +143,6 @@ dev = [
|
|||||||
"pdoc3>=0.11.5",
|
"pdoc3>=0.11.5",
|
||||||
"bump-my-version>=1.1.1",
|
"bump-my-version>=1.1.1",
|
||||||
"nox>=2025.2.9",
|
"nox>=2025.2.9",
|
||||||
"pywebview>=6.2.1",
|
|
||||||
"pythonnet==3.0.5",
|
|
||||||
]
|
]
|
||||||
nb = [
|
nb = [
|
||||||
"jupyterlab>=4.3.5",
|
"jupyterlab>=4.3.5",
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
import wce_crm.env
|
|
||||||
|
|
||||||
wce_crm.env.setup()
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TypedDict, cast
|
|
||||||
|
|
||||||
import polars as pl
|
|
||||||
|
|
||||||
from wce_crm import db
|
|
||||||
|
|
||||||
|
|
||||||
class CompanyInfo(TypedDict):
|
|
||||||
ma_id: str
|
|
||||||
wce_id: str
|
|
||||||
ma_unternehmensname: str
|
|
||||||
ma_branche: str
|
|
||||||
ma_strasse: str
|
|
||||||
ma_hausnummer: str
|
|
||||||
ma_plz: str
|
|
||||||
ma_ort: str
|
|
||||||
ma_plz_postfach: str
|
|
||||||
ma_postfach: str
|
|
||||||
ma_website: str
|
|
||||||
ma_mail: str
|
|
||||||
ma_telefonnummer: str
|
|
||||||
ma_faxnummer: str
|
|
||||||
ma_ersteintrag_datum: str
|
|
||||||
ma_aktualisierung_datum: str
|
|
||||||
ma_aktualisierung_nutzer: str
|
|
||||||
ma_sollprozess: str
|
|
||||||
ma_auslaendische_mitarbeiter: str
|
|
||||||
ma_quelle_information: str
|
|
||||||
ma_bemerkung: str
|
|
||||||
ma_kontakt: str
|
|
||||||
ma_schlagworte: str
|
|
||||||
ma_archiviert: str
|
|
||||||
|
|
||||||
|
|
||||||
class ContactPersonInfo(TypedDict):
|
|
||||||
an_id: str
|
|
||||||
ma_id: str
|
|
||||||
wce_id: str
|
|
||||||
st_id: str
|
|
||||||
an_sachgebiet: str
|
|
||||||
an_anrede: str
|
|
||||||
an_titel: str
|
|
||||||
an_nachname: str
|
|
||||||
an_vorname: str
|
|
||||||
an_position: str
|
|
||||||
an_mail: str
|
|
||||||
an_festnetz: str
|
|
||||||
an_mobil: str
|
|
||||||
an_faxnummer: str
|
|
||||||
an_hauptansprechpartner: str
|
|
||||||
an_anrede_anschrift: str
|
|
||||||
an_bemerkung: str
|
|
||||||
an_aktualisierung_datum: str
|
|
||||||
an_aktualisierung_nutzer: str
|
|
||||||
an_letztes_kontaktdatum: str
|
|
||||||
an_ersteintrag_datum: str
|
|
||||||
an_archiviert: str
|
|
||||||
|
|
||||||
|
|
||||||
def _transform_for_gui_output(
|
|
||||||
data: pl.DataFrame,
|
|
||||||
) -> pl.DataFrame:
|
|
||||||
q = (
|
|
||||||
data.lazy()
|
|
||||||
.with_columns(
|
|
||||||
pl.col(pl.Datetime).dt.to_string("%d.%m.%Y"),
|
|
||||||
pl.col(pl.Date).dt.to_string("%d.%m.%Y"),
|
|
||||||
pl.when(pl.col(pl.Boolean))
|
|
||||||
.then(pl.lit("Ja"))
|
|
||||||
.otherwise(pl.lit("Nein"))
|
|
||||||
.name.keep(),
|
|
||||||
)
|
|
||||||
.with_columns(pl.all().cast(pl.String))
|
|
||||||
)
|
|
||||||
|
|
||||||
return q.collect()
|
|
||||||
|
|
||||||
|
|
||||||
def comp_search_choices() -> tuple[tuple[str, int], ...]:
|
|
||||||
# TODO no reload functionality
|
|
||||||
q = db.df_crm_master.lazy()
|
|
||||||
counter = pl.int_range(0, pl.len()).over(pl.col.ma_unternehmensname)
|
|
||||||
q = q.with_columns(
|
|
||||||
dedupl=pl.when(counter == 0)
|
|
||||||
.then(pl.col.ma_unternehmensname)
|
|
||||||
.otherwise(pl.format("{} ({})", pl.col.ma_unternehmensname, counter))
|
|
||||||
)
|
|
||||||
df = q.collect()
|
|
||||||
|
|
||||||
# return dict(zip(df["dedupl"], df["ma_id"]))
|
|
||||||
return tuple(zip(df["dedupl"], df["ma_id"]))
|
|
||||||
|
|
||||||
|
|
||||||
def comp_search_get_info(
|
|
||||||
ma_id: int,
|
|
||||||
) -> CompanyInfo:
|
|
||||||
df = db.df_crm_master.filter(pl.col.ma_id == ma_id)
|
|
||||||
if df.height > 1 or df.height == 0:
|
|
||||||
raise ValueError(f"Größe des zurückgelieferten Datenpakets ungültig: {df.height}")
|
|
||||||
|
|
||||||
df = _transform_for_gui_output(df)
|
|
||||||
return cast(CompanyInfo, df.row(0, named=True))
|
|
||||||
|
|
||||||
|
|
||||||
def contact_person_search_choices(
|
|
||||||
ma_id: int | None,
|
|
||||||
use_both_names: bool,
|
|
||||||
) -> tuple[tuple[str, int], ...]:
|
|
||||||
# TODO no reload functionality
|
|
||||||
q = db.df_contact_person.lazy()
|
|
||||||
if ma_id is not None:
|
|
||||||
q = q.filter(pl.col.ma_id == ma_id)
|
|
||||||
|
|
||||||
dedupl_col = pl.col.an_nachname
|
|
||||||
if use_both_names:
|
|
||||||
q = q.with_columns(
|
|
||||||
name_search=(pl.format("{}, {}", pl.col.an_nachname, pl.col.an_vorname))
|
|
||||||
)
|
|
||||||
dedupl_col = pl.col.name_search
|
|
||||||
|
|
||||||
counter = pl.int_range(0, pl.len()).over(dedupl_col)
|
|
||||||
q = q.with_columns(
|
|
||||||
dedupl=pl.when(counter == 0)
|
|
||||||
.then(dedupl_col)
|
|
||||||
.otherwise(pl.format("{} ({})", dedupl_col, counter))
|
|
||||||
)
|
|
||||||
df = q.collect()
|
|
||||||
|
|
||||||
# return dict(zip(df["dedupl"], df["an_id"]))
|
|
||||||
return tuple(zip(df["dedupl"], df["an_id"]))
|
|
||||||
|
|
||||||
|
|
||||||
def contact_person_search_get_info(
|
|
||||||
an_id: int,
|
|
||||||
) -> ContactPersonInfo:
|
|
||||||
df = db.df_contact_person.filter(pl.col.an_id == an_id)
|
|
||||||
if df.height > 1 or df.height == 0:
|
|
||||||
raise ValueError(f"Größe des zurückgelieferten Datenpakets ungültig: {df.height}")
|
|
||||||
|
|
||||||
df = _transform_for_gui_output(df)
|
|
||||||
return cast(ContactPersonInfo, df.row(0, named=True))
|
|
||||||
@ -1,297 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import polars as pl
|
|
||||||
import sqlalchemy as sql
|
|
||||||
from sqlalchemy import Column, String, Table, TypeDecorator
|
|
||||||
|
|
||||||
from wce_crm import types as t
|
|
||||||
|
|
||||||
|
|
||||||
class SafeDateTime(TypeDecorator):
|
|
||||||
"""Cleans non-standard ISO strings before parsing."""
|
|
||||||
|
|
||||||
impl = String # We treat the underlying data as a String first
|
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 1. Remove the trailing 'ff' (or any trailing letters)
|
|
||||||
# 2. Replace comma with dot (SQLAlchemy prefers . over ,)
|
|
||||||
clean_value = re.sub(r"[a-zA-Z]+$", "", value).replace(",", ".")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return datetime.fromisoformat(clean_value)
|
|
||||||
except ValueError:
|
|
||||||
# Fallback if it's still weird
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
md_crm = sql.MetaData()
|
|
||||||
|
|
||||||
# ---------- OLD "Kontaktliste" ----------
|
|
||||||
|
|
||||||
md_kontaktliste = sql.MetaData()
|
|
||||||
|
|
||||||
ext_kl_unternehmen: sql.Table = Table(
|
|
||||||
"Unternehmen",
|
|
||||||
md_kontaktliste,
|
|
||||||
Column("u_id", sql.Integer, nullable=False, unique=True),
|
|
||||||
Column("u_zeitstempel_eintrag", sql.DateTime, nullable=False),
|
|
||||||
Column("u_rechtsform", sql.Text, nullable=False),
|
|
||||||
Column("u_firmenname", sql.Text, nullable=False),
|
|
||||||
Column("u_strasse", sql.Text, nullable=False),
|
|
||||||
Column("u_hausnummer", sql.Text, nullable=False),
|
|
||||||
Column("u_adresszusatz", sql.Text, nullable=True),
|
|
||||||
Column("u_plz", sql.Text, nullable=False),
|
|
||||||
Column("u_ort", sql.Text, nullable=False),
|
|
||||||
Column("u_postfach", sql.Text, nullable=True),
|
|
||||||
Column("u_website", sql.Text, nullable=True),
|
|
||||||
Column("u_anrede", sql.Text, nullable=False),
|
|
||||||
Column("u_titel", sql.Text, nullable=True),
|
|
||||||
Column("u_vorname", sql.Text, nullable=False),
|
|
||||||
Column("u_nachname", sql.Text, nullable=False),
|
|
||||||
Column("u_funktion", sql.Text, nullable=False),
|
|
||||||
Column("u_mail", sql.Text, nullable=False),
|
|
||||||
Column("u_telefon", sql.Text, nullable=False),
|
|
||||||
Column("u_plz_postfach", sql.Text, nullable=True),
|
|
||||||
Column("u_einwilligung_inhaber", sql.Boolean, nullable=True),
|
|
||||||
Column("u_einwilligung_ansprechpartner", sql.Boolean, nullable=True),
|
|
||||||
Column("u_aktiv", sql.Boolean, nullable=False, default=1),
|
|
||||||
)
|
|
||||||
|
|
||||||
ext_kl_unternehmen_schema: t.PolarsSchema = {
|
|
||||||
"u_id": pl.UInt64,
|
|
||||||
"u_zeitstempel_eintrag": pl.Datetime,
|
|
||||||
"u_rechtsform": pl.String,
|
|
||||||
"u_firmenname": pl.String,
|
|
||||||
"u_strasse": pl.String,
|
|
||||||
"u_hausnummer": pl.String,
|
|
||||||
"u_adresszusatz": pl.String,
|
|
||||||
"u_plz": pl.String,
|
|
||||||
"u_ort": pl.String,
|
|
||||||
"u_postfach": pl.String,
|
|
||||||
"u_website": pl.String,
|
|
||||||
"u_anrede": pl.String,
|
|
||||||
"u_titel": pl.String,
|
|
||||||
"u_vorname": pl.String,
|
|
||||||
"u_nachname": pl.String,
|
|
||||||
"u_funktion": pl.String,
|
|
||||||
"u_mail": pl.String,
|
|
||||||
"u_telefon": pl.String,
|
|
||||||
"u_plz_postfach": pl.String,
|
|
||||||
"u_einwilligung_inhaber": pl.Boolean,
|
|
||||||
"u_einwilligung_ansprechpartner": pl.Boolean,
|
|
||||||
"u_aktiv": pl.Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_ext_kontaktliste(
|
|
||||||
db_path: Path | None,
|
|
||||||
) -> pl.DataFrame:
|
|
||||||
if db_path is None:
|
|
||||||
ENV_PTH = os.environ.get("DOPT_DB_KONTAKTLISTE", None)
|
|
||||||
if ENV_PTH is None:
|
|
||||||
raise ValueError("No database path provided or found as ENV var.")
|
|
||||||
db_path = Path(ENV_PTH)
|
|
||||||
|
|
||||||
if not db_path.exists():
|
|
||||||
raise FileNotFoundError(f"Database not found under >{db_path}<")
|
|
||||||
|
|
||||||
engine = sql.create_engine(f"sqlite:///{db_path}")
|
|
||||||
stmt = sql.select(ext_kl_unternehmen)
|
|
||||||
return pl.read_database(stmt, engine, schema_overrides=ext_kl_unternehmen_schema)
|
|
||||||
|
|
||||||
|
|
||||||
df_kontaktliste = get_ext_kontaktliste(None)
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
ext_crm_master: sql.Table = Table(
|
|
||||||
"Master",
|
|
||||||
md_crm,
|
|
||||||
Column("ma_id", sql.Integer, nullable=False, unique=True),
|
|
||||||
Column("wce_id", sql.ForeignKey("Nutzer.wce_id")),
|
|
||||||
Column("ma_unternehmensname", sql.Text, nullable=True),
|
|
||||||
Column("ma_branche", sql.Text, nullable=True),
|
|
||||||
Column("ma_strasse", sql.Text, nullable=True),
|
|
||||||
Column("ma_hausnummer", sql.Text, nullable=True),
|
|
||||||
Column("ma_plz", sql.Text, nullable=True),
|
|
||||||
Column("ma_ort", sql.Text, nullable=True),
|
|
||||||
Column("ma_plz_postfach", sql.Text, nullable=True),
|
|
||||||
Column("ma_postfach", sql.Text, nullable=True),
|
|
||||||
Column("ma_website", sql.Text, nullable=True),
|
|
||||||
Column("ma_mail", sql.Text, nullable=True),
|
|
||||||
Column("ma_telefonnummer", sql.Text, nullable=True),
|
|
||||||
Column("ma_faxnummer", sql.Text, nullable=True),
|
|
||||||
Column("ma_ersteintrag_datum", SafeDateTime, nullable=True),
|
|
||||||
Column("ma_aktualisierung_datum", SafeDateTime, nullable=True),
|
|
||||||
Column("ma_aktualisierung_nutzer", sql.Text, nullable=True),
|
|
||||||
Column("ma_sollprozess", sql.Text, nullable=True),
|
|
||||||
Column("ma_auslaendische_mitarbeiter", sql.Text, nullable=True),
|
|
||||||
Column("ma_quelle_information", sql.Text, nullable=True),
|
|
||||||
Column("ma_bemerkung", sql.Text, nullable=True),
|
|
||||||
Column("ma_kontakt", sql.Boolean, nullable=True),
|
|
||||||
Column("ma_schlagworte", sql.Text, nullable=True),
|
|
||||||
Column("ma_archiviert", sql.Boolean, nullable=True, default=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ext_crm_master_schema: t.PolarsSchema = {
|
|
||||||
"ma_id": pl.UInt64,
|
|
||||||
"wce_id": pl.UInt64,
|
|
||||||
"ma_unternehmensname": pl.String,
|
|
||||||
"ma_branche": pl.String,
|
|
||||||
"ma_strasse": pl.String,
|
|
||||||
"ma_hausnummer": pl.String,
|
|
||||||
"ma_plz": pl.String,
|
|
||||||
"ma_ort": pl.String,
|
|
||||||
"ma_plz_postfach": pl.String,
|
|
||||||
"ma_postfach": pl.String,
|
|
||||||
"ma_website": pl.String,
|
|
||||||
"ma_mail": pl.String,
|
|
||||||
"ma_telefonnummer": pl.String,
|
|
||||||
"ma_faxnummer": pl.String,
|
|
||||||
"ma_ersteintrag_datum": pl.Datetime,
|
|
||||||
"ma_aktualisierung_datum": pl.Datetime,
|
|
||||||
"ma_aktualisierung_nutzer": pl.String,
|
|
||||||
"ma_sollprozess": pl.String,
|
|
||||||
"ma_auslaendische_mitarbeiter": pl.String,
|
|
||||||
"ma_quelle_information": pl.String,
|
|
||||||
"ma_bemerkung": pl.String,
|
|
||||||
"ma_kontakt": pl.Boolean,
|
|
||||||
"ma_schlagworte": pl.String,
|
|
||||||
"ma_archiviert": pl.Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_ext_crm_master(
|
|
||||||
db_path: Path | None,
|
|
||||||
) -> pl.DataFrame:
|
|
||||||
if db_path is None:
|
|
||||||
ENV_PTH = os.environ.get("DOPT_DB_CRM", None)
|
|
||||||
if ENV_PTH is None:
|
|
||||||
raise ValueError("No database path provided or found as ENV var.")
|
|
||||||
db_path = Path(ENV_PTH)
|
|
||||||
|
|
||||||
if not db_path.exists():
|
|
||||||
raise FileNotFoundError(f"Database not found under >{db_path}<")
|
|
||||||
|
|
||||||
engine = sql.create_engine(f"sqlite:///{db_path}")
|
|
||||||
stmt = sql.select(ext_crm_master)
|
|
||||||
return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema)
|
|
||||||
|
|
||||||
|
|
||||||
df_crm_master = get_ext_crm_master(None)
|
|
||||||
|
|
||||||
ext_crm_nutzer: sql.Table = Table(
|
|
||||||
"Nutzer",
|
|
||||||
md_crm,
|
|
||||||
Column("wce_id", sql.Integer, nullable=False, unique=True),
|
|
||||||
Column("wce_name", sql.Text, nullable=True),
|
|
||||||
Column("wce_vorname", sql.Text, nullable=True),
|
|
||||||
Column("wce_kuerzel", sql.Text, nullable=True),
|
|
||||||
Column("wce_passwort", sql.Text, nullable=True),
|
|
||||||
Column("wce_angelegt_am", sql.DateTime, nullable=True),
|
|
||||||
Column("wce_rolle", sql.Text, nullable=True),
|
|
||||||
Column("wce_angelegt_von", sql.Text, nullable=True),
|
|
||||||
Column("wce_aktiv", sql.Boolean, nullable=True),
|
|
||||||
Column("wce_letzter_login", sql.DateTime, nullable=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
ext_crm_nutzer_schema: t.PolarsSchema = {
|
|
||||||
"wce_id": pl.UInt64,
|
|
||||||
"wce_name": pl.String,
|
|
||||||
"wce_vorname": pl.String,
|
|
||||||
"wce_kuerzel": pl.String,
|
|
||||||
"wce_passwort": pl.String,
|
|
||||||
"wce_angelegt_am": pl.Datetime,
|
|
||||||
"wce_rolle": pl.String,
|
|
||||||
"wce_angelegt_von": pl.String,
|
|
||||||
"wce_aktiv": pl.Boolean,
|
|
||||||
"wce_letzter_login": pl.Datetime,
|
|
||||||
}
|
|
||||||
|
|
||||||
ext_crm_contact_person: sql.Table = Table(
|
|
||||||
"Ansprechpartner",
|
|
||||||
md_crm,
|
|
||||||
Column("an_id", sql.Integer, nullable=False, unique=True),
|
|
||||||
Column("ma_id", sql.ForeignKey("Master.ma_id")),
|
|
||||||
Column("wce_id", sql.ForeignKey("Nutzer.wce_id")),
|
|
||||||
Column("st_id", sql.Integer, nullable=False),
|
|
||||||
Column("an_sachgebiet", sql.Text, nullable=True),
|
|
||||||
Column("an_anrede", sql.Text, nullable=True),
|
|
||||||
Column("an_titel", sql.Text, nullable=True),
|
|
||||||
Column("an_nachname", sql.Text, nullable=True),
|
|
||||||
Column("an_vorname", sql.Text, nullable=True),
|
|
||||||
Column("an_position", sql.Text, nullable=True),
|
|
||||||
Column("an_mail", sql.Text, nullable=True),
|
|
||||||
Column("an_festnetz", sql.Text, nullable=True),
|
|
||||||
Column("an_mobil", sql.Text, nullable=True),
|
|
||||||
Column("an_faxnummer", sql.Text, nullable=True),
|
|
||||||
Column("an_hauptansprechpartner", sql.Text, nullable=True),
|
|
||||||
Column("an_anrede_anschrift", sql.Text, nullable=True),
|
|
||||||
Column("an_bemerkung", sql.Text, nullable=True),
|
|
||||||
Column("an_aktualisierung_datum", SafeDateTime, nullable=True),
|
|
||||||
Column("an_aktualisierung_nutzer", sql.Text, nullable=True),
|
|
||||||
Column("an_letztes_kontaktdatum", SafeDateTime, nullable=True),
|
|
||||||
Column("an_ersteintrag_datum", SafeDateTime, nullable=True),
|
|
||||||
Column("an_archiviert", sql.Boolean, nullable=True, default=0),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ext_crm_contact_person_schema: t.PolarsSchema = {
|
|
||||||
"an_id": pl.UInt64,
|
|
||||||
"ma_id": pl.UInt64,
|
|
||||||
"wce_id": pl.UInt64,
|
|
||||||
"st_id": pl.UInt64,
|
|
||||||
"an_sachgebiet": pl.String,
|
|
||||||
"an_anrede": pl.String,
|
|
||||||
"an_titel": pl.String,
|
|
||||||
"an_nachname": pl.String,
|
|
||||||
"an_vorname": pl.String,
|
|
||||||
"an_position": pl.String,
|
|
||||||
"an_mail": pl.String,
|
|
||||||
"an_festnetz": pl.String,
|
|
||||||
"an_mobil": pl.String,
|
|
||||||
"an_faxnummer": pl.String,
|
|
||||||
"an_hauptansprechpartner": pl.String,
|
|
||||||
"an_anrede_anschrift": pl.String,
|
|
||||||
"an_bemerkung": pl.String,
|
|
||||||
"an_aktualisierung_datum": pl.Datetime,
|
|
||||||
"an_aktualisierung_nutzer": pl.String,
|
|
||||||
"an_letztes_kontaktdatum": pl.Datetime,
|
|
||||||
"an_ersteintrag_datum": pl.Datetime,
|
|
||||||
"an_archiviert": pl.Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_ext_crm_contact_person(
|
|
||||||
db_path: Path | None,
|
|
||||||
) -> pl.DataFrame:
|
|
||||||
if db_path is None:
|
|
||||||
ENV_PTH = os.environ.get("DOPT_DB_CRM", None)
|
|
||||||
if ENV_PTH is None:
|
|
||||||
raise ValueError("No database path provided or found as ENV var.")
|
|
||||||
db_path = Path(ENV_PTH)
|
|
||||||
|
|
||||||
if not db_path.exists():
|
|
||||||
raise FileNotFoundError(f"Database not found under >{db_path}<")
|
|
||||||
|
|
||||||
engine = sql.create_engine(f"sqlite:///{db_path}")
|
|
||||||
stmt = sql.select(ext_crm_contact_person)
|
|
||||||
df = pl.read_database(stmt, engine, schema_overrides=ext_crm_contact_person_schema)
|
|
||||||
# TODO Database seems to contain entries with invalid characters like \t or \n
|
|
||||||
df = df.with_columns(
|
|
||||||
pl.col(pl.String).str.replace_all(r"[\r\t\n]", " ").str.strip_chars(" ")
|
|
||||||
)
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
df_contact_person = get_ext_crm_contact_person(None)
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).parents[2]
|
|
||||||
DB_PATH = PROJECT_ROOT / "data/db"
|
|
||||||
|
|
||||||
DB_KONTAKTLISTE = DB_PATH / "wce_kontaktliste.db"
|
|
||||||
assert DB_KONTAKTLISTE.exists()
|
|
||||||
DB_CRM = DB_PATH / "wce_crm.db"
|
|
||||||
assert DB_CRM.exists()
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
|
||||||
os.environ["DOPT_DB_KONTAKTLISTE"] = str(DB_KONTAKTLISTE)
|
|
||||||
os.environ["DOPT_DB_CRM"] = str(DB_CRM)
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
DOPT_DB_KONTAKTLISTE: Pfad zur Datenbank der Kontaktliste, falls nicht direkt übergeben (Prototypenphase)
|
|
||||||
DOPT_DB_CRM: Pfad zur CRM-Datenbank, falls nicht direkt übergeben (Prototypenphase)
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, TypeAlias
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
import polars as pl
|
|
||||||
|
|
||||||
|
|
||||||
PolarsSchema: TypeAlias = dict[str, type["pl.DataType"]]
|
|
||||||
Loading…
x
Reference in New Issue
Block a user