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,
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(