added page for consulting sessions with dynamic table

This commit is contained in:
2026-07-01 16:36:15 +02:00
parent 10c8b79cb3
commit 1d353d863e
2 changed files with 414 additions and 98 deletions
+411 -98
View File
@@ -28,7 +28,7 @@ from PySide6.QtCore import (
Signal, Signal,
qInstallMessageHandler, qInstallMessageHandler,
) )
from PySide6.QtGui import QAction from PySide6.QtGui import QAction, QFont
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
@@ -77,6 +77,7 @@ from wce_crm.logging import (
logger_auto_form, logger_auto_form,
logger_get_data, logger_get_data,
logger_gui, logger_gui,
logger_page_consulting,
) )
from wce_crm.types import ExtMaId, InitRecId, UserId from wce_crm.types import ExtMaId, InitRecId, UserId
@@ -2883,29 +2884,40 @@ class MasterDataFormWidget(QWidget):
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class Session: class Session:
user_id: UserId | None = None user_id: UserId
user_name: str | None = None user_name: str
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class ConsultingSession: class Page_Consulting_ConsultingSession_State:
user_id: UserId user_id: UserId # must be set with the session (always known)
user_name: str user_name: str # must be set with the session (always known)
timestamp: datetime.datetime cons_session_id: int | None = None # known and not known entries
partner: str timestamp: datetime.datetime = dc.field(
interaction_scheme: str default_factory=lambda: datetime.datetime.now(tz=datetime.UTC)
topic: str # TODO should be linked to something, target unclear (need specification by customer) )
notes: str partner: str = ""
response: 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) @dc.dataclass(slots=True)
class Page_Consulting_State: class Page_Consulting_State:
session: Session | None = None session: Session
un_id: InitRecId | None = None un_id: InitRecId | None = None
pers_id: InitRecId | None = None pers_id: InitRecId | None = None
cons_type: ConsultingType | 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): class Page_Consulting(QWidget):
@@ -2920,10 +2932,8 @@ class Page_Consulting(QWidget):
session: Session, session: Session,
): ):
super().__init__() super().__init__()
self.session = session
# state management # state management
self.STATE = Page_Consulting_State() self.STATE = Page_Consulting_State(session=session)
# self.current_id: InitRecId = -1 # self.current_id: InitRecId = -1
# self.COMPANY_FETAURES_MASTER_DATA: dict[str, str] = { # self.COMPANY_FETAURES_MASTER_DATA: dict[str, str] = {
@@ -2951,7 +2961,7 @@ class Page_Consulting(QWidget):
# --- HEADER --- # --- HEADER ---
header_container = QWidget() header_container = QWidget()
header_container.setMinimumWidth(700) header_container.setMinimumWidth(700)
header_container.setMaximumWidth(1200) header_container.setMaximumWidth(1500)
header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
header_layout = QVBoxLayout(header_container) header_layout = QVBoxLayout(header_container)
header_layout.setContentsMargins(0, 0, 0, 10) header_layout.setContentsMargins(0, 0, 0, 10)
@@ -2984,7 +2994,7 @@ class Page_Consulting(QWidget):
scroll_area = QScrollArea() scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True) scroll_area.setWidgetResizable(True)
scroll_area.setMinimumWidth(700) scroll_area.setMinimumWidth(700)
scroll_area.setMaximumWidth(1200) scroll_area.setMaximumWidth(1500)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# optional: remove frame to look more modern # optional: remove frame to look more modern
scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setFrameShape(QFrame.Shape.NoFrame)
@@ -3009,6 +3019,9 @@ class Page_Consulting(QWidget):
# layout_change_title.addWidget(change_title_btn) # layout_change_title.addWidget(change_title_btn)
# change_title_btn.clicked.connect(self.set_company_name) # change_title_btn.clicked.connect(self.set_company_name)
# container_layout.addLayout(layout_change_title) # 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 = QFrame()
separator2.setFrameShape(QFrame.Shape.HLine) 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_pauschal_btn)
type_layout.addWidget(self.type_individual_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 = QVBoxLayout()
self.layout_sessions.setContentsMargins(0, 0, 0, 0) # self.layout_sessions.setContentsMargins(0, 0, 0, 0)
container_layout.addLayout(self.layout_sessions) # container_layout.addLayout(self.layout_sessions)
self.btn_add_cons_session = QPushButton("+ Hinzufügen") # self.btn_add_cons_session = QPushButton("+ Hinzufügen")
self.btn_add_cons_session.setStyleSheet( # self.btn_add_cons_session.setStyleSheet(
"color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" # "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;"
) # )
self.btn_add_cons_session.clicked.connect(self.add_cons_entry) # self.btn_add_cons_session.clicked.connect(self.add_cons_entry)
self.layout_sessions.addWidget(self.btn_add_cons_session) # self.layout_sessions.addWidget(self.btn_add_cons_session)
self.layout_sessions.addStretch(1) # self.layout_sessions.addStretch(1)
self.cons_entries: list[QWidget] = [] # self.cons_entries: list[QWidget] = []
# self.layout_sessions.addStretch() # self.layout_sessions.addStretch()
container_layout.addStretch() # container_layout.addStretch()
# --- STYLE --- # --- STYLE ---
self.setStyleSheet(""" self.setStyleSheet("""
@@ -3084,45 +3101,53 @@ class Page_Consulting(QWidget):
# } # }
# """) # """)
def add_cons_entry(self) -> None: # def add_cons_entry(self) -> None:
logger_gui.debug("[Consulting Page] Adding entry...") # logger_gui.debug("[Consulting Page] Adding entry...")
row_widget = QWidget() # row_widget = QWidget()
entry_row = QHBoxLayout(row_widget) # entry_row = QHBoxLayout(row_widget)
entry_fields = QHBoxLayout() # entry_fields = QHBoxLayout()
entry_row.addLayout(entry_fields) # entry_row.addLayout(entry_fields)
txt1 = QLineEdit() # txt1 = QLineEdit()
txt2 = QPlainTextEdit() # txt2 = QPlainTextEdit()
txt2.setMaximumHeight(100) # txt2.setMaximumHeight(100)
entry_fields.addWidget(txt1) # entry_fields.addWidget(txt1)
entry_fields.addWidget(txt2) # entry_fields.addWidget(txt2)
del_btn = QPushButton("🗑️") # del_btn = QPushButton("🗑️")
del_btn.setFixedSize(30, 30) # del_btn.setFixedSize(30, 30)
# Lambda with default parameter to delete exactly this(!) box # # Lambda with default parameter to delete exactly this(!) box
del_btn.clicked.connect( # del_btn.clicked.connect(
lambda checked=False, widget=row_widget: self.remove_cons_entry(widget) # lambda checked=False, widget=row_widget: self.remove_cons_entry(widget)
) # )
entry_row.addWidget(del_btn) # entry_row.addWidget(del_btn)
row_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) # row_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
# row_widget.setMaximumHeight(200) # # row_widget.setMaximumHeight(200)
self.cons_entries.append(row_widget) # self.cons_entries.append(row_widget)
self.layout_sessions.insertWidget(self.layout_sessions.count() - 2, row_widget) # self.layout_sessions.insertWidget(self.layout_sessions.count() - 2, row_widget)
def remove_cons_entry( # def remove_cons_entry(
self, # self,
row_widget: QWidget, # row_widget: QWidget,
) -> None: # ) -> None:
logger_gui.debug("[Consulting Page]: Delete clicked...") # logger_gui.debug("[Consulting Page]: Delete clicked...")
self.layout_sessions.removeWidget(row_widget) # self.layout_sessions.removeWidget(row_widget)
row_widget.deleteLater() # row_widget.deleteLater()
self.cons_entries.remove(row_widget) # self.cons_entries.remove(row_widget)
def _debug_get_data_table(self) -> None:
self.table_sessions.get_data()
# TODO placeholder, check if needed # 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: ... def _sync_ui_to_state(self) -> None: ...
@@ -3154,49 +3179,337 @@ class Page_Consulting(QWidget):
self.STATE.pers_id = req.pers_id self.STATE.pers_id = req.pers_id
self.STATE.cons_type = req.cons_type self.STATE.cons_type = req.cons_type
# def set_company_name(self, name: str) -> None: self._sync_state_to_ui()
# if not name:
# name = self.change_title_text.text()
# if not name:
# name = "n.v."
# txt = f"Unternehmen: {name}"
# self.title_company_name.setText(txt)
# def load_from_id( class Page_Consulting_ConsultingSession(QWidget):
# self, """child of 'Page_Consulting_DynamicTable'"""
# 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_}")
# if id_ < 0: delete_request = Signal(QWidget)
# # 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
# if id_ == self.current_id: def __init__(
# logger_gui.debug("ID did not change. Do nothing") self,
# self.current_id = id_ site_state: Page_Consulting_State,
# return 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_) self.user_field = QLineEdit(
# ma_id = cast(ExtMaId, init_rec["Partnersuche__un_suche"]) placeholderText="wird über aktiven Nutzer bei Anlage gesetzt"
# assert ma_id is not None, "no ma_id obtained" )
# comp_info = backend.initrec_comp_search_get_info(ma_id) 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(): self.plain_text = QPlainTextEdit()
# if feature in self.COMPANY_FETAURES_MASTER_DATA: self.response = QPlainTextEdit()
# data[self.COMPANY_FETAURES_MASTER_DATA[feature]] = value
# self.table_master_data.load_data(data) self.btn_delete = QPushButton("")
# self.set_company_name(data["Name:"]) self.btn_delete.setFlat(True) # Entfernt den blockigen Rahmen
# self.current_id = id_ 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( def clear_layout(
+3
View File
@@ -37,6 +37,9 @@ logger_get_data.setLevel(logging.DEBUG)
logger_auto_form = logger_gui.getChild("get_data_auto_form") logger_auto_form = logger_gui.getChild("get_data_auto_form")
logger_auto_form.setLevel(logging.DEBUG) logger_auto_form.setLevel(logging.DEBUG)
logger_page_consulting = logger_gui.getChild("page_consulting")
logger_page_consulting.setLevel(logging.DEBUG)
# ** Backend # ** Backend
logger_back = logger_base.getChild("backend") logger_back = logger_base.getChild("backend")
logger_back.setLevel(logging.DEBUG) logger_back.setLevel(logging.DEBUG)