prepare recursive UI build

This commit is contained in:
2026-05-11 16:28:20 +02:00
parent c3462d3d3a
commit 2c072e2252
3 changed files with 620 additions and 72 deletions

View File

@@ -428,10 +428,16 @@ class FormField:
raise ValueError("Invalid field definition: Dropdown requires options")
@dc.dataclass(slots=True)
class FormFieldDynList:
label: str
fields: Sequence[FormField]
@dc.dataclass(slots=True)
class FormFieldGroup:
label: str | None
fields: Sequence[FormField]
fields: Sequence[FormField] | FormFieldDynList
FORM_FIELD_DEF = [
@@ -594,6 +600,400 @@ FORM_FIELDS_CONTACT_PERSON = [
),
]
FORM_FIELDS_MASTER_DATA = [
FormField(
"Titel",
FormFieldType.TEXT,
required=False,
tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person "
"nicht die zu beratende Person ist"
),
),
FormField(
"Anrede",
FormFieldType.TEXT,
required=True,
),
FormField(
"Name",
FormFieldType.TEXT,
required=True,
),
FormField(
"Vorname",
FormFieldType.TEXT,
required=False,
),
FormField(
"Geburtsdatum",
FormFieldType.DATE,
required=False,
tooltip=(
"* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt "
"(Regelung bei Ü45 Jahre)"
),
),
FormField(
"Herkunftsland",
FormFieldType.DROPDOWN,
required=True,
options=["LÄNDERLISTE NOCH ZU ERGÄNZEN"],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Staatsangehörigkeit",
FormFieldType.DROPDOWN,
required=False,
options=["LÄNDERLISTE NOCH ZU ERGÄNZEN"],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Rückkehrer",
FormFieldType.DROPDOWN,
required=False,
options=["ja", "nein"],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Wo befindet sich die Person?",
FormFieldType.DROPDOWN,
required=True,
options=["Inland", "Ausland EU/EWR", "Ausland Drittstaat"],
),
FormField(
"Straße",
FormFieldType.TEXT,
required=False,
),
FormField(
"Hausnummer",
FormFieldType.TEXT,
required=False,
),
FormField(
"PLZ",
FormFieldType.TEXT,
required=False,
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
),
FormField(
"Bundesland",
FormFieldType.DROPDOWN,
required=False,
options=["BUNDESLÄNDER NOCH ZU ERGÄNZEN"],
tooltip=(
"nur wenn Inland angegeben und die Angabe zieht es in keine Dokumente "
"rüber! Liste Bundesländer verwenden"
),
),
FormField(
"Land",
FormFieldType.TEXT,
required=False,
),
FormField(
"Festnetznummer",
FormFieldType.TEXT,
required=False,
),
FormField(
"Mobilfunknummer",
FormFieldType.TEXT,
required=False,
),
FormField(
"E-Mail",
FormFieldType.TEXT,
required=False,
),
FormField(
"Familienstand",
FormFieldType.TEXT,
required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
),
FormField(
"Anzahl Kinder",
FormFieldType.DROPDOWN,
required=False,
options=[str(x) for x in range(11)],
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
),
]
FORM_FIELDS_ADDITIONAL_DATA = [
FormField(
"Deutsch als Kommunikationssprache",
FormFieldType.DROPDOWN,
required=False,
options=["nein", "ja, als Muttersprache", "ja, als Fremdsprache"],
),
FormField(
"Aufenthaltstitel",
FormFieldType.DROPDOWN,
required=False,
options=[
"anerkannter Flüchtling §§ 22 - 26 AufenthG",
"Aufenthaltsgestattung §55 AufenthG",
"Blaue Karte EU § 18g AufenthG",
"BüMA (Bescheinigung über Meldung als Asylsuchender)",
"Duldung § 60 AufenthG",
"bisher kein Aufenthaltstitel",
"Deutscher",
"familiäre Gründe §§ 27 - 36 AufenthG",
"Niederlassungserlaubnis §9 AufenthG",
"Staatsbürger EUR/EWR/CH",
"Aufenthalt für Ausbildung §§ 16 - 17 AufenthG",
"Aufenthalt für Erwerbstätigkeit §§ 18- 21 AufenthG",
"Chancenaufenthaltsrecht §104c AufenthG",
"Sonstiges",
],
tooltip="sofern nicht bekannt, unbedingt einfordern",
),
FormField(
"Gültigkeit Aufenthaltsstatus",
FormFieldType.DATE,
required=False,
),
FormField(
"Arbeitsstatus aktuell",
FormFieldType.DROPDOWN,
required=False,
options=[
"Arbeitslos",
"Ausbildung/Qualifizierung Inland",
"geringfügig beschäftigt",
"in Anstellung Inland",
"selbstständig Inland",
"Ausbildung/Qualifizierung Ausland",
"in Anstellung Ausland",
"selbstständig Ausland",
],
),
FormField(
"Gemeldet bei Institutionen ",
FormFieldType.DROPDOWN,
required=False,
options=[
"bei keiner",
"Jobcenter mit Leistungsbezug",
"Jobcenter ohne Leistungsbezug",
"Sozialamt mit Leistungsbezug",
"Sozialamt ohne Leistungsbezug",
"Agentur für Arbeit mit Leistungsbezug",
"Agentur für Arbeit ohne Leistungsbezug",
],
),
]
FORM_FIELDS_SCHOOL = [
FormField(
"Abschluss",
FormFieldType.TEXT,
required=False,
),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
),
FormField(
"Schule",
FormFieldType.TEXT,
required=False,
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
),
FormField(
"Land",
FormFieldType.DROPDOWN,
required=False,
options=["LÄNDERLISTE ERGÄNZEN"],
),
FormField(
"Abschlussjahr",
FormFieldType.TEXT,
required=False,
),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
),
]
FORM_FIELDS_HIGHER_EDUCATION = [
FormField(
"Anerkennung",
FormFieldType.TEXT,
required=False,
),
FormField(
"Abschlussgrad",
FormFieldType.TEXT,
required=False,
tooltip=(
"bitte den Titel eingeben z.B. Doktor, Diplom oder "
"Betriebswirt (Fachschulabschluss)"
),
),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
),
FormField(
"Hochschule / Ausbildungsbetrieb / Berufsschule",
FormFieldType.TEXT,
required=False,
),
FormField(
"Beruf / Fachrichtung",
FormFieldType.TEXT,
required=False,
tooltip=(
"bitte spezifizieren z.B. Allgemeinmedizin, Ingenieur Maschinenbau, "
"technischer Betriebswirt Datenverarbeitung"
),
),
FormField(
"Land",
FormFieldType.DROPDOWN,
required=False,
options=["LÄNDERLISTE ERGÄNZEN"],
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
),
FormField(
"Abschlussjahr",
FormFieldType.TEXT,
required=False,
),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
tooltip="z.B. Promotionen oder den Studiengang angeben",
),
]
FORM_FIELDS_WORK_EXPERIENCE = [
FormField(
"Branche",
FormFieldType.DROPDOWN,
required=False,
options=["DROPDOWN-LISTE AN ANDERER STELLE DEFINIERT"],
),
FormField(
"Berufsbezeichnung/Tätigkeit",
FormFieldType.TEXT,
required=False,
),
FormField(
"Funktion",
FormFieldType.DROPDOWN,
required=False,
options=[
"Auszubildender",
"Fachkraft",
"Hilfskraft",
"Akademiker",
"Führungskraft",
"Praktikant",
"FSJ/BFD",
"Elternzeit",
"Sabbatical",
"Sonstiges",
],
),
FormField(
"Unternehmen",
FormFieldType.TEXT,
required=False,
),
FormField(
"Land",
FormFieldType.DROPDOWN,
required=False,
options=["LÄNDERLISTE ERGÄNZEN"],
),
FormField(
"Zeitspanne (von ... bis ...)",
FormFieldType.TEXT,
required=False,
),
FormField(
"Beschäftsigungsart",
FormFieldType.DROPDOWN,
required=False,
options=[
"Vollzeit",
"Teilzeit",
"Sonstiges",
],
tooltip="Minijob, Praktikum, Wehrdienst, soziale Dienste",
),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
),
]
FORM_FIELDS_LANGUAGES = [
FormField(
"Sprache",
FormFieldType.TEXT,
required=False,
),
FormField(
"Niveau",
FormFieldType.DROPDOWN,
required=False,
options=[
"A1",
"A2",
"B1",
"B2",
"C1",
"C2",
],
),
FormField(
"Nachweis",
FormFieldType.DROPDOWN,
required=False,
options=[
"vorhanden",
"nicht vorhanden",
],
),
FormField(
"Art des Nachweises (NUR WENN VORHANDEN)",
FormFieldType.TEXT,
required=False,
),
FormField(
"Datum des Nachweises (NUR WENN VORHANDEN)",
FormFieldType.DATE,
required=False,
),
]
FORM_DYN_LIST = FormFieldDynList("Dynamische Liste", FORM_FIELDS_SCHOOL)
FORM_FIELD_GROUPS = [
FormFieldGroup(
"Status && Projektrelevanz",
@@ -608,6 +1008,13 @@ FORM_FIELD_GROUPS = [
],
),
FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON),
FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA),
FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA),
FormFieldGroup("Schule (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_SCHOOL),
FormFieldGroup("Studium/Ausbildung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_HIGHER_EDUCATION),
FormFieldGroup("Arbeitserfahrung (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_WORK_EXPERIENCE),
FormFieldGroup("Sprachkenntnisse (MIT PLUS-ERWEITERUNG)", FORM_FIELDS_LANGUAGES),
FormFieldGroup("Dynamische Liste", FORM_DYN_LIST),
# FormFieldGroup("Test-2", FORM_FIELD_DEF2),
]
@@ -645,9 +1052,14 @@ class AutoForm(QWidget):
self.widgets: dict[str, QWidget] = {}
for fg in form_field_groups:
form_group = FormGroupWidget(fg.fields, fg.label)
self.main_layout.addWidget(form_group)
self._add_widgets(form_group.widgets)
_sub = fg.fields
if isinstance(_sub, FormFieldDynList):
widget = DynamicListWidget(_sub.fields, _sub.label)
else:
widget = FormGroupWidget(_sub, fg.label)
self.main_layout.addWidget(widget)
# TODO widget list is not updated with dynamic lists
# self._add_widgets(widget.widgets)
# self.widgets.update(form_group.widgets)
# buttons
@@ -707,7 +1119,10 @@ class AutoForm(QWidget):
# Wir gehen unsere Feld-Definitionen durch
for fg in self.form_field_groups:
for field in fg.fields:
if isinstance(fg.fields, DynamicListWidget):
continue
for field in fg.fields: # type: ignore
widget = self.widgets[field.key]
# 1. Zuerst setzen wir das Design des Feldes wieder auf "Normal" zurück.
@@ -747,8 +1162,9 @@ class AutoForm(QWidget):
# --- 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
error_text = (
"Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n- "
+ "\n- ".join(errors)
)
QMessageBox.warning(self, "Fehlende Angaben", error_text)
self._enable_save()
@@ -762,7 +1178,10 @@ class AutoForm(QWidget):
def reset_form(self) -> None:
for fg in self.form_field_groups:
for field in fg.fields:
if isinstance(fg.fields, DynamicListWidget):
continue
for field in fg.fields: # type: ignore
widget = self.widgets[field.key]
if field.readonly:
@@ -803,6 +1222,109 @@ class AutoForm(QWidget):
return data
class DynamicListWidget(QWidget):
"""
Ein Widget, das dynamisch beliebig viele Sub-Formulare
(z.B. Adressen) erzeugen und verwalten kann.
"""
def __init__(self, fields: Sequence[FormField], title="Eintrag"):
super().__init__()
self.fields = fields
self.title = title
self.widgets: dict[str, QWidget] = {}
# Das Hauptlayout für diese Liste
self.group_box = QGroupBox(title)
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.inner_layout = QVBoxLayout()
self.main_layout.addWidget(self.group_box)
self.group_box.setLayout(self.inner_layout)
# Liste, in der wir uns die generierten Sub-Formulare merken
self.sub_forms = []
# Der "+ Hinzufügen" Button
self.add_btn = QPushButton("+ Weitere hinzufügen")
self.add_btn.setStyleSheet(
"color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;"
)
self.add_btn.clicked.connect(self.add_entry)
self.inner_layout.addWidget(self.add_btn)
# Direkt beim Start ein leeres Formular anzeigen (optional)
self.add_entry()
def add_entry(self):
"""Erzeugt ein neues Sub-Formular und fügt es VOR dem Plus-Button ein."""
# 1. Den Rahmen für das einzelne Sub-Formular bauen
entry_box = QGroupBox(f"{self.title} {len(self.sub_forms) + 1}")
entry_layout = QHBoxLayout(entry_box)
# TEST auto form generator
# TODO: keys may not be fixed
form_widget = FormGroupWidget(self.fields, None)
self.widgets.update(form_widget.widgets)
# 2. Das eigentliche Formular (hier könntest du auch deinen automatisierten
# Form-Generator rekursiv aufrufen!)
# target_layout = QFormLayout()
# street_input = QLineEdit()
# city_input = QLineEdit()
# target_layout.addRow("Straße:", street_input)
# target_layout.addRow("Stadt:", city_input)
# 3. Den "Löschen" (-) Button bauen
del_btn = QPushButton("🗑️")
del_btn.setFixedSize(30, 30)
# Lambda mit default-Parameter, um genau DIESE entry_box zu löschen
del_btn.clicked.connect(lambda checked=False, box=entry_box: self.remove_entry(box))
# 4. Alles zusammensetzen
# entry_layout.addLayout(target_layout)
entry_layout.addWidget(form_widget)
entry_layout.addWidget(del_btn)
# 5. Daten merken, um sie später auszulesen
self.sub_forms.append({"box": entry_box, "form_widget": form_widget})
# 6. In das Hauptlayout einfügen (Index - 1 bedeutet: VOR dem "+" Button)
self.inner_layout.insertWidget(self.main_layout.count() - 1, entry_box)
self.update_titles()
def remove_entry(self, box_to_remove: QGroupBox):
"""Löscht ein Sub-Formular wieder."""
# Das Widget aus dem Layout entfernen und zerstören
self.main_layout.removeWidget(box_to_remove)
box_to_remove.deleteLater()
# Aus unserer internen Liste filtern
self.sub_forms = [f for f in self.sub_forms if f["box"] != box_to_remove]
self.update_titles()
def update_titles(self):
"""Nummeriert die Boxen nach dem Löschen neu durch."""
for index, form in enumerate(self.sub_forms):
form["box"].setTitle(f"{self.title} {index + 1}")
def get_data(self):
"""Liest die Daten aller dynamisch erzeugten Formulare aus!"""
raise NotImplementedError()
results = []
for form in self.sub_forms:
# Hier bauen wir ein Dictionary pro Sub-Formular
entry_data = {
"strasse": form["widgets"]["strasse"].text(),
"ort": form["widgets"]["ort"].text(),
}
results.append(entry_data)
return results
class FormGroupWidget(QWidget):
def __init__(
self,
@@ -844,16 +1366,21 @@ class FormGroupWidget(QWidget):
# --- LAYOUT ---
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.inner_layout = QVBoxLayout()
# self.inner_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.inner_layout.addLayout(self.form_layout)
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)
self.group_box.setLayout(self.inner_layout)
# self.group_box.setLayout(self.form_layout)
else:
self.main_layout.addLayout(self.form_layout)
# self.main_layout.addLayout(self.form_layout)
self.main_layout.addLayout(self.inner_layout)
self.form_layout.setSpacing(10) # Abstand zwischen den Zeilen
@@ -919,20 +1446,30 @@ class FormGroupWidget(QWidget):
case FormFieldType.DROPDOWN:
widget = QComboBox()
assert field.options
widget.addItem("--- Bitte wählen ---")
widget.addItems(field.options)
if field.placeholder:
widget.setPlaceholderText(field.placeholder)
if field.fill_value:
widget.setCurrentText(field.fill_value)
else:
widget.setCurrentIndex(0)
if field.readonly:
widget.setEnabled(False)
widget.setProperty("styleClass", "stempel")
# case FormFieldType.DYNAMIC_LIST:
# widget = DynamicListWidget(field.label)
case _:
raise NotImplementedError(f"Not supported field type: {field.type.value}")
self.widgets[field.key] = widget
# if field.type is FormFieldType.DYNAMIC_LIST:
# self.inner_layout.addWidget(widget)
# continue
if not field.tooltip:
self.form_layout.addRow(field.label, widget)
continue
@@ -952,6 +1489,17 @@ class FormGroupWidget(QWidget):
self.form_layout.addRow(field.label, field_layout)
# from PySide6.QtWidgets import (
# QFormLayout,
# QGroupBox,
# QHBoxLayout,
# QLineEdit,
# QPushButton,
# QVBoxLayout,
# QWidget,
# )
class ClickableCell(QFrame):
# Wir definieren ein Signal, das ein Dictionary (die Daten) mitschickt
clicked = Signal(dict)
@@ -1273,7 +1821,7 @@ class SearchFormPage(QWidget):
]
)
combo.setPlaceholderText("Bitte auswählen")
combo.model().item(0).setEnabled(False)
combo.model().item(0).setEnabled(False) # type: ignore
hor_layout.addWidget(label)
hor_layout.addWidget(combo, stretch=1)
container_layout.addLayout(hor_layout)