From 1d353d863e036e5f9eeb35cfd64d2ead7f0f3e6a Mon Sep 17 00:00:00 2001 From: foefl Date: Wed, 1 Jul 2026 16:36:15 +0200 Subject: [PATCH] added page for consulting sessions with dynamic table --- src/wce_crm/gui.py | 509 +++++++++++++++++++++++++++++++++-------- src/wce_crm/logging.py | 3 + 2 files changed, 414 insertions(+), 98 deletions(-) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index e598691..e5f1d0f 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -28,7 +28,7 @@ from PySide6.QtCore import ( Signal, qInstallMessageHandler, ) -from PySide6.QtGui import QAction +from PySide6.QtGui import QAction, QFont from PySide6.QtWidgets import ( QAbstractItemView, QApplication, @@ -77,6 +77,7 @@ from wce_crm.logging import ( logger_auto_form, logger_get_data, logger_gui, + logger_page_consulting, ) from wce_crm.types import ExtMaId, InitRecId, UserId @@ -2883,29 +2884,40 @@ class MasterDataFormWidget(QWidget): @dc.dataclass(slots=True) class Session: - user_id: UserId | None = None - user_name: str | None = None + user_id: UserId + user_name: str @dc.dataclass(slots=True) -class ConsultingSession: - user_id: UserId - user_name: str - timestamp: datetime.datetime - partner: str - interaction_scheme: str - topic: str # TODO should be linked to something, target unclear (need specification by customer) - notes: str - response: str +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( + 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 = "" + + +@dc.dataclass(slots=True) +class Page_Consulting_Table_State: + rows: list[Page_Consulting_ConsultingSession] = dc.field(default_factory=list) + row_states: list[Page_Consulting_ConsultingSession_State] = dc.field(default_factory=list) @dc.dataclass(slots=True) class Page_Consulting_State: - session: Session | None = None + session: Session un_id: InitRecId | None = None pers_id: InitRecId | None = None cons_type: ConsultingType | None = None - cons_sessions: list[ConsultingSession] = dc.field(default_factory=list) + cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field( + default_factory=list + ) class Page_Consulting(QWidget): @@ -2920,10 +2932,8 @@ class Page_Consulting(QWidget): session: Session, ): super().__init__() - self.session = session - # state management - self.STATE = Page_Consulting_State() + self.STATE = Page_Consulting_State(session=session) # self.current_id: InitRecId = -1 # self.COMPANY_FETAURES_MASTER_DATA: dict[str, str] = { @@ -2951,7 +2961,7 @@ class Page_Consulting(QWidget): # --- HEADER --- header_container = QWidget() header_container.setMinimumWidth(700) - header_container.setMaximumWidth(1200) + header_container.setMaximumWidth(1500) header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) header_layout = QVBoxLayout(header_container) header_layout.setContentsMargins(0, 0, 0, 10) @@ -2984,7 +2994,7 @@ class Page_Consulting(QWidget): scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setMinimumWidth(700) - scroll_area.setMaximumWidth(1200) + scroll_area.setMaximumWidth(1500) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # optional: remove frame to look more modern scroll_area.setFrameShape(QFrame.Shape.NoFrame) @@ -3009,6 +3019,9 @@ class Page_Consulting(QWidget): # layout_change_title.addWidget(change_title_btn) # change_title_btn.clicked.connect(self.set_company_name) # container_layout.addLayout(layout_change_title) + btn_get_data_table = QPushButton("Call 'get_data' from table") + btn_get_data_table.clicked.connect(self._debug_get_data_table) + container_layout.addWidget(btn_get_data_table) separator2 = QFrame() separator2.setFrameShape(QFrame.Shape.HLine) @@ -3033,24 +3046,28 @@ class Page_Consulting(QWidget): type_layout.addWidget(self.type_pauschal_btn) type_layout.addWidget(self.type_individual_btn) - container_layout.addSpacing(20) + # container_layout.addSpacing(50) + table_state = Page_Consulting_Table_State() + self.table_sessions = Page_Consulting_Table(site_state=self.STATE, state=table_state) + container_layout.addWidget(self.table_sessions) + # container_layout.addSpacing(50) - self.layout_sessions = QVBoxLayout() - self.layout_sessions.setContentsMargins(0, 0, 0, 0) - container_layout.addLayout(self.layout_sessions) - self.btn_add_cons_session = QPushButton("+ Hinzufügen") - self.btn_add_cons_session.setStyleSheet( - "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" - ) - self.btn_add_cons_session.clicked.connect(self.add_cons_entry) - self.layout_sessions.addWidget(self.btn_add_cons_session) - self.layout_sessions.addStretch(1) + # self.layout_sessions = QVBoxLayout() + # self.layout_sessions.setContentsMargins(0, 0, 0, 0) + # container_layout.addLayout(self.layout_sessions) + # self.btn_add_cons_session = QPushButton("+ Hinzufügen") + # self.btn_add_cons_session.setStyleSheet( + # "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" + # ) + # self.btn_add_cons_session.clicked.connect(self.add_cons_entry) + # self.layout_sessions.addWidget(self.btn_add_cons_session) + # self.layout_sessions.addStretch(1) - self.cons_entries: list[QWidget] = [] + # self.cons_entries: list[QWidget] = [] # self.layout_sessions.addStretch() - container_layout.addStretch() + # container_layout.addStretch() # --- STYLE --- self.setStyleSheet(""" @@ -3084,45 +3101,53 @@ class Page_Consulting(QWidget): # } # """) - def add_cons_entry(self) -> None: - logger_gui.debug("[Consulting Page] Adding entry...") + # def add_cons_entry(self) -> None: + # logger_gui.debug("[Consulting Page] Adding entry...") - row_widget = QWidget() - entry_row = QHBoxLayout(row_widget) - entry_fields = QHBoxLayout() - entry_row.addLayout(entry_fields) + # row_widget = QWidget() + # entry_row = QHBoxLayout(row_widget) + # entry_fields = QHBoxLayout() + # entry_row.addLayout(entry_fields) - txt1 = QLineEdit() - txt2 = QPlainTextEdit() - txt2.setMaximumHeight(100) - entry_fields.addWidget(txt1) - entry_fields.addWidget(txt2) + # txt1 = QLineEdit() + # txt2 = QPlainTextEdit() + # txt2.setMaximumHeight(100) + # entry_fields.addWidget(txt1) + # entry_fields.addWidget(txt2) - del_btn = QPushButton("🗑️") - del_btn.setFixedSize(30, 30) - # Lambda with default parameter to delete exactly this(!) box - del_btn.clicked.connect( - lambda checked=False, widget=row_widget: self.remove_cons_entry(widget) - ) - entry_row.addWidget(del_btn) + # del_btn = QPushButton("🗑️") + # del_btn.setFixedSize(30, 30) + # # Lambda with default parameter to delete exactly this(!) box + # del_btn.clicked.connect( + # lambda checked=False, widget=row_widget: self.remove_cons_entry(widget) + # ) + # entry_row.addWidget(del_btn) - row_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - # row_widget.setMaximumHeight(200) + # row_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + # # row_widget.setMaximumHeight(200) - self.cons_entries.append(row_widget) - self.layout_sessions.insertWidget(self.layout_sessions.count() - 2, row_widget) + # self.cons_entries.append(row_widget) + # self.layout_sessions.insertWidget(self.layout_sessions.count() - 2, row_widget) - def remove_cons_entry( - self, - row_widget: QWidget, - ) -> None: - logger_gui.debug("[Consulting Page]: Delete clicked...") - self.layout_sessions.removeWidget(row_widget) - row_widget.deleteLater() - self.cons_entries.remove(row_widget) + # def remove_cons_entry( + # self, + # row_widget: QWidget, + # ) -> None: + # logger_gui.debug("[Consulting Page]: Delete clicked...") + # self.layout_sessions.removeWidget(row_widget) + # row_widget.deleteLater() + # self.cons_entries.remove(row_widget) + + def _debug_get_data_table(self) -> None: + self.table_sessions.get_data() # TODO placeholder, check if needed - def _sync_state_to_ui(self) -> None: ... + def _sync_state_to_ui(self) -> None: + match self.STATE.cons_type: + case ConsultingType.PAUSCHAL: + self.type_pauschal_btn.setChecked(True) + case ConsultingType.INDIVIUAL: + self.type_individual_btn.setChecked(True) def _sync_ui_to_state(self) -> None: ... @@ -3154,49 +3179,337 @@ class Page_Consulting(QWidget): self.STATE.pers_id = req.pers_id self.STATE.cons_type = req.cons_type - # def set_company_name(self, name: str) -> None: - # if not name: - # name = self.change_title_text.text() - # if not name: - # name = "n.v." + self._sync_state_to_ui() - # txt = f"Unternehmen: {name}" - # self.title_company_name.setText(txt) - # def load_from_id( - # self, - # id_: InitRecId, - # ) -> None: - # assert id_ is not None, "tried to load with InitRecId None" - # logger_gui.debug(f"Called function to load with ID: {id_}") +class Page_Consulting_ConsultingSession(QWidget): + """child of 'Page_Consulting_DynamicTable'""" - # if id_ < 0: - # # just clear and do nothing else - # logger_gui.debug("just reset with ID < 0") - # self.table_master_data.reset() - # self.set_company_name("") - # self.current_id = id_ - # return + delete_request = Signal(QWidget) - # if id_ == self.current_id: - # logger_gui.debug("ID did not change. Do nothing") - # self.current_id = id_ - # return + def __init__( + self, + site_state: Page_Consulting_State, + state: Page_Consulting_ConsultingSession_State, + ) -> None: + super().__init__() + assert isinstance(site_state, Page_Consulting_State) + assert isinstance(state, Page_Consulting_ConsultingSession_State) + self.SITE_STATE = site_state + self.STATE = state + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 2, 0, 2) # Sehr kompakt + layout.setSpacing(10) + layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) + ROW_HEIGHT = 70 - # init_rec = backend.initrec_company_get_initial_recording(id_) - # ma_id = cast(ExtMaId, init_rec["Partnersuche__un_suche"]) - # assert ma_id is not None, "no ma_id obtained" - # comp_info = backend.initrec_comp_search_get_info(ma_id) + self.user_field = QLineEdit( + placeholderText="wird über aktiven Nutzer bei Anlage gesetzt" + ) + self.timestamp = QLineEdit() # TODO change to custom date field - # data: dict[str, Any] = {} + self.contact = QLineEdit(placeholderText="Freitext") + self.contact_type = QLineEdit(placeholderText="sollte Dropdown sein") # Dropdown + self.crm_link = QLineEdit(placeholderText="CRM Links") - # for feature, value in comp_info.items(): - # if feature in self.COMPANY_FETAURES_MASTER_DATA: - # data[self.COMPANY_FETAURES_MASTER_DATA[feature]] = value + self.plain_text = QPlainTextEdit() + self.response = QPlainTextEdit() - # self.table_master_data.load_data(data) - # self.set_company_name(data["Name:"]) - # self.current_id = id_ + self.btn_delete = QPushButton("✕") + self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen + self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor) + + 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) + + widgets = ( + self.user_field, + self.timestamp, + self.contact, + self.contact_type, + self.crm_link, + self.plain_text, + self.response, + self.btn_delete, + ) + for widget in widgets: + widget.setFixedHeight(ROW_HEIGHT) + layout.addWidget(widget) + + # column stretching: must be matched with header + num_widgets = len(widgets) + for i, widget in enumerate(widgets): + stretch: int + if i == (num_widgets - 1): + stretch = 0 + else: + if "plaintext" in widget.__class__.__name__.lower(): + stretch = 2 + else: + stretch = 1 + + layout.setStretch(i, stretch) + + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + self._sync_state_to_gui() # ?? only needed if entry is from database? + + def _sync_state_to_gui(self) -> None: + # TODO set correct timestamp + 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) + set_widget_value(self.contact, self.STATE.partner) + 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) + + def _sync_gui_to_state(self) -> None: + # TODO: get timestamp from custom field self.STATE.timestamp + self.STATE.timestamp = datetime.datetime.strptime( + self.timestamp.text(), "%d.%m.%Y %H:%M:%S" + ) + 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() + + def get_data(self) -> Page_Consulting_ConsultingSession_State: + self._sync_gui_to_state() + + return self.STATE + + +class Page_Consulting_Table(QWidget): + """child of 'Page_Consulting'""" + + def __init__( + self, + site_state: Page_Consulting_State, + state: Page_Consulting_Table_State, + ) -> None: + super().__init__() + self.setWindowTitle("Native Dynamische Liste") + self.resize(600, 400) + + self.STATE = state + self.SITE_STATE = site_state + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 15, 0, 15) + main_layout.setSpacing(10) + + # --- BUTTON (ADD) --- + self.btn_add = QPushButton("+ Neuen Eintrag hinzufügen") + self.btn_add.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn_add.setObjectName("BlueAddButton") # ID vergeben + main_layout.addWidget(self.btn_add) + self.btn_add.clicked.connect(lambda: self.add_row(None)) + + self.btn_add.setStyleSheet(""" + #BlueAddButton { + background-color: #007acc; + color: white; + border: none; + border-radius: 6px; + padding: 8px 15px; + font-weight: bold; + } + #BlueAddButton:hover { + background-color: #0098ff; + } + """) + + # --- HEADER --- + header_widget = QWidget() + header_layout = QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 5, 0, 5) + header_layout.setSpacing(10) + + lbl_user_field = QLabel("Wer") + lbl_timestamp = QLabel("Wann") + lbl_contact = QLabel("Mit wem") + lbl_contact_type = QLabel("Wie") + lbl_crm_link = QLabel("Was") + lbl_plain_text = QLabel("Anmerkungen") + lbl_response = QLabel("Rückmeldung") + lbl_action_delete = QLabel("") + lbl_action_delete.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl_action_delete.setFixedWidth(30) + + labels = ( + lbl_user_field, + lbl_timestamp, + lbl_contact, + lbl_contact_type, + lbl_crm_link, + lbl_plain_text, + lbl_response, + lbl_action_delete, + ) + + header_font = QFont() + header_font.setBold(True) + for lbl in labels: + lbl.setFont(header_font) + lbl.setStyleSheet("color: #666666;") + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(lbl) + + plaintext_labels = {lbl_plain_text, lbl_response} + + num_labels = len(labels) + for i, widget in enumerate(labels): + stretch: int + if i == (num_labels - 1): + stretch = 0 + else: + if widget in plaintext_labels: + stretch = 2 + else: + stretch = 1 + + header_layout.setStretch(i, stretch) + + main_layout.addWidget(header_widget) + + separator = QFrame() + separator.setFrameShape(QFrame.Shape.HLine) + separator.setFrameShadow(QFrame.Shadow.Sunken) + main_layout.addWidget(separator) + + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) + + self.container_widget = QWidget() + self.container_layout = QVBoxLayout(self.container_widget) + self.container_layout.setContentsMargins(0, 5, 0, 5) + self.container_layout.setSpacing(6) + self.container_layout.addStretch() + + self.scroll_area.setWidget(self.container_widget) + main_layout.addWidget(self.scroll_area) + + # --- STYLING (QSS) --- + # self.setStyleSheet(""" + # QWidget { + # background-color: #1e1e24; + # color: #e0e0e6; + # font-family: 'Segoe UI', sans-serif; + # font-size: 13px; + # } + # #HeaderWidget QLabel { + # font-weight: bold; + # color: #888892; + # font-size: 11px; + # letter-spacing: 1px; + # } + # QLineEdit { + # background-color: #2a2a32; + # border: 1px solid #3f3f4a; + # border-radius: 6px; + # padding: 6px 10px; + # color: white; + # } + # QLineEdit:focus { + # border: 1px solid #007acc; + # } + # QPushButton { + # background-color: #007acc; + # color: white; + # border: none; + # border-radius: 6px; + # padding: 8px 15px; + # font-weight: bold; + # } + # QPushButton:hover { + # background-color: #0098ff; + # } + # #DeleteButton { + # background-color: transparent; + # color: #ff5555; + # border: 1px solid #3f3f4a; + # font-size: 14px; + # padding: 4px; + # } + # #DeleteButton:hover { + # background-color: #ff3333; + # color: white; + # border-color: #ff3333; + # } + # QScrollArea { + # background-color: transparent; + # } + # """) + + if DEBUG: + # add rows for debugging + self.add_row() + self.add_row() + + def add_row( + self, + row_state: Page_Consulting_ConsultingSession_State | None = None, + ) -> 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, + ) + + row = Page_Consulting_ConsultingSession(site_state=self.SITE_STATE, state=row_state) + row.delete_request.connect(self.remove_row) + + index = self.container_layout.count() - 1 + self.container_layout.insertWidget(index, row) + self.STATE.rows.append(row) + + def remove_row( + self, + table_row_widget: Page_Consulting_ConsultingSession, + ) -> None: + self.container_layout.removeWidget(table_row_widget) + self.STATE.rows.remove(table_row_widget) + table_row_widget.deleteLater() + + def reset(self) -> None: + while self.STATE.rows: + self.remove_row(self.STATE.rows[0]) + + def _sync_state_to_gui(self) -> None: + for row_state in self.STATE.row_states: + self.add_row(row_state) + + def _sync_gui_to_state(self) -> None: + row_data: list[Page_Consulting_ConsultingSession_State] = [] + + for i in range(self.container_layout.count() - 1): + item = self.container_layout.itemAt(i) + assert item, "item not found" + widget = item.widget() + assert widget, "no widget obtained" + + if isinstance(widget, Page_Consulting_ConsultingSession): + row_data.append(widget.get_data()) + + assert len(row_data) == len(self.STATE.rows), "mismatch in row count and data length" + self.STATE.row_states = row_data + + def get_data(self) -> Page_Consulting_Table_State: + logger_page_consulting.debug("[Page Consulting -- Table] Syncing GUI to state...") + self._sync_gui_to_state() + + logger_page_consulting.debug("[Page Consulting -- Table]: State = %s", self.STATE) + + return self.STATE + + +#################################################################### def clear_layout( diff --git a/src/wce_crm/logging.py b/src/wce_crm/logging.py index a9a36ca..9d0cfd7 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -37,6 +37,9 @@ logger_get_data.setLevel(logging.DEBUG) logger_auto_form = logger_gui.getChild("get_data_auto_form") logger_auto_form.setLevel(logging.DEBUG) +logger_page_consulting = logger_gui.getChild("page_consulting") +logger_page_consulting.setLevel(logging.DEBUG) + # ** Backend logger_back = logger_base.getChild("backend") logger_back.setLevel(logging.DEBUG)