diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index a066cff..a3c1ad3 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -9,12 +9,11 @@ import logging import pickle import re import sys -import time import uuid from collections import defaultdict from collections.abc import Container, Iterable, Sequence from pathlib import Path -from pprint import pformat, pprint +from pprint import pformat from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast from typing_extensions import override @@ -50,6 +49,7 @@ from PySide6.QtWidgets import ( QGroupBox, QHBoxLayout, QLabel, + QLayout, QLineEdit, QMainWindow, QMessageBox, @@ -159,13 +159,13 @@ def unmerge_dict_to_list( class FlatBaseModel(BaseModel): """ - Optimierte Pydantic-Basisklasse, die JSON-Strings und Doppel-Unterstriche - vollständig rekursiv (tiefenunabhängig) auflöst. + Optimised Pydantic base class, which parses JSON strings and column + separators recursively and correctly """ @classmethod def _recursive_parse_json(cls, data: Any) -> Any: - """Sucht im gesamten Objekt nach JSON-Listen-Strings und entpackt sie.""" + """look for JSON list strings and parse them""" if isinstance(data, str) and data.startswith("[") and data.endswith("]"): try: parsed = json.loads(data) @@ -181,9 +181,8 @@ class FlatBaseModel(BaseModel): @classmethod def _recursive_unflatten(cls, data: Any) -> Any: - """Baut im gesamten Objekt flache '__'-Schlüssel in geschachtelte Dicts um.""" + """building nested structure using column spearator sequence""" if isinstance(data, dict): - # 1. Die aktuelle Ebene dieses Dictionaries ent-flachen unflattened_level = {} for key, value in data.items(): if COLUMN_SEP in key: @@ -197,46 +196,45 @@ class FlatBaseModel(BaseModel): else: unflattened_level[key] = value - # 2. Jetzt tiefer in die Werte gehen (rekursiv für Unter-Dicts) return {k: cls._recursive_unflatten(v) for k, v in unflattened_level.items()} elif isinstance(data, list): - # Auch durch Listen (z.B. deine Schulbildung) wandern und Sub-Dicts ent-flachen return [cls._recursive_unflatten(item) for item in data] return data @model_validator(mode="before") @classmethod - def __unflatten_input(cls, data: Any) -> Any: - """Eingangskontrolle: Bereitet flache DB/GUI-Daten sauber für Pydantic vor.""" + def __unflatten_input(cls, data: Any) -> Any: # type: ignore + """entry control: prepare flat DB/GUI data for Pydantic""" if not isinstance(data, dict): return data - # Schritt 1: Alle JSON-Strings (egal wie tief) in echte Listen umwandeln - # Das verwandelt '[1, 2, 3]' zuverlässig in [1, 2, 3] + # setp 1: convert all JSON-Strings to lists json_parsed_data = cls._recursive_parse_json(data) - - # Schritt 2: Alle '__' Schlüssel (egal wie tief) in Unter-Strukturen falten + # step 2: build nested structure based on defined separator sequence final_nested_data = cls._recursive_unflatten(json_parsed_data) return final_nested_data def to_db(self, *args, **kwargs) -> dict[str, Any]: - """Ausgang für die DB: Flach, Listen sind JSON-Strings.""" + """output for DB: flat, lists as JSON-Strings""" nested = super().model_dump(*args, **kwargs) return self.__flatten_dict(nested, serialize_lists=True) def to_gui(self, *args, **kwargs) -> dict[str, Any]: - """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen.""" + """output for GUI: flat, but lists remain Python lists""" nested = super().model_dump(*args, **kwargs) return self.__flatten_dict(nested, serialize_lists=False) @classmethod def __flatten_dict( - cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True - ) -> dict: - """Rekursiver Helfer zum Abflachen von Strukturen.""" + cls, + nested_dict: dict, + parent_key: str = "", + serialize_lists: bool = True, + ) -> dict[str, Any]: + """recursive function to flatten the structure (for outputs)""" items = [] for k, v in nested_dict.items(): new_key = f"{parent_key}{COLUMN_SEP}{k}" if parent_key else k @@ -435,11 +433,6 @@ class FormField: if self.required: self.label += "*" - # if ( - # self.type in (FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN) - # and not options - # ): - # raise ValueError("Invalid field definition: Dropdown requires options") if self.type is FormFieldType.CUSTOM and not self.custom_widget: raise ValueError("Custom widget must be named using parameter >custom_widget<") elif self.type is FormFieldType.CUSTOM and self.custom_widget not in CUSTOM_WIDGETS: @@ -845,30 +838,31 @@ def reset_form( widget.setStyleSheet("") -def _insert_nested( - target_dict: dict[str, Any], - key_path: Sequence[str], - value: Any, -): - # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}} - for part in key_path[:-1]: - target_dict = target_dict.setdefault(part, {}) - target_dict[key_path[-1]] = value +# TODO: check removal +# def _insert_nested( +# target_dict: dict[str, Any], +# key_path: Sequence[str], +# value: Any, +# ): +# # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}} +# for part in key_path[:-1]: +# target_dict = target_dict.setdefault(part, {}) +# target_dict[key_path[-1]] = value -def _get_nested( - target_dict: dict[str, Any], - key_path: Sequence[str], -) -> Any: - # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}} - current = target_dict - for part in key_path: - if isinstance(current, dict) and part in current: - current = current[part] - else: - return None # path does not exist +# def _get_nested( +# target_dict: dict[str, Any], +# key_path: Sequence[str], +# ) -> Any: +# # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}} +# current = target_dict +# for part in key_path: +# if isinstance(current, dict) and part in current: +# current = current[part] +# else: +# return None # path does not exist - return current +# return current @dc.dataclass(slots=True) @@ -975,22 +969,15 @@ def get_form_data( elif isinstance(widget, QComboBox): value = widget.currentData() elif isinstance(widget, DynamicListWidget): - ... # this should be a list: each dynamic list contains a list # of such dictionaries value = widget.get_form_data() - # value = [val for val in form_data.values()] - # print(">>>>>>>>> Key: ", key) - # print(">>>>>>>>> Form Data after call:") - # pprint(value) - elif isinstance(widget, (DynamicDropdownWidget, CustomWidget)): - # this is a special data structure with some assumptions of the widget's structure - value = widget.get_form_data() - # print(">>>>>>>>> Key: ", key) - # print(">>>>>>>>> Form Data after call:") - # pprint(value) - # _insert_nested(raw_data, key.split(COLUMN_SEP), value) + elif isinstance(widget, (DynamicDropdownWidget, CustomWidget)): + # this is a special data structure with some assumptions of the widget's internals + value = widget.get_form_data() + + # TODO: check removal # logger_get_data.info("Key: %s", key) # logger_get_data.info("Value: %s", value) @@ -1061,6 +1048,7 @@ def set_form_data( if data_key not in filter_keys: continue + # TODO check removal # key_path = key.split(COLUMN_SEP) # value = _get_nested(data, key_path) # value = data.get(key, None) @@ -1518,18 +1506,7 @@ class Grunderfassung_SuchWidget(CustomWidget): @override def get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry, filter_keys=self.DATA_EXPORT_FIELDS) - logger_get_data.debug("\n\n\n------- Form data CustomWidget:\n%s", pformat(form_data)) - - return form_data - # TODO check removal - - form_data = tuple(get_leaf_dicts(form_data))[0] - - if DEBUG_SEARCH_WIDGET: - self.export_data = form_data - logger_search_widget.debug( - "form_data Grunderfassung_SuchWidget:\n%s", pformat(form_data) - ) + logger_get_data.debug("Form data CustomWidget:\n%s", pformat(form_data)) return form_data @@ -1543,130 +1520,9 @@ class Grunderfassung_SuchWidget(CustomWidget): raise RuntimeError("No data. Get form data first.") data = self.export_data - # formatted_data = {} - # _insert_nested(formatted_data, self.prefix.split(COLUMN_SEP), data) - - # if DEBUG_SEARCH_WIDGET: - # logger_search_widget.debug("\nformatted data:\n%s", pformat(formatted_data)) - # logger_get_data.debug(">>>>>>>>> Call set_form_data for CustomSearchWidget") - # pprint_registry(self.widget_registry) set_form_data(self.widget_registry, data, filter_keys=self.DATA_EXPORT_FIELDS) -class ContactPersonForm_Search(QWidget): - def __init__(self): - super().__init__() - - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - form_layout = QFormLayout() - form_layout.setSpacing(10) - # TODO: remove title - title = QLabel("--- Suche Nutzer ---") - title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold; - main_layout.addWidget(title) - # --- SEARCH --- - # self.search_input = QComboBox(placeholderText="Tippen zum Suchen...") - self.search_input = QComboBox() - self.search_input.setEditable(True) - self.search_input.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) - line_edit = self.search_input.lineEdit() - assert line_edit - line_edit.setPlaceholderText("Suchen...") - # --- FILLED FIELDS --- - form_layout.addRow("Suche Ansprechpartner:", self.search_input) - self.completer = self.search_input.completer() - assert self.completer - self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) - self.completer.setFilterMode(Qt.MatchFlag.MatchContains) - self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) - self.search_input.activated.connect(self.search_result_selected) - # salutation - self.gui_titel = QLineEdit() - self.gui_anrede = QLineEdit() - hor_layout = QHBoxLayout() - hor_layout.addWidget(self.gui_anrede, stretch=1) - hor_layout.addWidget(self.gui_titel, stretch=1) - form_layout.addRow("Anrede / Titel:", hor_layout) - # names - self.gui_nachname = QLineEdit() - self.gui_vorname = QLineEdit() - hor_layout = QHBoxLayout() - hor_layout.addWidget(self.gui_nachname, stretch=1) - hor_layout.addWidget(self.gui_vorname, stretch=1) - form_layout.addRow("Nachname / Vorname:", hor_layout) - # phones - phone_layout = QHBoxLayout() - phone_layout.setContentsMargins(0, 0, 0, 0) - phone_layout.setSpacing(10) - self.gui_landline_number = QLineEdit() - self.gui_mobile_number = QLineEdit() - phone_layout.addWidget(self.gui_landline_number, stretch=1) - phone_layout.addWidget(self.gui_mobile_number, stretch=1) - form_layout.addRow("Telefon Festnetz / Mobil:", phone_layout) - # additional - self.gui_email = QLineEdit() - form_layout.addRow("E-Mail:", self.gui_email) - self.gui_funktion = QLineEdit() - form_layout.addRow("Funktion", self.gui_funktion) - - main_layout.addLayout(form_layout) - - # auto-filled fields are readonly - self.autofilled_fields: tuple[QLineEdit, ...] = ( - self.gui_titel, - self.gui_anrede, - self.gui_nachname, - self.gui_vorname, - self.gui_landline_number, - self.gui_mobile_number, - self.gui_email, - self.gui_funktion, - ) - for field in self.autofilled_fields: - field.setReadOnly(True) - field.setProperty("styleClass", "stempel") - - self.update_search_data(None) - - def fill_out( - self, - info: be_init_rec.ContactPersonInfo, - ) -> None: - self.gui_titel.setText(info["an_titel"]) - self.gui_anrede.setText(info["an_anrede"]) - self.gui_nachname.setText(info["an_nachname"]) - self.gui_vorname.setText(info["an_vorname"]) - self.gui_landline_number.setText(info["an_festnetz"]) - self.gui_mobile_number.setText(info["an_mobil"]) - self.gui_email.setText(info["an_mail"]) - self.gui_funktion.setText(info["an_position"]) - - def search_result_selected( - self, - index: int, - ): - an_id = self.search_input.itemData(index) - comp_info = be_init_rec.contact_person_search_get_info( - an_id=an_id, - ) - self.fill_out(comp_info) - - def clear_autofilled_fields(self) -> None: - self.search_input.clear() - for field in self.autofilled_fields: - field.clear() - - def update_search_data( - self, - company_id: int | None, - ) -> None: - self.clear_autofilled_fields() - search_choices = be_init_rec.contact_person_search_choices(company_id, True) - for item, db_index in search_choices: - self.search_input.addItem(item, db_index) - - def enhanced_label( base_label: str, add_text: str, @@ -1707,6 +1563,8 @@ def search_widgets_by_key( class AutoForm(QWidget): + save_clicked_form = Signal() # formular saved (data changed for front page) + def __init__( self, cfg: AutoFormConfig, @@ -1802,7 +1660,7 @@ class AutoForm(QWidget): self.save_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) - self.save_btn.clicked.connect(self.on_save_clicked) + self.save_btn.clicked.connect(self.save_data) self.layout_btn.addWidget(self.save_btn) self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)") self.reset_btn.setShortcut("Ctrl+Z") @@ -1813,14 +1671,13 @@ class AutoForm(QWidget): self.reset_btn.clicked.connect(self.reset_form) self.layout_btn.addWidget(self.reset_btn) - # self.ignored_keys = ignored_keys self.current_id: int = -1 - # test button - # self.main_layout.addSpacing(10) - # self.main_layout.addWidget(self.test_button) def _set_db_index(self) -> None: - index = int(self.id_field_input.text()) + try: + index = int(self.id_field_input.text()) + except ValueError: + index = -1 self.current_id = index logger.debug("Set index to %d", self.current_id) @@ -1851,7 +1708,6 @@ class AutoForm(QWidget): if lookup_id > 0: logger_get_data_auto_form.debug("Load from DB:") loaded_data = self.cfg.data_get(lookup_id) - # loaded_data = be_init_rec.get_initial_recording(lookup_id) else: logger_get_data_auto_form.debug("Load from pickled object:") loaded_data = load_pydantic_model_dict_db() @@ -1868,7 +1724,7 @@ class AutoForm(QWidget): self.set_form_data(form_data) self.current_id = lookup_id - def on_save_clicked(self) -> None: + def save_data(self) -> None: self._disable_save() errors = self.validate_form_data() @@ -1883,7 +1739,6 @@ class AutoForm(QWidget): return # no errors: data can be saved - # TODO: change routine to trigger data saving in the backend logger.info("Erfolg! Alle Daten sind valide.") logger.info("Get form data call...") form_data = self.get_form_data() @@ -1897,18 +1752,18 @@ class AutoForm(QWidget): # logger.debug("%s", pformat(validated_data.model_dump())) except ValidationError as e: # catch errors and show them in GUI - fehler_texte = [] + error_texts = [] # Pydantic detailed error list for error in e.errors(): - print(error) + logger.error("Error during validation phase:\n%s", pformat(error)) - # error['loc'][0] = name of field - fehlerhaftes_feld = str(error["loc"][0]) - grund = error["msg"] + error_field = str(error["loc"][0]) + reason = error["msg"] - fehler_texte.append(f"- {fehlerhaftes_feld}: {grund}") + error_texts.append(f"- {error_field}: {reason}") + # TODO: Is this needed and feasible? # formatting red # if fehlerhaftes_feld in self.widgets: # self.widgets[fehlerhaftes_feld].setStyleSheet( @@ -1916,40 +1771,30 @@ class AutoForm(QWidget): # ) # tell user what went wrong - QMessageBox.warning(self, "Eingabefehler", "\n".join(fehler_texte)) + QMessageBox.warning(self, "Eingabefehler", "\n".join(error_texts)) else: # !! this code is only called if the 'try' block was successful - # save_pydantic_model_dict_db(validated_data) # TODO save data to database - logger.info( - "\n\n>>>>>>>>>>>>> Form data without 'exlude':\n%s", - pformat(validated_data.to_db()), + db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) + logger.debug( + ("Form data with 'exlude' (must be saved in the database):\n%s"), + pformat(db_data), ) - logger.info( - ( - "\n\n>>>>>>>>>>>>> Form data with 'exlude' " - "(must be saved in the database):\n%s" - ), - pformat(validated_data.to_db(exclude=self.cfg.ignored_keys)), - ) - db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) if self.current_id < 0: logger.debug("Insert triggered") self.cfg.data_insert(db_data) - # be_init_rec.insert_initial_recording(db_data) else: logger.debug("Update triggered") self.cfg.data_update(self.current_id, db_data) - # be_init_rec.update_initial_recording(self.current_id, db_data) logger_get_data.info("Data saved successfully") + self.save_clicked_form.emit() self.reset_form() finally: # always re-enable save, even if error occurred self._enable_save() - # ------------------------------------------------------------ def validate_form_data(self) -> list[str]: return validate_form_data(self.widget_registry) @@ -2007,11 +1852,8 @@ class DynamicListWidget(QWidget): "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) - self.widget_registry_base_size = len(self.widget_registry) - # add empty sub form as initial value self.add_empty_entry = add_empty_entry if self.add_empty_entry: @@ -2180,7 +2022,6 @@ class DynamicDropdownWidget(QWidget): self.form_layout, self.widget_registry, prefix=f"{self.prefix}", - # prefix=f"{self.prefix}-[0]", ) dropdown_widget_entry = tuple(self.widget_registry.values())[0] dropdown_widget = dropdown_widget_entry["widget"] @@ -2195,7 +2036,6 @@ class DynamicDropdownWidget(QWidget): self.sub_forms: list[SubForm] = [] self.dropdown_widget.currentTextChanged.connect(self.on_anzahl_changed) - self.widget_registry_base_size = len(self.widget_registry) def on_anzahl_changed( self, @@ -2234,8 +2074,6 @@ class DynamicDropdownWidget(QWidget): parent_layout=form_layout, widget_registry=sub_form.registry, prefix=f"{self.prefix}", - # widget_registry=self.widget_registry, - # prefix=f"{self.prefix}-[{number_form}]", ) self.rows_layout.addWidget(container) @@ -2268,47 +2106,15 @@ class DynamicDropdownWidget(QWidget): return errors def get_form_data(self) -> dict[str, Any]: - # raw_data = get_form_data(self.widget_registry) form_data = get_form_data(self.widget_registry) sub_form_data_dicts = (get_form_data(sub.registry) for sub in self.sub_forms) sub_data = merge_dicts_to_lists(sub_form_data_dicts) form_data.update(sub_data) - logger_get_data.debug("##################") - # logger_get_data.debug("\nSub data:\n%s\n\n", pformat(sub_data)) - # logger_get_data.debug("Form data:\n%s", pformat(form_data)) - logger_get_data.debug( - "\n\n\n------- Form data DynamicDropdownWidget:\n%s", pformat(form_data) - ) + logger_get_data.debug("Form data DynamicDropdownWidget:\n%s", pformat(form_data)) return form_data - # TODO check removal - - # each sub form has its own numbered key: We need to get rid of these - # This widget has one first dropdown value and then an arbitrary number of - # list values - # The following procedure assumes that the data is extracted as one - # dropdown value and a list of associated values in the form of the following schema - # data = {"key_dropdown": dropdown_value, "key_child_field": list[child_values] | None} - - # print(">>>> Dynamic dropdown: ") - # pprint(raw_data) - - # export_dict: dict[str, Any] = {} - # children_values: list[str] | None = None - - # for idx, data_dict in enumerate(get_leaf_dicts(raw_data)): - # if idx == 0: - # export_dict.update(data_dict) - # else: - # for key in data_dict: - # if key not in export_dict: - # children_values = export_dict.setdefault(key, []) - # assert children_values is not None - # children_values.append(data_dict[key]) - - # return export_dict def set_form_data( self, @@ -2353,7 +2159,7 @@ class DynamicDropdownWidget(QWidget): class NoScrollFilter(QObject): - """disables scrolling in fields which are not in focus""" + """disables scrolling in fields""" def eventFilter( self, @@ -2373,13 +2179,15 @@ class NoScrollFilter(QObject): class ClickableCell(QFrame): """cell in the table on the startup screen""" - clicked = Signal( - be_init_rec.FrontpageCompany - ) # Signal which also sends data as dictionary + clicked = Signal(be_init_rec.FrontpageCompany) - def __init__(self, text: str, data_record: be_init_rec.FrontpageCompany): + def __init__( + self, + text: str, + data_record: be_init_rec.FrontpageCompany, + ): super().__init__() - self.data_record = data_record # Wir merken uns den ganzen Datensatz + self.data_record = data_record self.setStyleSheet(""" ClickableCell { background-color: white; @@ -2399,18 +2207,13 @@ class ClickableCell(QFrame): def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: - # Wenn geklickt wird, senden wir die Daten aus self.clicked.emit(self.data_record) class HeaderCell(QLabel): def __init__(self, text): super().__init__(text) - - # Textausrichtung zentrieren self.setAlignment(Qt.AlignmentFlag.AlignCenter) - - # Styling: Fetter Text, grauer Hintergrund (entspricht slate-200), leicht abgerundet self.setStyleSheet(""" HeaderCell { background-color: #e2e8f0; @@ -2422,45 +2225,6 @@ class HeaderCell(QLabel): """) -class DetailView(QWidget): - """example only: show what happens if a cell was clicked on the start up page""" - - back_requested = Signal() # Signal für den Zurück-Button - - def __init__(self): - super().__init__() - layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) - - # Zurück-Button - back_btn = QPushButton("← Zurück zur Tabelle") - back_btn.setFixedWidth(200) - back_btn.clicked.connect(lambda: self.back_requested.emit()) - layout.addWidget(back_btn) - - # Platzhalter für die Details - self.title_label = QLabel("Titel") - self.title_label.setStyleSheet( - "font-size: 24px; font-weight: bold; margin-top: 20px;" - ) - layout.addWidget(self.title_label) - - self.info_label = QLabel("Zusatzinfos...") - self.info_label.setWordWrap(True) - self.info_label.setStyleSheet("font-size: 16px; color: #475569; margin-top: 10px;") - layout.addWidget(self.info_label) - - def update_content(self, data): - # Diese Methode füllt die Seite mit den echten Daten - self.title_label.setText(f"Details für: {data.get('c1', 'Unbekannt')}") - self.info_label.setText( - f"Beschreibung: {data.get('c2')}\n\n" - f"Abteilung: {data.get('c3', 'Keine Angabe')}\n" - f"Status: {data.get('c4')}\n" - f"Datum: {data.get('date')}" - ) - - class NewEntrySelect_view(QWidget): """view for new entry for initial recording ("Grunderfassung")""" @@ -3327,9 +3091,11 @@ CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig( ) -class SearchFormPage(QWidget): - back_main_requested = Signal() # Signal für den Zurück-Button - back_requested = Signal() # Signal für den Zurück-Button +# TODO clean code +class PageFormCompany(QWidget): + back_main_requested = Signal() # back to main page + back_requested = Signal() # back button + save_clicked_form = Signal() # form saved (data changed for front page) def __init__(self): super().__init__() @@ -3389,97 +3155,43 @@ class SearchFormPage(QWidget): # vert_layout.addSpacing(20) vert_layout.addWidget(scroll_area) - # --- KOPF Metadaten --- container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) - inf_block_1 = QHBoxLayout() - inhalte = [ - "Fall-Nr.:", - "Ersteintrag Datum:", - "Aktualisierung Datum:", - "Aktualisierung Nutzer:", - ] - for entry in inhalte: - label = QLabel(entry) - label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - field = QLineEdit(placeholderText="...") - field.setText("22.04.2026") - field.setReadOnly(True) - field.setProperty("styleClass", "stempel") - field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - inf_block_1.addWidget(label) - inf_block_1.addWidget(field) - - container_layout.addLayout(inf_block_1) - - # --- NOTIZEN Unternehmen --- - # eventuell später verknüpft - inf_block_2 = QHBoxLayout() - label = QLabel("Notizen:") - 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...")) - - container_layout.addLayout(inf_block_2) - container_layout.addSpacing(10) - - # --- SUCHE MIT NAMEN --- - # self.company_search = Grunderfassung_Suche() - # self.company_search = Grunderfassung_Suche(FORM_FIELDS_SEARCH_HEAD) - # container_layout.addWidget(self.company_search) - # self.contact_person_search = ContactPersonForm_Search() - # container_layout.addWidget(self.contact_person_search) - # hor_layout = QHBoxLayout() - # label = QLabel("Wie sind Sie auf uns aufmerksam geworden?") - # combo = QComboBox() - # combo.addItems( - # [ - # "--- Bitte wählen ---", - # "Agentur für Arbeit", - # "Ausländerbehörde", - # "Jobcenter", - # "Freunde/Familie", - # "Anerkennungsstelle", - # "Beratungsstelle", - # "Internet", - # "Arbeitgeber", - # "Bildungsdienstleister", - # "Welcome-Mappe", - # "Newsletter WFE", - # "Newsletter RM", - # "Sonstiges", - # ] - # ) - # combo.setPlaceholderText("Bitte auswählen") - # combo.model().item(0).setEnabled(False) # type: ignore - # hor_layout.addWidget(label) - # hor_layout.addWidget(combo, stretch=1) - # container_layout.addLayout(hor_layout) - - # container_layout.addWidget( - # QLabel('Platzhalter "Wie sind Sie auf uns aufmerksam geworden?"') - # ) - # self.company_search.company_selected.connect(self.update_contact_persons) - - # container_layout.addSpacing(10) - - # --- 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) + # --- AUTO FORM LAYOUT --- + container_layout.addSpacing(20) + # 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")) self.auto_form = AutoForm(cfg=CONFIG_GRUNDERFASSUNG_UNTERNEHMEN) container_layout.addWidget(self.auto_form) + self.auto_form.save_clicked_form.connect(lambda: self.save_clicked_form.emit()) container_layout.addSpacing(30) - # def update_contact_persons( - # self, - # company_id: int, - # ) -> None: - # self.contact_person_search.update_search_data(company_id) + def reset_form(self) -> None: + self.auto_form.reset_form() + + +def clear_layout( + layout: QLayout | None, +) -> None: + if layout is None: + return + + # for idx in range(start_idx, layout.count()): + while layout.count(): + child = layout.takeAt(0) + if child is None: + continue + + widget = child.widget() + if widget is not None: + widget.deleteLater() + + elif child.layout(): + clear_layout(child.layout()) # 2. Das Hauptfenster mit dem Grid-Layout @@ -3489,106 +3201,99 @@ class MainWindow(QMainWindow): self.setWindowTitle("Master") self.resize(1800, 1000) - # --- 1. DAS MENÜ ERSTELLEN --- + # MENU self.create_menu() - # DER STACK (Stapel) + # STACK: stack to change between 'sites' self.stack = QStackedWidget() self.setCentralWidget(self.stack) - # SEITE 1: Die Tabellen-Ansicht (unser bisheriger Code) + # MAIN PAGE: table view self.main_page = self.setup_main_page() self.stack.addWidget(self.main_page) - # SEITE 2: Die Detail-Ansicht - self.detail_page = DetailView() - self.detail_page.back_requested.connect(self.show_main_page) - self.stack.addWidget(self.detail_page) - - # SEITE: Neue Einträge hinzufügen + # SITE: add new entries for 'Grunderfassung' self.new_entry_select = NewEntrySelect_view() self.new_entry_select.back_requested.connect(self.show_main_page) self.new_entry_select.company_requested.connect(self.show_company_page) self.stack.addWidget(self.new_entry_select) - # SEITE: Bsp. - self.company_recording_page = SearchFormPage() + # SITE: 'Grunderfassung Unternehmen' + self.company_recording_page = PageFormCompany() self.company_recording_page.back_main_requested.connect(self.show_main_page) self.company_recording_page.back_requested.connect(self.show_new_entry_select) + self.company_recording_page.save_clicked_form.connect(self.update_grid) self.stack.addWidget(self.company_recording_page) def setup_main_page(self): - # --- 2. DAS ZENTRALE WIDGET --- - # Da das QMainWindow den Rahmen vorgibt, brauchen wir ein Container-Widget für die Mitte + # QMainWindow defines frame --> container widget needed for the middle main_widget = QWidget() - # self.setCentralWidget(central_widget) - # Das Haupt-Layout des Fensters (Horizontal) outer_layout = QHBoxLayout(main_widget) - vert_layout = QVBoxLayout() - # add button + # add buttons new_btn = QPushButton("Neu →") new_btn.setFixedWidth(100) new_btn.setFixedHeight(40) new_btn.clicked.connect(self.show_new_entry_select) - # back_btn.clicked.connect(lambda: self.back_requested.emit()) - # layout.addWidget(back_btn) - # Ein Container-Widget für deine Tabelle/Grid + # TODO remove + update_btn = QPushButton("UPDATE") + update_btn.setFixedWidth(100) + update_btn.setFixedHeight(40) + update_btn.clicked.connect(self.update_grid) + clear_btn = QPushButton("CLEAR") + clear_btn.setFixedWidth(100) + clear_btn.setFixedHeight(40) + clear_btn.clicked.connect(self._clear_layout) + + # container for table or grid container = QWidget() - # 2. NEU: Dem Container sagen: "Dehne dich so weit aus, wie du darfst!" - # container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - - # SCROLL-BEREICH scroll_area = QScrollArea() - scroll_area.setWidgetResizable( - True - ) # WICHTIG: Erlaubt dem Grid im Inneren, sich an die Breite anzupassen + scroll_area.setWidgetResizable(True) scroll_area.setMinimumWidth(700) - scroll_area.setMaximumWidth( - 1500 - ) # Die Breiten-Begrenzung wandert nun auf die ScrollArea + scroll_area.setMaximumWidth(1500) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - # Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht + # delete frame of ScrollArea to get a flat look scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setWidget(container) vert_layout.addWidget(new_btn) + vert_layout.addWidget(update_btn) + vert_layout.addWidget(clear_btn) vert_layout.addSpacing(20) vert_layout.addWidget(scroll_area) - - # Zentrierung durch "Stretches" (wie mx-auto) - # Wir fügen links und rechts vom Container Platzhalter ein + # springs left and right of container to center the it 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) # 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 + container_layout.setContentsMargins(0, 0, 0, 0) + # grid for table without direct container + self.header_grid = QGridLayout() + self.header_grid.setColumnStretch(0, 1) + self.header_grid.setColumnStretch(1, 1) + self.header_grid.setColumnStretch(2, 1) + self.header_grid.setColumnStretch(3, 1) + self.header_grid.setColumnMinimumWidth(4, 100) + self.grid = QGridLayout() - self.grid.setSpacing(10) # Entspricht gap-2 + self.grid.setSpacing(10) self.grid.setColumnStretch(0, 1) self.grid.setColumnStretch(1, 1) self.grid.setColumnStretch(2, 1) self.grid.setColumnStretch(3, 1) + self.grid.setColumnMinimumWidth(4, 100) - # 3. Wir fügen das Grid oben in das vertikale Layout ein + # add grid to vertical layout + container_layout.addLayout(self.header_grid) 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! + # spring below the layout to push it to top container_layout.addStretch() - # Zeilen-Zähler (0 ist für die Überschriften) self.current_row = 0 - # Beispieldaten (Projekt Gamma hat kein 'c3') headers = [ "UN/ NWP/ Kontaktperson", "Individualberatung", @@ -3597,55 +3302,37 @@ class MainWindow(QMainWindow): "Datum", ] for col_idx, title in enumerate(headers): - self.grid.addWidget(HeaderCell(title), self.current_row, col_idx) + self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx) - self.current_row += 1 + # self.current_row += 1 + self.update_grid() + + return main_widget + + def _clear_layout(self) -> None: + clear_layout(self.grid) + + def update_grid(self) -> None: + clear_layout(self.grid) data = be_init_rec.get_company_list() - # 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.", - # }, - # ] - for entry in data: self.add_row_to_grid(entry) - return main_widget - - # --- HILFSMETHODE UM EINE ZEILE EINZUFÜGEN --- def add_row_to_grid(self, entry: be_init_rec.FrontpageCompany): row = self.current_row - # Beim Erstellen der Zelle übergeben wir den kompletten Datensatz - cell = ClickableCell(entry.name, entry) - cell.clicked.connect(self.goto_initial_recording) - self.grid.addWidget(cell, row, 0) - # Wir verbinden das Klick-Signal der Zelle mit unserer Wechsel-Funktion + # NAME + cell_name = ClickableCell(entry.name, entry) + cell_name.clicked.connect(self.goto_initial_recording) + self.grid.addWidget(cell_name, row, 0) + # DATE + cell_date_name = entry.Metadaten_aktualisierung.date().strftime(DATE_FMT) + cell_date = ClickableCell(cell_date_name, entry) + cell_date.clicked.connect(self.goto_initial_recording) + self.grid.addWidget(cell_date, row, 4) + # self.grid.addWidget(ClickableCell(entry["c2"], entry), row, 1) # c3_value = entry.get("c3") @@ -3682,6 +3369,7 @@ class MainWindow(QMainWindow): self.stack.setCurrentWidget(self.new_entry_select) def show_company_page(self): + self.company_recording_page.reset_form() self.stack.setCurrentWidget(self.company_recording_page) # --- MENÜ LOGIK --- diff --git a/prototypes/tests.py b/prototypes/tests.py index 1022ef5..3a1ab35 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -20,6 +20,9 @@ from PySide6.QtCore import QDate, Qt from wce_crm import constants, db from wce_crm.backend import backend +# %% +int("test") + # %% db_path = constants.Config.DB_PATH_MAIN crm_path = constants.Config.DB_PATH_CRM diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index 390afd9..a2c5605 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses as dc +import datetime from typing import Any, TypedDict, cast import polars as pl @@ -185,13 +186,15 @@ class FrontpageCompany: erfassung_id: int ma_id: int name: str + Metadaten_aktualisierung: datetime.datetime def get_company_list() -> list[FrontpageCompany]: stmt = sql.select( db.grunderfassung_unternehmen.c.erfassung_id, db.grunderfassung_unternehmen.c.Partnersuche__un_suche, - ) + db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, + ).order_by(db.grunderfassung_unternehmen.c.Metadaten_aktualisierung.desc()) with db.ENGINE.connect() as conn: res = conn.execute(stmt) @@ -201,9 +204,12 @@ def get_company_list() -> list[FrontpageCompany]: for entry in res: erfassung_id = entry[0] ma_id = entry[1] + datetime_akt = cast(datetime.datetime, entry[2]) + datetime_akt = datetime_akt.astimezone() + comp_info = comp_search_get_info(ma_id) name = comp_info["ma_unternehmensname"] - front_page_companies.append(FrontpageCompany(erfassung_id, ma_id, name)) + front_page_companies.append(FrontpageCompany(erfassung_id, ma_id, name, datetime_akt)) return front_page_companies