diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index c58f359..c50b8d8 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -4,12 +4,17 @@ import dataclasses as dc import enum import uuid from collections.abc import Sequence -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar import babel from wce_crm.custom_widget_registry import CUSTOM_WIDGET_NAMES +if TYPE_CHECKING: + from PySide6.QtWidgets import QWidget + +T = TypeVar("T", bound="QWidget") + class FormFieldType(enum.StrEnum): GROUP = enum.auto() @@ -31,7 +36,7 @@ class FormFieldType(enum.StrEnum): @dc.dataclass(slots=True) -class FormField: +class FormField(Generic[T]): label: str type: FormFieldType children: Sequence[FormField] = dc.field(default_factory=list) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index a83d396..2b44401 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -12,7 +12,17 @@ from collections import defaultdict from collections.abc import Container, Iterable, Sequence from pathlib import Path from pprint import pformat -from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Final, + Literal, + Protocol, + TypeAlias, + TypedDict, + TypeVar, + cast, +) from typing_extensions import override from dopt_basics import datetime as dopt_dt @@ -92,6 +102,7 @@ if TYPE_CHECKING: K = TypeVar("K") V = TypeVar("V") +T = TypeVar("T", bound="QWidget") DEBUG: bool = True @@ -274,7 +285,7 @@ class CustomWidget(QWidget): def _add_widget_to_layout( - form_field: FormField, + form_field: FormField[T], widget: QWidget, target_layout: QLayout, widget_only: bool = False, @@ -299,10 +310,10 @@ def _add_widget_to_layout( def build_and_add_widget( - form_field: FormField, + form_field: FormField[T], parent_layout: QLayout, prefix: str = "", -) -> tuple[str, QWidget]: +) -> tuple[str, T]: no_scroll_filter = NoScrollFilter(parent_layout) if prefix and form_field.key: full_key = f"{prefix}{COLUMN_SEP}{form_field.key}" if prefix else form_field.key @@ -311,7 +322,6 @@ def build_and_add_widget( else: full_key = form_field.key - widget: QWidget layout_widget_only: bool = False install_no_scroll_filter: bool = True @@ -496,7 +506,7 @@ def build_and_add_widget( _add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only) - return full_key, widget + return full_key, cast(T, widget) def _build_ui_recursively( @@ -711,40 +721,35 @@ def get_form_data( ignored_keys.append(key) continue - if isinstance(widget, QLineEdit): - data = widget.text() - if data != "": - value = data - if form_field.type is FormFieldType.TEXT_DATE: - value = datetime.datetime.strptime(value, DATE_FMT).date() - elif form_field.type is FormFieldType.TEXT_DATETIME: - value = datetime.datetime.strptime(value, DATETIME_FMT) - elif isinstance(widget, QPlainTextEdit): - data = widget.toPlainText() - if data != "": - value = data - elif isinstance(widget, QDateEdit): - qt_date = widget.date() - value = qt_date.toPython() - elif isinstance(widget, FlexibleDateInput): - value = widget.get_pydate() - elif isinstance(widget, FlexibleDateTimeInput): - value = widget.get_pydatetime() - elif isinstance(widget, QComboBox): - value = widget.currentData() - elif isinstance( - widget, - ( - DynamicListWidget, - DynamicDropdownWidgetNumeric, - DynamicDropdownWidgetOption, - CustomWidget, - ), - ): - # this is a special data structure with some assumptions of the widget's internals - # DynamicListWidget: this is a list -> each dynamic list contains a list - # of such dictionaries (handled below) - value = widget.get_form_data() + if form_field.type is FormFieldType.TEXT_DATE: + value = get_widget_value(widget, "date") + elif form_field.type is FormFieldType.TEXT_DATETIME: + value = get_widget_value(widget, "datetime") + else: + value = get_widget_value(widget) + + # elif isinstance(widget, QDateEdit): + # qt_date = widget.date() + # value = qt_date.toPython() + # elif isinstance(widget, FlexibleDateInput): + # value = widget.get_pydate() + # elif isinstance(widget, FlexibleDateTimeInput): + # value = widget.get_pydatetime() + # elif isinstance(widget, QComboBox): + # value = widget.currentData() + # elif isinstance( + # widget, + # ( + # DynamicListWidget, + # DynamicDropdownWidgetNumeric, + # DynamicDropdownWidgetOption, + # CustomWidget, + # ), + # ): + # # this is a special data structure with some assumptions of the widget's internals + # # DynamicListWidget: this is a list -> each dynamic list contains a list + # # of such dictionaries (handled below) + # value = widget.get_form_data() if isinstance(value, dict): raw_data.update(value) @@ -756,6 +761,50 @@ def get_form_data( return raw_data +def get_widget_value( + widget: QWidget, + text_type: Literal["default", "date", "datetime"] = "default", +) -> Any: + value: Any | None = None + + if isinstance(widget, QLineEdit): + data = widget.text() + if data != "": + value = data + if text_type == "date": + value = datetime.datetime.strptime(value, DATE_FMT).date() + elif text_type == "datetime": + value = datetime.datetime.strptime(value, DATETIME_FMT) + elif isinstance(widget, QPlainTextEdit): + data = widget.toPlainText() + if data != "": + value = data + elif isinstance(widget, QDateEdit): + qt_date = widget.date() + value = qt_date.toPython() + elif isinstance(widget, FlexibleDateInput): + value = widget.get_pydate() + elif isinstance(widget, FlexibleDateTimeInput): + value = widget.get_pydatetime() + elif isinstance(widget, QComboBox): + value = widget.currentData() + elif isinstance( + widget, + ( + DynamicListWidget, + DynamicDropdownWidgetNumeric, + DynamicDropdownWidgetOption, + CustomWidget, + ), + ): + # this is a special data structure with some assumptions of the widget's internals + # DynamicListWidget: this is a list -> each dynamic list contains a list + # of such dictionaries (handled below) + value = widget.get_form_data() + + return value + + def set_widget_value( widget: QWidget, value: Any, @@ -3301,17 +3350,19 @@ class Session: @dc.dataclass(slots=True) class Page_Consulting_ConsultingSession_State: - user_id: UserId # must be set with the session (always known) - user_name: str # must be set with the session (always known) - cons_session_id: int | None = None # known and not known entries - timestamp: datetime.datetime = dc.field( + nutzer_id: UserId # must be set with the session (always known) + nutzer_name: str # must be set with the session (always known) + beratung_id: int | None = None # known and not known entries + zeitstempel: datetime.datetime = dc.field( default_factory=lambda: datetime.datetime.now(tz=datetime.UTC) ) - partner: str = "" - interaction_scheme: str = "" # set string like in other dropdowns or use integer (enum) - topic: str = "" # TODO should be linked to something, target unclear (need specification by customer) - notes: str = "" - response: str = "" + ansprechpartner: str | None = None + interaction_scheme: str | None = None + topic: str | None = ( + None # TODO should be linked to something, target unclear (need specification by customer) + ) + anmerkungen: str | None = None + rueckmeldung: str | None = None @dc.dataclass(slots=True) @@ -3614,23 +3665,38 @@ class Page_Consulting_ConsultingSession(QWidget): layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) ROW_HEIGHT = 70 - self.user_field = QLineEdit( - placeholderText="wird über aktiven Nutzer bei Anlage gesetzt" + user_field_def = FormField[QLineEdit]( + "", + FormFieldType.TEXT, + readonly=True, + placeholder="wird über aktiven Nutzer bei Anlage gesetzt", ) - self.user_field.setReadOnly(True) - self.user_field.setProperty("styleClass", "stempel") + _, self.user_field = build_and_add_widget(user_field_def, layout) + # ?? date or datetime? - # self.timestamp = FlexibleDateInput() - # self.timestamp.set_pydate(datetime.datetime.now()) - self.timestamp = FlexibleDateTimeInput() + timestamp_def = FormField[FlexibleDateTimeInput]("", FormFieldType.DATETIME_FLEXIBLE) + _, self.timestamp = build_and_add_widget(timestamp_def, layout) self.timestamp.set_pydatetime(datetime.datetime.now()) - self.contact = QLineEdit(placeholderText="Freitext") - self.contact_type = QLineEdit(placeholderText="sollte Dropdown sein") # Dropdown - self.crm_link = QLineEdit(placeholderText="CRM Links") + contact_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="Freitext") + _, self.contact = build_and_add_widget(contact_def, layout) - self.plain_text = QPlainTextEdit() - self.response = QPlainTextEdit() + contact_type_def = FormField[QComboBox]( + "", + FormFieldType.DROPDOWN, + placeholder="sollte Dropdown sein", + options=[("Persönlich", None), ("Telefon", None), ("Mail", None)], + ) + _, self.contact_type = build_and_add_widget(contact_type_def, layout) + + crm_link_def = FormField[QLineEdit]("", FormFieldType.TEXT, placeholder="CRM Links") + _, self.crm_link = build_and_add_widget(crm_link_def, layout) + + plain_text_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT) + _, self.plain_text = build_and_add_widget(plain_text_def, layout) + + response_def = FormField[QPlainTextEdit]("", FormFieldType.LONGTEXT) + _, self.response = build_and_add_widget(response_def, layout) self.btn_delete = QPushButton("✕") self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen @@ -3639,6 +3705,7 @@ class Page_Consulting_ConsultingSession(QWidget): self.btn_delete.setStyleSheet("color: #cc0000; font-weight: bold;") self.btn_delete.clicked.connect(lambda: self.delete_request.emit(self)) self.btn_delete.setFixedWidth(30) + layout.addWidget(self.btn_delete) widgets = ( self.user_field, @@ -3652,7 +3719,7 @@ class Page_Consulting_ConsultingSession(QWidget): ) for widget in widgets: widget.setFixedHeight(ROW_HEIGHT) - layout.addWidget(widget) + # layout.addWidget(widget) # column stretching: must be matched with header num_widgets = len(widgets) @@ -3676,22 +3743,22 @@ class Page_Consulting_ConsultingSession(QWidget): logger_page_consulting.debug( "[Page Consulting -- Consulting Session]: State = %s", self.STATE ) - set_widget_value(self.user_field, self.STATE.user_name) - set_widget_value(self.timestamp, self.STATE.timestamp.astimezone(TIMEZONE_CEST)) - set_widget_value(self.contact, self.STATE.partner) + set_widget_value(self.user_field, self.STATE.nutzer_name) + set_widget_value(self.timestamp, self.STATE.zeitstempel.astimezone(TIMEZONE_CEST)) + set_widget_value(self.contact, self.STATE.ansprechpartner) set_widget_value(self.contact_type, self.STATE.interaction_scheme) set_widget_value(self.crm_link, self.STATE.topic) - set_widget_value(self.plain_text, self.STATE.notes) - set_widget_value(self.response, self.STATE.response) + set_widget_value(self.plain_text, self.STATE.anmerkungen) + set_widget_value(self.response, self.STATE.rueckmeldung) def _sync_gui_to_state(self) -> None: # TODO: get timestamp from custom field self.STATE.timestamp - self.STATE.timestamp = self.timestamp.get_pydatetime() - self.STATE.partner = self.contact.text() - self.STATE.interaction_scheme = self.contact_type.text() - self.STATE.topic = self.crm_link.text() # TODO: change to correct link - self.STATE.notes = self.plain_text.toPlainText() - self.STATE.response = self.response.toPlainText() + self.STATE.zeitstempel = self.timestamp.get_pydatetime() + self.STATE.ansprechpartner = get_widget_value(self.contact) + self.STATE.interaction_scheme = get_widget_value(self.contact_type) + self.STATE.topic = get_widget_value(self.crm_link) # TODO: change to correct link + self.STATE.anmerkungen = get_widget_value(self.plain_text) + self.STATE.rueckmeldung = get_widget_value(self.response) def get_data(self) -> Page_Consulting_ConsultingSession_State: self._sync_gui_to_state() @@ -3873,8 +3940,8 @@ class Page_Consulting_Table(QWidget): ) -> None: if row_state is None: row_state = Page_Consulting_ConsultingSession_State( - user_id=self.SITE_STATE.session.user_id, - user_name=self.SITE_STATE.session.user_name, + nutzer_id=self.SITE_STATE.session.user_id, + nutzer_name=self.SITE_STATE.session.user_name, ) row = Page_Consulting_ConsultingSession(site_state=self.SITE_STATE, state=row_state)