further prototyping, added first DB interactions

This commit is contained in:
2026-04-23 15:57:39 +02:00
parent e4ebb1ee7f
commit c5aadd502d
12 changed files with 1196 additions and 283 deletions

142
prototypes/db_access.py Normal file
View File

@@ -0,0 +1,142 @@
# %%
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 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

View File

@@ -1,24 +1,30 @@
from __future__ import annotations
import dataclasses as dc
import enum
import sys
import time
from collections.abc import Sequence
from PySide6.QtCore import Qt, Signal # Signal ist wichtig!
from PySide6.QtCore import QDate, Qt, QTimer, Signal # Signal ist wichtig!
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QCompleter,
QDateEdit,
QDialog,
QDialogButtonBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QMainWindow,
QMessageBox,
QPlainTextEdit,
QPushButton,
QScrollArea,
@@ -28,6 +34,22 @@ from PySide6.QtWidgets import (
QWidget,
)
from wce_crm.backend import initial_recording as be_init_rec
QSS = """
*[styleClass="stempel"] {
background-color: #f1f5f9;
color: #333D4B;
border: 1px dashed #cbd5e1;
border-radius: 4px;
padding: 5px;
}
*[styleClass="stempel"]:focus {
border: 1px dashed #cbd5e1;
}
"""
@dc.dataclass(slots=True)
class Address:
@@ -134,16 +156,24 @@ class AddressForm_Search(QWidget):
super().__init__()
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
form_layout = QFormLayout()
form_layout.setSpacing(15)
form_layout.setSpacing(10)
# title
title = QLabel("--- Suche Unternehmen ---")
title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold;
main_layout.addWidget(title)
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)
# search_data = [addr.name for addr in ADDRESSES]
# self.SEARCH_MAP = {addr.name: addr for addr in ADDRESSES}
self.SEARCH_MAP = be_init_rec.comp_search_choice_mapping()
self.search_data = tuple(self.SEARCH_MAP.keys())
self.completer = QCompleter(self.search_data)
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.completer.setFilterMode(Qt.MatchFlag.MatchContains)
self.search_input.setCompleter(self.completer)
self.completer.activated.connect(self.search_result_selected)
@@ -207,15 +237,23 @@ class AddressForm_Search(QWidget):
}
""")
def fill_out(self, address: Address):
addr_ = address.export()
def fill_out(self, comp_info: be_init_rec.CompanyInfo):
# addr_ = address.export()
for field, value in zip(self.autofilled_fields, addr_.values()):
field.setText(value)
# for field, value in zip(self.autofilled_fields, addr_.values()):
# field.setText(value)
self.company_input.setText(comp_info["ma_unternehmensname"])
self.street_input.setText(comp_info["ma_strasse"])
self.number_input.setText(comp_info["ma_hausnummer"])
self.zip_input.setText(comp_info["ma_plz"])
self.city_input.setText(comp_info["ma_ort"])
def search_result_selected(self, name):
address = self.SEARCH_MAP[name]
self.fill_out(address)
comp_info = be_init_rec.comp_search_get_info(
ma_id=self.SEARCH_MAP[name],
)
self.fill_out(comp_info)
class DropdownSearch(QWidget):
@@ -242,11 +280,11 @@ class DropdownSearch(QWidget):
# --- WICHTIGE EINSTELLUNGEN ---
# Ignoriert Groß-/Kleinschreibung (sehr wichtig für eine gute Suche!)
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
# 'MatchContains' sorgt dafür, dass "pha" auch "Projekt Alpha" findet.
# Standard ist 'MatchStartsWith' (findet nur Worte am Anfang).
self.completer.setFilterMode(Qt.MatchContains)
self.completer.setFilterMode(Qt.MatchFlag.MatchContains)
# 4. Den Completer an das Eingabefeld binden
self.search_input.setCompleter(self.completer)
@@ -261,78 +299,405 @@ class DropdownSearch(QWidget):
# Hier könntest du z.B. deine Detail-Seite für dieses Projekt öffnen
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
placeholder: str | None = None
fill_value: str | None = None
readonly: bool = False
def __post_init__(self) -> None:
self.label = self.label.strip()
if not self.label.endswith(":"):
self.label += ":"
if self.required:
self.label += "*"
@dc.dataclass(slots=True)
class FormFieldGroup:
key: str
label: str
fields: Sequence[FormField]
FORM_FIELD_DEF = [
FormField("name", "Projektname", FormFieldType.TEXT, True, "Bitte füllen..."),
FormField("descr", "Beschreibung", FormFieldType.LONGTEXT, False),
FormField(
"ext_data",
"Externe Daten",
FormFieldType.TEXT,
True,
fill_value="Lorem ipsum und so weiter...",
readonly=True,
),
FormField("init_date", "Auftragsdatum", FormFieldType.DATE, True),
FormField("start_date", "Startdatum", FormFieldType.DATE, True),
FormField(
"ms_date1",
"MS Datum 1",
FormFieldType.DATE,
False,
"",
fill_value="26.07.2026",
readonly=True,
),
FormField(
"ms_date2",
"MS Datum 2",
FormFieldType.DATE,
False,
"",
fill_value="30.08.2026",
readonly=False,
),
FormField(
"important:notes",
"Wichtige Notizen",
FormFieldType.LONGTEXT,
True,
"Text eingeben...",
),
]
FORM_FIELD_DEF2 = [
FormField("name", "Projektname", FormFieldType.TEXT, True, "Bitte füllen..."),
FormField("descr", "Beschreibung", FormFieldType.LONGTEXT, False),
FormField(
"ext_data",
"Externe Daten",
FormFieldType.TEXT,
True,
fill_value="Lorem ipsum und so weiter...",
readonly=True,
),
FormField("init_date", "Auftragsdatum", FormFieldType.DATE, True),
FormField("start_date", "Startdatum", FormFieldType.DATE, True),
FormField(
"ms_date1",
"MS Datum 1",
FormFieldType.DATE,
False,
"",
fill_value="26.07.2026",
readonly=True,
),
FormField(
"ms_date2",
"MS Datum 2",
FormFieldType.DATE,
False,
"",
fill_value="30.08.2026",
readonly=False,
),
FormField(
"important:notes",
"Wichtige Notizen",
FormFieldType.LONGTEXT,
True,
"Text eingeben...",
),
]
FORM_FIELD_GROUPS = [
FormFieldGroup("group1", "Test-1", FORM_FIELD_DEF),
FormFieldGroup("group2", "Test-2", FORM_FIELD_DEF2),
]
class MyForm(QWidget):
def __init__(self):
def __init__(
self,
form_field_groups: Sequence[FormFieldGroup],
add_buttons: bool = True,
) -> None:
super().__init__()
# --- LAYOUT ---
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_field_groups = form_field_groups
# 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
for fg in form_field_groups:
widget = MyFormPart(fg.fields, fg.label, add_buttons=False)
self.main_layout.addWidget(widget)
# 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"},
]
# buttons
# self.add_buttons = add_buttons
# if self.add_buttons:
# self.layout_btn = QHBoxLayout()
# self.main_layout.addLayout(self.layout_btn)
# self.save_btn_txt_enabled = "Speichern (Strg + S)"
# self.save_btn_txt_disabled = "Wird gespeichert..."
# self.save_btn = QPushButton(self.save_btn_txt_enabled)
# self.save_btn.setShortcut("Ctrl+S")
# self.save_btn.setFixedHeight(50)
# self.save_btn.setSizePolicy(
# QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
# )
# self.save_btn.clicked.connect(self.on_save_clicked)
# self.layout_btn.addWidget(self.save_btn)
# self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
# self.reset_btn.setShortcut("Ctrl+Z")
# self.reset_btn.setFixedHeight(50)
# self.reset_btn.setSizePolicy(
# QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
# )
# self.reset_btn.clicked.connect(self.reset_form)
# self.layout_btn.addWidget(self.reset_btn)
# Dictionary, um die erstellten Widgets zu speichern (für späteren Zugriff)
self.widgets = {}
# Automatischer Aufbau des Formulars
class MyFormPart(QWidget):
def __init__(
self,
field_definitons: Sequence[FormField],
group_name: str | None = None,
add_buttons: bool = False,
):
super().__init__()
self.setStyleSheet("""
QGroupBox {
font-size: 16px;
font-weight: bold;
border: 1px solid #cbd5e1; /* Heller, moderner Rahmen */
border-radius: 8px; /* Abgerundete Ecken */
margin-top: 15px; /* Platz für die Überschrift schaffen */
padding-top: 15px; /* Abstand zwischen Rahmen und erstem Feld */
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left; /* Überschrift oben links */
padding: 0 5px; /* Etwas Luft links und rechts vom Text */
color: #334155; /* Dunkelgraue Schrift */
}
""")
# --- LAYOUT ---
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.group_box: QGroupBox | None = None
self.group_name = group_name
if self.group_name:
self.group_box = QGroupBox(group_name)
self.main_layout.addWidget(self.group_box)
self.group_box.setLayout(self.form_layout)
else:
self.main_layout.addLayout(self.form_layout)
self.form_layout.setSpacing(10) # Abstand zwischen den Zeilen
self.field_definitions = field_definitons
self.widgets: dict[str, QWidget] = {}
# automatic build
self.create_form_fields()
def create_form_fields(self):
# buttons
self.add_buttons = add_buttons
if self.add_buttons:
self.layout_btn = QHBoxLayout()
self.main_layout.addLayout(self.layout_btn)
self.save_btn_txt_enabled = "Speichern (Strg + S)"
self.save_btn_txt_disabled = "Wird gespeichert..."
self.save_btn = QPushButton(self.save_btn_txt_enabled)
self.save_btn.setShortcut("Ctrl+S")
self.save_btn.setFixedHeight(50)
self.save_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.save_btn.clicked.connect(self.on_save_clicked)
self.layout_btn.addWidget(self.save_btn)
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
self.reset_btn.setShortcut("Ctrl+Z")
self.reset_btn.setFixedHeight(50)
self.reset_btn.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.reset_btn.clicked.connect(self.reset_form)
self.layout_btn.addWidget(self.reset_btn)
def create_form_fields(self) -> None:
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
match field.type:
case FormFieldType.TEXT:
widget = QLineEdit()
if field.placeholder:
widget.setPlaceholderText(field.placeholder)
if field.fill_value:
widget.setText(field.fill_value)
if field.readonly:
widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
elif field["type"] == "longtext":
widget = QPlainTextEdit()
widget.setMaximumHeight(80) # Kompakte Höhe für Formulare
case FormFieldType.LONGTEXT:
widget = QPlainTextEdit()
widget.setMaximumHeight(80) # Kompakte Höhe für Formulare
if field.placeholder:
widget.setPlaceholderText(field.placeholder)
if field.readonly:
widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
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;")
case FormFieldType.DATE:
widget = QDateEdit() # Oder QDateEdit
widget.setCalendarPopup(True)
cal = widget.calendarWidget()
if cal:
cal.setFirstDayOfWeek(Qt.DayOfWeek.Monday)
cal.setGridVisible(True)
cal.setStyleSheet("""
QCalendarWidget QAbstractItemView {
selection-background-color: #2980b9;
selection-color: white;
}
""")
if field.fill_value:
set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy")
if not set_date.isValid():
raise ValueError(
f"Could not parse date field value >{field.fill_value}<"
)
widget.setDate(set_date)
else:
widget.setDate(QDate.currentDate())
if field.readonly:
widget = QLineEdit()
widget.setReadOnly(True) # Falls es ein Fixwert ist
widget.setProperty("styleClass", "stempel")
if field.fill_value:
widget.setText(field.fill_value)
case _:
raise NotImplementedError(f"Not supported field type: {field.type.value}")
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)
self.widgets[field.key] = widget
self.form_layout.addRow(field.label, widget)
def get_form_data(self):
def get_form_data(self) -> ...:
"""Liest alle Felder automatisch aus"""
data = {}
for key, widget in self.widgets.items():
if isinstance(widget, QLineEdit):
if isinstance(widget, (QLineEdit, QDateEdit)):
data[key] = widget.text()
elif isinstance(widget, QPlainTextEdit):
data[key] = widget.toPlainText()
return data
def _disable_save(self) -> None:
self.save_btn.setEnabled(False)
self.save_btn.setText(self.save_btn_txt_disabled)
def _enable_save(
self,
timeout: int = 3000,
) -> None:
QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
self.save_btn.setText(self.save_btn_txt_enabled)
def on_save_clicked(self) -> None:
self._disable_save()
errors = [] # Hier sammeln wir die Namen der fehlenden Felder
# time.sleep(0.5)
# Wir gehen unsere Feld-Definitionen durch
for field in self.field_definitions:
widget = self.widgets[field.key]
# 1. Zuerst setzen wir das Design des Feldes wieder auf "Normal" zurück.
# Falls der Nutzer den Fehler vorher schon korrigiert hat, muss der rote Rand weg!
if not field.readonly:
widget.setStyleSheet("")
# 2. Ist es überhaupt ein Pflichtfeld?
if not field.required:
continue
is_empty = False
if isinstance(widget, (QLineEdit, QDateEdit)):
if not widget.text().strip():
is_empty = True
elif isinstance(widget, QPlainTextEdit):
if not widget.toPlainText().strip():
is_empty = True
if not is_empty:
continue
errors.append(
field.label.replace("*", "").replace(":", "")
) # Sternchen für die Fehlermeldung entfernen
# Optisches Feedback: Heller roter Hintergrund und roter Rand
widget.setStyleSheet("""
border: 1px solid #ef4444;
background-color: #ffe9e9;
padding: 4px;
border-radius: 4px;
""")
# --- ERGEBNIS AUSWERTEN ---
if errors:
# Es gibt Fehler! Speichern abbrechen und Pop-up anzeigen.
error_text = "Bitte fülle die folgenden Pflichtfelder aus:\n\n- " + "\n- ".join(
errors
)
QMessageBox.warning(self, "Fehlende Angaben", error_text)
self._enable_save()
return
# Wenn wir hier ankommen, ist die Liste 'errors' leer. Alles ist korrekt ausgefüllt!
# time.sleep(0.5)
print("Erfolg! Alle Daten sind valide.")
self.reset_form()
self._enable_save()
def reset_form(self) -> None:
for field in self.field_definitions:
widget = self.widgets[field.key]
if field.readonly:
continue
if isinstance(widget, QLineEdit):
widget.clear()
if field.fill_value:
widget.setText(field.fill_value)
elif isinstance(widget, QPlainTextEdit):
widget.clear()
elif isinstance(widget, QDateEdit):
if field.fill_value:
set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy")
if not set_date.isValid():
raise ValueError(
f"Could not parse date field value >{field.fill_value}<"
)
widget.setDate(set_date)
else:
widget.setDate(QDate.currentDate())
widget.setStyleSheet("")
class ClickableCell(QFrame):
# Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt
@@ -355,11 +720,11 @@ class ClickableCell(QFrame):
layout = QVBoxLayout(self)
label = QLabel(text)
label.setWordWrap(True)
label.setAlignment(Qt.AlignCenter)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(label)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if event.button() == Qt.MouseButton.LeftButton:
# Wenn geklickt wird, senden wir die Daten aus
self.clicked.emit(self.data_record)
@@ -369,7 +734,7 @@ class HeaderCell(QLabel):
super().__init__(text)
# Textausrichtung zentrieren
self.setAlignment(Qt.AlignCenter)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet
self.setStyleSheet("""
@@ -407,7 +772,9 @@ class NewEntryDialog(QDialog):
layout.addRow("Datum:", self.input_date)
# Standard-Buttons (OK und Abbrechen)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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)
@@ -431,7 +798,7 @@ class DetailView(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
# Zurück-Button
back_btn = QPushButton("← Zurück zur Tabelle")
@@ -470,7 +837,7 @@ class NewEntrySelect_view(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
# Zurück-Button
back_btn = QPushButton("← Zurück zur Übersicht")
@@ -524,38 +891,65 @@ class SearchFormPage(QWidget):
def __init__(self):
super().__init__()
# Hauptlayout der Seite
main_layout = QVBoxLayout(self)
outer_layout = QHBoxLayout(self)
vert_layout = QVBoxLayout()
# main_layout.setContentsMargins(0, 0, 0, 0)
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.AlignmentFlag.AlignTop)
# --- 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)
# --- HEADER ---
header_container = QWidget()
header_container.setMinimumWidth(700)
header_container.setMaximumWidth(1000)
header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 10)
back_btn_main = QPushButton("← Zurück zur Übersicht")
back_btn_main.clicked.connect(lambda: self.back_main_requested.emit())
back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
back_btn_main.setMinimumWidth(200)
back_btn_main.setMaximumWidth(200)
back_btn_step = QPushButton("← Zurück")
back_btn_step.clicked.connect(lambda: self.back_requested.emit())
back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
back_btn_step.setMinimumWidth(200)
back_btn_step.setMaximumWidth(200)
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.setSpacing(5)
header_layout.addWidget(back_btn_step)
header_layout.addWidget(back_btn_main)
header_layout.addWidget(title)
# header_layout.addStretch() # Drückt den Titel nach links
main_layout.addLayout(header_layout)
vert_layout.addWidget(header_container)
# --- HAUPTINHALT ---
container = QWidget()
# 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(1000)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setWidget(container)
# vert_layout.addSpacing(20)
vert_layout.addWidget(scroll_area)
# --- KOPF Metadaten ---
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
# --- KOPF Unternehmen ---
inf_block_1 = QHBoxLayout()
inhalte = [
"Fall-Nr.:",
@@ -565,7 +959,7 @@ class SearchFormPage(QWidget):
]
for entry in inhalte:
label = QLabel(entry)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
field = QLineEdit(placeholderText="...")
field.setText("22.04.2026")
field.setReadOnly(True)
@@ -582,107 +976,47 @@ class SearchFormPage(QWidget):
border: 1px #cbd5e1;
}
""")
field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
inf_block_1.addWidget(label)
inf_block_1.addWidget(field)
inf_block_1.addStretch()
main_layout.addLayout(inf_block_1)
container_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)
label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.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)
container_layout.addLayout(inf_block_2)
container_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()
# --- Test Formularlayout ---
container_layout.addSpacing(30)
title = QLabel("--- Automatische Form ---")
title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold;
container_layout.addWidget(title)
# container_layout.addWidget(MyFormPart(FORM_FIELD_DEF, "Test-Gruppe"))
container_layout.addWidget(MyForm(FORM_FIELD_GROUPS))
# 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)
container_layout.addSpacing(30)
# --- SUCHE MIT NAMEN ---
# 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)
container_layout.addWidget(addr_widget)
main_layout.addSpacing(30)
container_layout.addSpacing(30)
main_layout.addWidget(DropdownSearch())
# container_layout.addWidget(DropdownSearch())
main_layout.addSpacing(30)
container_layout.addSpacing(30)
# --- 2. SUCH-FORMULAR ---
# --- SUCH-FORMULAR ---
form_layout = (
QHBoxLayout()
) # Horizontal, damit Suchen und Filtern nebeneinander stehen
@@ -702,12 +1036,12 @@ class SearchFormPage(QWidget):
form_layout.addWidget(QLabel("Status:"))
form_layout.addWidget(self.status_filter, stretch=1)
main_layout.addLayout(form_layout)
container_layout.addLayout(form_layout)
# --- 3. ERGEBNIS-BEREICH ---
# Für den Anfang ein einfaches Listen-Widget
self.results_list = QListWidget()
main_layout.addWidget(
container_layout.addWidget(
self.results_list, stretch=100
) # Nimmt den restlichen Platz ein
@@ -740,7 +1074,7 @@ class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Master")
self.resize(1800, 200)
self.resize(1800, 1000)
# --- 1. DAS MENÜ ERSTELLEN ---
self.create_menu()
@@ -801,9 +1135,9 @@ class MainWindow(QMainWindow):
scroll_area.setMaximumWidth(
1500
) # Die Breiten-Begrenzung wandert nun auf die ScrollArea
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht
scroll_area.setFrameShape(QFrame.NoFrame)
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setWidget(container)
vert_layout.addWidget(new_btn)
@@ -817,7 +1151,7 @@ class MainWindow(QMainWindow):
# outer_layout.addWidget(scroll_area, stretch=100)
outer_layout.addStretch(1)
# Optional: Damit der Container oben am Rand klebt
outer_layout.setAlignment(Qt.AlignTop)
outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Wir geben dem Container ein vertikales Layout
container_layout = QVBoxLayout(container)
@@ -961,6 +1295,7 @@ class MainWindow(QMainWindow):
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyleSheet(QSS)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@@ -1,28 +1,45 @@
# %%
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 Address:
street: str
number: int
postal_code: str
city: str
class FormField:
key: str
label: str
type: FormFieldType
required: bool
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
def __post_init__(self) -> None:
self.label = self.label.strip()
if not self.label.endswith(":"):
self.label += ":"
if self.required:
self.label += "*"
# %%
addr = Address("Teststraße", 202, "09111", "Chemnitz")
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