generated from dopt-python/py311
6558 lines
225 KiB
Python
6558 lines
225 KiB
Python
from __future__ import annotations
|
||
|
||
import copy
|
||
import dataclasses as dc
|
||
import datetime
|
||
import re
|
||
import sys
|
||
import traceback
|
||
from collections import defaultdict
|
||
from collections.abc import Callable, Container, Iterable, Sequence
|
||
from pprint import pformat
|
||
from typing import (
|
||
TYPE_CHECKING,
|
||
Any,
|
||
Final,
|
||
Literal,
|
||
Protocol,
|
||
TypeAlias,
|
||
TypedDict,
|
||
TypeVar,
|
||
cast,
|
||
)
|
||
from typing_extensions import override
|
||
|
||
from dopt_basics.result_pattern import STATUS_HANDLER
|
||
from pydantic import (
|
||
ValidationError,
|
||
)
|
||
from PySide6.QtCore import (
|
||
QDate,
|
||
QDateTime,
|
||
QEvent,
|
||
QKeyCombination,
|
||
QLibraryInfo,
|
||
QLocale,
|
||
QObject,
|
||
Qt,
|
||
QTimer,
|
||
QtMsgType,
|
||
QTranslator,
|
||
Signal,
|
||
qInstallMessageHandler,
|
||
)
|
||
from PySide6.QtGui import QAction, QFont, QKeySequence
|
||
from PySide6.QtWidgets import (
|
||
QAbstractItemView,
|
||
QApplication,
|
||
QBoxLayout,
|
||
QButtonGroup,
|
||
QCalendarWidget,
|
||
QComboBox,
|
||
QCompleter,
|
||
QDateEdit,
|
||
QDialog,
|
||
QDialogButtonBox,
|
||
QFormLayout,
|
||
QFrame,
|
||
QGridLayout,
|
||
QGroupBox,
|
||
QHBoxLayout,
|
||
QHeaderView,
|
||
QLabel,
|
||
QLayout,
|
||
QLineEdit,
|
||
QListWidget,
|
||
QListWidgetItem,
|
||
QMainWindow,
|
||
QMessageBox,
|
||
QPlainTextEdit,
|
||
QPushButton,
|
||
QRadioButton,
|
||
QScrollArea,
|
||
QSizePolicy,
|
||
QStackedWidget,
|
||
QTableWidget,
|
||
QTableWidgetItem,
|
||
QToolButton,
|
||
QVBoxLayout,
|
||
QWidget,
|
||
)
|
||
|
||
import wce_crm.constants
|
||
from wce_crm import types
|
||
from wce_crm.backend import backend
|
||
from wce_crm.constants import TIMEZONE_CEST
|
||
from wce_crm.data_models import (
|
||
COLUMN_SEP,
|
||
INITREC_CONFIGS,
|
||
Beratungsgespraech_Einzelgespraech,
|
||
Beratungsgespraech_Vorgang,
|
||
InitRec,
|
||
Page_CompanyProfile_State,
|
||
Page_Consulting_ConsultingSession_State,
|
||
Page_Consulting_Linking_State,
|
||
Page_Consulting_State,
|
||
Page_Consulting_Table_State,
|
||
Page_InitRec_Form_Data,
|
||
Page_InitRec_Form_State,
|
||
Page_InitRec_State,
|
||
Page_MainPage_State,
|
||
Page_NewEntry_State,
|
||
Session,
|
||
set_page_state,
|
||
translate_pydantic_errors,
|
||
)
|
||
from wce_crm.form_defs import (
|
||
FormField,
|
||
FormFieldType,
|
||
)
|
||
from wce_crm.logging import (
|
||
logger_custom_fields,
|
||
logger_get_data,
|
||
logger_gui,
|
||
logger_initrecform,
|
||
logger_page_consulting,
|
||
logger_page_initrec,
|
||
)
|
||
from wce_crm.types import (
|
||
CompanyProfileConsultationEntry,
|
||
ConsultingType,
|
||
EntityIds,
|
||
EntityType,
|
||
RecId,
|
||
RecordingType,
|
||
)
|
||
|
||
if TYPE_CHECKING:
|
||
from wce_crm.data_models import (
|
||
Module,
|
||
Page_InitRec_Form_State,
|
||
WrapperModule,
|
||
)
|
||
from wce_crm.types import (
|
||
ConsId,
|
||
ExtMaId,
|
||
LinkingConsultationsEntryCompany,
|
||
LinkingConsultationsEntryPerson,
|
||
)
|
||
|
||
K = TypeVar("K")
|
||
V = TypeVar("V")
|
||
T = TypeVar("T")
|
||
W = TypeVar("W", bound="QWidget")
|
||
|
||
|
||
DEBUG: bool = True
|
||
DEBUG_SEARCH_WIDGET: bool = False
|
||
DEBUG_NO_DATABASE: bool = False
|
||
DEBUG_GET_SET: bool = False
|
||
|
||
# fallback if deployed: disable all debug flags
|
||
if not wce_crm.constants.Config.DEVELOPMENT_STATE:
|
||
DEBUG = False
|
||
DEBUG_SEARCH_WIDGET = False
|
||
DEBUG_NO_DATABASE = False
|
||
DEBUG_GET_SET = False
|
||
|
||
|
||
QSS = """
|
||
*[styleClass="stempel"] {
|
||
background-color: #f1f5f9;
|
||
color: #333D4B;
|
||
border: 1px dashed #cbd5e1;
|
||
border-radius: 4px;
|
||
padding: 5px;
|
||
}
|
||
|
||
*[styleClass="stempel"]:focus {
|
||
border: 1px solid #cbd5e1;
|
||
}
|
||
|
||
QPushButton[styleClass="addButton"] {
|
||
background-color: #007acc;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 8px 15px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
QPushButton[styleClass="addButton"]:hover {
|
||
background-color: #0098ff;
|
||
}
|
||
|
||
QPushButton[styleClass="addButton"]:disabled {
|
||
background-color: #cbd5e1;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
QPushButton[styleClass="outlineAddButton"] {
|
||
background-color: transparent;
|
||
color: #007acc;
|
||
border: 1px solid #007acc;
|
||
border-radius: 6px;
|
||
padding: 5px 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
QPushButton[styleClass="outlineAddButton"]:hover {
|
||
background-color: #007acc;
|
||
color: white;
|
||
}
|
||
|
||
QPushButton[styleClass="outlineAddButton"]:disabled {
|
||
/* background-color: #cbd5e1; */
|
||
background-color: transparent;
|
||
color: #94a3b8;
|
||
border: none;
|
||
}
|
||
|
||
QPushButton[styleClass="softAddButton"] {
|
||
background-color: #e0f2fe;
|
||
color: #0369a1;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
QPushButton[styleClass="softAddButton"]:hover {
|
||
background-color: #bae6fd;
|
||
color: #0369a1;
|
||
}
|
||
|
||
QPushButton[styleClass="flatAddButton"] {
|
||
background-color: transparent;
|
||
color: #475569;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
QPushButton[styleClass="flatAddButton"]:hover {
|
||
background-color: #f1f5f9;
|
||
color: #007acc;
|
||
}
|
||
"""
|
||
DROPDOWN_DEFAULT: Final[str] = "--- Bitte wählen ---"
|
||
DYNAMIC_LIST_KEY_PATTERN: Final[re.Pattern] = re.compile(r"-\[(\d+)\]")
|
||
DATETIME_FMT: Final[str] = "%d.%m.%Y %H:%M:%S"
|
||
DATE_FMT: Final[str] = "%d.%m.%Y"
|
||
DEFAULT_READONLY_STYLECLASS: Final[str] = "stempel"
|
||
|
||
|
||
def merge_dicts_to_lists(
|
||
dict_iter: Iterable[dict[K, V]],
|
||
) -> dict[K, list[V]]:
|
||
merged = defaultdict(list)
|
||
|
||
for d in dict_iter:
|
||
for key, value in d.items():
|
||
merged[key].append(value)
|
||
|
||
return dict(merged)
|
||
|
||
|
||
def unmerge_dict_to_list(
|
||
merged_dict: dict[K, list[V]],
|
||
) -> list[dict[K, V]]:
|
||
if not merged_dict:
|
||
return []
|
||
|
||
keys = merged_dict.keys()
|
||
value_lists = merged_dict.values()
|
||
|
||
return [dict(zip(keys, row)) for row in zip(*value_lists)]
|
||
|
||
|
||
def pprint_registry(widget_registry: WidgetRegistry) -> None:
|
||
print("\n\n>>> Widget registry:")
|
||
for key, entry in widget_registry.items():
|
||
print(f"Key: {key}")
|
||
print(f"\twidget: {entry['widget']}")
|
||
print(f"\tfield key: {entry['form_field'].key}")
|
||
print(f"\tfield type: {entry['form_field'].type}")
|
||
|
||
|
||
def pformat_registry(widget_registry: WidgetRegistry) -> str:
|
||
lines: list[str] = []
|
||
lines.append("\n\n>>> Widget registry:")
|
||
for key, entry in widget_registry.items():
|
||
lines.append(f"Key: {key}")
|
||
lines.append(f"\twidget: {entry['widget']}")
|
||
lines.append(f"\tfield key: {entry['form_field'].key}")
|
||
lines.append(f"\tfield type: {entry['form_field'].type}")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
class CustomForm(Protocol):
|
||
def get_form_data(self) -> dict[str, Any] | list[dict[str, Any]]: ...
|
||
|
||
def set_form_data(self, data: Any) -> None: ...
|
||
|
||
def reset(self) -> None: ...
|
||
|
||
def validate(self) -> list[str]: ...
|
||
|
||
def lock(self) -> None: ...
|
||
|
||
def unlock(self) -> None: ...
|
||
|
||
|
||
class CustomFormWidget(QWidget):
|
||
def __init__(
|
||
self,
|
||
form_fields: Sequence[FormField],
|
||
label: str = "Suche",
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.form_fields = form_fields
|
||
self.label = label
|
||
self.prefix = prefix
|
||
|
||
def get_form_data(self) -> dict[str, Any]: ...
|
||
|
||
def set_form_data(self, data: Any) -> None: ...
|
||
|
||
def reset(self) -> None: ...
|
||
|
||
def validate(self) -> list[str]: ...
|
||
|
||
def lock(self) -> None: ...
|
||
|
||
def unlock(self) -> None: ...
|
||
|
||
|
||
def _add_widget_to_layout(
|
||
form_field: FormField[W],
|
||
widget: QWidget,
|
||
target_layout: QLayout,
|
||
widget_only: bool = False,
|
||
) -> None:
|
||
|
||
if isinstance(target_layout, QFormLayout):
|
||
if widget_only:
|
||
target_layout.addRow(widget)
|
||
elif form_field.tooltip:
|
||
tooltip_layout = _add_tooltip(widget, form_field.tooltip)
|
||
target_layout.addRow(form_field.label, tooltip_layout)
|
||
else:
|
||
target_layout.addRow(form_field.label, widget)
|
||
elif isinstance(target_layout, QBoxLayout):
|
||
if widget_only:
|
||
target_layout.addWidget(widget)
|
||
elif form_field.tooltip:
|
||
tooltip_layout = _add_tooltip(widget, form_field.tooltip)
|
||
target_layout.addLayout(tooltip_layout)
|
||
else:
|
||
target_layout.addWidget(widget)
|
||
|
||
|
||
def build_and_add_widget(
|
||
form_field: FormField[W],
|
||
parent_layout: QLayout,
|
||
widget_registry: WidgetRegistry | None = None,
|
||
prefix: str = "",
|
||
) -> tuple[str, W]:
|
||
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
|
||
elif prefix and not form_field.key:
|
||
full_key = f"{prefix}"
|
||
else:
|
||
full_key = form_field.key
|
||
|
||
layout_widget_only: bool = False
|
||
install_no_scroll_filter: bool = True
|
||
|
||
match form_field.type:
|
||
case (
|
||
FormFieldType.DYNAMIC_LIST
|
||
| FormFieldType.DYNAMIC_DROPDOWN_NUMERIC
|
||
| FormFieldType.DYNAMIC_DROPDOWN_OPTION
|
||
| FormFieldType.CUSTOM
|
||
):
|
||
layout_widget_only = True
|
||
install_no_scroll_filter = False
|
||
|
||
match form_field.type:
|
||
case FormFieldType.GROUP:
|
||
raise TypeError("FormField Groups are not supported for direct building")
|
||
|
||
case FormFieldType.TEXT | FormFieldType.TEXT_DATE | FormFieldType.TEXT_DATETIME:
|
||
widget = QLineEdit()
|
||
if form_field.placeholder:
|
||
widget.setPlaceholderText(form_field.placeholder)
|
||
if form_field.fill_value:
|
||
widget.setText(form_field.fill_value)
|
||
if form_field.readonly:
|
||
widget.setReadOnly(True)
|
||
widget.setProperty("styleClass", "stempel")
|
||
|
||
case FormFieldType.TEXT_SEARCH:
|
||
widget = QLineEdit()
|
||
if form_field.placeholder:
|
||
widget.setPlaceholderText(form_field.placeholder)
|
||
else:
|
||
widget.setPlaceholderText("Tippen zum Suchen...")
|
||
|
||
search_completer = QCompleter()
|
||
search_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||
search_completer.setFilterMode(Qt.MatchFlag.MatchContains)
|
||
widget.setCompleter(search_completer)
|
||
|
||
case FormFieldType.LONGTEXT:
|
||
widget = QPlainTextEdit()
|
||
widget.setMaximumHeight(80)
|
||
if form_field.placeholder:
|
||
widget.setPlaceholderText(form_field.placeholder)
|
||
if form_field.readonly:
|
||
widget.setReadOnly(True)
|
||
widget.setProperty("styleClass", "stempel")
|
||
|
||
case FormFieldType.DATE:
|
||
widget = QDateEdit()
|
||
widget.setCalendarPopup(True)
|
||
cal = widget.calendarWidget()
|
||
if cal:
|
||
cal.setFirstDayOfWeek(Qt.DayOfWeek.Monday)
|
||
cal.setGridVisible(True)
|
||
cal.setStyleSheet("""
|
||
QCalendarWidget QAbstractItemView {
|
||
selection-background-color: #2980b9;
|
||
selection-color: white;
|
||
}
|
||
""")
|
||
if form_field.fill_value:
|
||
set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy")
|
||
if not set_date.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.setDate(set_date)
|
||
else:
|
||
widget.setDate(QDate.currentDate())
|
||
|
||
if form_field.readonly:
|
||
widget = QLineEdit()
|
||
widget.setReadOnly(True)
|
||
widget.setProperty("styleClass", "stempel")
|
||
if form_field.fill_value:
|
||
widget.setText(form_field.fill_value)
|
||
|
||
case FormFieldType.DATE_FLEXIBLE:
|
||
widget = FlexibleDateInput()
|
||
if form_field.fill_value:
|
||
set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy")
|
||
if not set_date.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.set_date(set_date)
|
||
|
||
if form_field.readonly:
|
||
widget.setReadOnly(True)
|
||
if form_field.required:
|
||
widget.setRequired(True)
|
||
|
||
case FormFieldType.DATETIME_FLEXIBLE:
|
||
widget = FlexibleDateTimeInput()
|
||
if form_field.fill_value:
|
||
set_datetime = QDateTime.fromString(form_field.fill_value, "dd.MM.yyyy hh:mm")
|
||
if not set_datetime.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.set_datetime(set_datetime)
|
||
|
||
if form_field.readonly:
|
||
widget.setReadOnly(True)
|
||
if form_field.required:
|
||
widget.setRequired(True)
|
||
|
||
case FormFieldType.DROPDOWN:
|
||
widget = QComboBox()
|
||
widget.addItem(DROPDOWN_DEFAULT, None)
|
||
for option in form_field.dropdown_options:
|
||
widget.addItem(option.label, option.data)
|
||
|
||
if form_field.placeholder:
|
||
widget.setPlaceholderText(form_field.placeholder)
|
||
if form_field.fill_value:
|
||
widget.setCurrentText(form_field.fill_value)
|
||
else:
|
||
widget.setCurrentIndex(0)
|
||
if form_field.readonly:
|
||
widget.setEnabled(False)
|
||
widget.setProperty("styleClass", "stempel")
|
||
|
||
case FormFieldType.EXTENDED_DROPDOWN:
|
||
widget = QComboBox()
|
||
widget.setEditable(True)
|
||
widget.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
|
||
completer = widget.completer()
|
||
assert completer
|
||
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||
completer.setFilterMode(Qt.MatchFlag.MatchContains)
|
||
completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||
|
||
widget.addItem(DROPDOWN_DEFAULT, None)
|
||
for option in form_field.dropdown_options:
|
||
widget.addItem(option.label, option.data)
|
||
|
||
if form_field.placeholder:
|
||
line_edit = widget.lineEdit()
|
||
assert line_edit
|
||
line_edit.setPlaceholderText(form_field.placeholder)
|
||
if form_field.fill_value:
|
||
widget.setCurrentText(form_field.fill_value)
|
||
else:
|
||
widget.setCurrentIndex(-1)
|
||
if form_field.readonly:
|
||
widget.setEnabled(False)
|
||
widget.setProperty("styleClass", "stempel")
|
||
|
||
case FormFieldType.DYNAMIC_LIST:
|
||
widget = DynamicListWidget(
|
||
form_field.children,
|
||
form_field.label,
|
||
prefix=f"{full_key}",
|
||
)
|
||
|
||
case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC:
|
||
widget = DynamicDropdownWidgetNumeric(
|
||
form_field.children,
|
||
form_field.label,
|
||
prefix=f"{full_key}",
|
||
)
|
||
|
||
case FormFieldType.DYNAMIC_DROPDOWN_OPTION:
|
||
widget = DynamicDropdownWidgetOption(
|
||
form_field.children,
|
||
form_field.trigger_value,
|
||
form_field.label,
|
||
prefix=f"{full_key}",
|
||
)
|
||
|
||
case FormFieldType.CUSTOM:
|
||
widget_class = CUSTOM_WIDGETS[form_field.custom_widget]
|
||
widget = widget_class(
|
||
form_fields=form_field.children,
|
||
label=form_field.label,
|
||
prefix=f"{full_key}",
|
||
)
|
||
|
||
case _:
|
||
raise NotImplementedError(f"Not supported field type: {form_field.type.value}")
|
||
|
||
if install_no_scroll_filter:
|
||
widget.installEventFilter(no_scroll_filter)
|
||
|
||
_add_widget_to_layout(form_field, widget, parent_layout, widget_only=layout_widget_only)
|
||
|
||
if widget_registry is not None:
|
||
widget_registry[full_key] = {
|
||
"widget": widget,
|
||
"form_field": form_field,
|
||
}
|
||
|
||
return full_key, cast(W, widget)
|
||
|
||
|
||
def _build_ui_recursively(
|
||
schema: Sequence[FormField],
|
||
parent_layout: QFormLayout,
|
||
widget_registry: WidgetRegistry,
|
||
prefix: str = "",
|
||
) -> list[str]:
|
||
keys: list[str] = []
|
||
for field in schema:
|
||
if prefix and field.key:
|
||
full_key = f"{prefix}{COLUMN_SEP}{field.key}" if prefix else field.key
|
||
elif prefix and not field.key:
|
||
full_key = prefix
|
||
else:
|
||
full_key = field.key
|
||
|
||
match field.type:
|
||
case FormFieldType.GROUP:
|
||
group_box = QGroupBox(field.label)
|
||
group_layout = QFormLayout(group_box)
|
||
_build_ui_recursively(
|
||
schema=field.children,
|
||
parent_layout=group_layout,
|
||
widget_registry=widget_registry,
|
||
prefix=f"{full_key}",
|
||
)
|
||
parent_layout.addRow(group_box)
|
||
|
||
case _:
|
||
full_key, _ = build_and_add_widget(
|
||
field,
|
||
parent_layout,
|
||
widget_registry=widget_registry,
|
||
prefix=prefix,
|
||
)
|
||
keys.append(full_key)
|
||
|
||
return keys
|
||
|
||
|
||
def _add_tooltip(
|
||
widget: QWidget,
|
||
tooltip: str,
|
||
) -> QHBoxLayout:
|
||
field_layout = QHBoxLayout()
|
||
field_layout.setContentsMargins(0, 0, 0, 0)
|
||
field_layout.setSpacing(5)
|
||
info_btn = QPushButton("ℹ️")
|
||
info_btn.setFixedSize(28, 27)
|
||
info_btn.setFlat(True)
|
||
info_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||
info_btn.setToolTip(tooltip)
|
||
field_layout.addWidget(widget)
|
||
field_layout.addWidget(info_btn)
|
||
|
||
return field_layout
|
||
|
||
|
||
def reset_form(
|
||
widget_registry: WidgetRegistry,
|
||
) -> None:
|
||
for registry_entry in widget_registry.values(): # type: ignore
|
||
widget = registry_entry["widget"]
|
||
form_field = registry_entry["form_field"]
|
||
|
||
# TODO check if this behaviour is correct in other contexts, deactivated because of
|
||
# company search widget
|
||
# if form_field.readonly:
|
||
# continue
|
||
|
||
if isinstance(widget, QLineEdit):
|
||
widget.clear()
|
||
if form_field.fill_value:
|
||
widget.setText(form_field.fill_value)
|
||
elif isinstance(widget, QPlainTextEdit):
|
||
widget.clear()
|
||
elif isinstance(widget, QDateEdit):
|
||
if form_field.fill_value:
|
||
set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy")
|
||
if not set_date.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.setDate(set_date)
|
||
else:
|
||
widget.setDate(QDate.currentDate())
|
||
elif isinstance(widget, FlexibleDateInput):
|
||
widget.reset()
|
||
if form_field.fill_value:
|
||
set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy")
|
||
if not set_date.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.set_date(set_date)
|
||
elif isinstance(widget, FlexibleDateTimeInput):
|
||
widget.reset()
|
||
if form_field.fill_value:
|
||
set_datetime = QDateTime.fromString(form_field.fill_value, "dd.MM.yyyy hh:mm")
|
||
if not set_datetime.isValid():
|
||
raise ValueError(
|
||
f"Could not parse date field value >{form_field.fill_value}<"
|
||
)
|
||
widget.set_datetime(set_datetime)
|
||
elif isinstance(widget, QComboBox):
|
||
if form_field.fill_value:
|
||
widget.setCurrentText(form_field.fill_value)
|
||
elif form_field.type is FormFieldType.DROPDOWN:
|
||
widget.setCurrentIndex(0)
|
||
else:
|
||
widget.setCurrentIndex(-1)
|
||
elif isinstance(
|
||
widget,
|
||
(
|
||
DynamicListWidget,
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
CustomFormWidget,
|
||
),
|
||
):
|
||
# custom widget classes manage their widgets on their own
|
||
widget.reset()
|
||
|
||
widget.setStyleSheet("")
|
||
|
||
|
||
@dc.dataclass(slots=True)
|
||
class SubForm:
|
||
entry_box: QWidget
|
||
prefix_parent: str
|
||
index: int
|
||
prefix: str = ""
|
||
registry: WidgetRegistry = dc.field(default_factory=dict)
|
||
full_keys: list[str] = dc.field(default_factory=list)
|
||
|
||
def __post_init__(self) -> None:
|
||
self.prefix = f"{self.prefix_parent}-[{self.index}]"
|
||
|
||
def update_index(self, new_idx: int) -> None:
|
||
self.index = new_idx
|
||
self.prefix = f"{self.prefix_parent}-[{self.index}]"
|
||
|
||
|
||
def change_sub_form_widget_registry(
|
||
widget_registry: WidgetRegistry,
|
||
sub_form: SubForm,
|
||
new_idx: int,
|
||
) -> None:
|
||
if sub_form.index == new_idx:
|
||
return
|
||
|
||
old_key_part = sub_form.prefix
|
||
new_key_part = f"{sub_form.prefix_parent}-[{new_idx}]"
|
||
|
||
for key in tuple(widget_registry.keys()):
|
||
splitted = key.split(COLUMN_SEP)
|
||
key_part, rest = splitted[0], splitted[1:]
|
||
|
||
if key_part == old_key_part:
|
||
old_key = COLUMN_SEP.join([old_key_part] + rest)
|
||
new_key = COLUMN_SEP.join([new_key_part] + rest)
|
||
widget_registry[new_key] = widget_registry[old_key]
|
||
del widget_registry[key]
|
||
|
||
sub_form.update_index(new_idx)
|
||
|
||
|
||
def update_sub_forms(
|
||
widget_registry: WidgetRegistry,
|
||
sub_forms: Sequence[SubForm],
|
||
base_label: str = "",
|
||
):
|
||
total_num_sub_forms = len(sub_forms)
|
||
for index, sub_form in enumerate(sub_forms, start=1):
|
||
if isinstance(sub_form.entry_box, QGroupBox) and base_label:
|
||
sub_form.entry_box.setTitle(f"{base_label} {index}")
|
||
change_sub_form_widget_registry(
|
||
widget_registry,
|
||
sub_form,
|
||
index,
|
||
)
|
||
|
||
for key in tuple(widget_registry.keys()):
|
||
matches = DYNAMIC_LIST_KEY_PATTERN.search(key)
|
||
if not matches:
|
||
continue
|
||
|
||
counter_sub_form = int(matches.group(1))
|
||
if counter_sub_form > total_num_sub_forms:
|
||
del widget_registry[key]
|
||
|
||
|
||
def get_form_data(
|
||
widget_registry: WidgetRegistry,
|
||
filter_keys: Container[str] = tuple(),
|
||
) -> dict[str, Any]:
|
||
raw_data: dict[str, Any] = {}
|
||
ignored_keys: list[str] = []
|
||
for key, registry_entry in widget_registry.items():
|
||
value: Any | None = None
|
||
|
||
if filter_keys:
|
||
data_key = key.split(COLUMN_SEP)[-1]
|
||
if data_key not in filter_keys:
|
||
continue
|
||
|
||
widget = registry_entry["widget"]
|
||
form_field = registry_entry["form_field"]
|
||
if form_field.ignore_get_data:
|
||
ignored_keys.append(key)
|
||
continue
|
||
|
||
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)
|
||
|
||
if isinstance(value, dict):
|
||
raw_data.update(value)
|
||
else:
|
||
raw_data[key] = value
|
||
|
||
logger_get_data.debug("[base:get_form_data]>>>>> RAW DATA:\n%s", pformat(raw_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,
|
||
CustomFormWidget,
|
||
),
|
||
):
|
||
# 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,
|
||
) -> None:
|
||
if value is None:
|
||
return
|
||
elif value is True:
|
||
value = "ja"
|
||
elif value is False:
|
||
value = "nein"
|
||
|
||
if isinstance(widget, QLineEdit):
|
||
if isinstance(value, datetime.datetime):
|
||
value = value.astimezone()
|
||
value = value.strftime(DATETIME_FMT)
|
||
elif isinstance(value, datetime.date):
|
||
value = value.strftime(DATE_FMT)
|
||
|
||
widget.setText(str(value))
|
||
|
||
elif isinstance(widget, QPlainTextEdit):
|
||
widget.setPlainText(str(value))
|
||
|
||
elif isinstance(widget, QDateEdit):
|
||
assert isinstance(value, datetime.date)
|
||
set_date = QDate(value.year, value.month, value.day)
|
||
if not set_date.isValid():
|
||
raise ValueError(f"Could not parse date field value >{value}<")
|
||
widget.setDate(set_date)
|
||
elif isinstance(widget, FlexibleDateInput):
|
||
assert isinstance(value, datetime.datetime)
|
||
try:
|
||
widget.set_pydate(value, raise_error=True)
|
||
except ValueError:
|
||
raise ValueError(f"Could not parse date field value >{value}<")
|
||
elif isinstance(widget, FlexibleDateTimeInput):
|
||
assert isinstance(value, datetime.datetime)
|
||
try:
|
||
widget.set_pydatetime(value, raise_error=True)
|
||
except ValueError:
|
||
raise ValueError(f"Could not parse datetime field value >{value}<")
|
||
elif isinstance(widget, QComboBox):
|
||
index = widget.findData(value)
|
||
if index >= 0:
|
||
widget.setCurrentIndex(index)
|
||
|
||
elif isinstance(widget, DynamicListWidget):
|
||
assert isinstance(value, list)
|
||
widget.set_form_data(value)
|
||
|
||
elif isinstance(
|
||
widget,
|
||
(
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
Grunderfassung_SuchWidget,
|
||
),
|
||
):
|
||
assert isinstance(value, dict)
|
||
widget.set_form_data(value)
|
||
|
||
|
||
def set_form_data(
|
||
widget_registry: WidgetRegistry,
|
||
data: dict[str, Any],
|
||
filter_keys: Container[str] = tuple(),
|
||
) -> None:
|
||
for key, entry in widget_registry.items():
|
||
widget = entry["widget"]
|
||
|
||
if filter_keys:
|
||
data_key = key.split(COLUMN_SEP)[-1]
|
||
if data_key not in filter_keys:
|
||
continue
|
||
|
||
if isinstance(
|
||
widget,
|
||
(
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
Grunderfassung_SuchWidget,
|
||
),
|
||
):
|
||
value = data
|
||
else:
|
||
if key not in data:
|
||
logger_gui.error("Key not in data: %s", key)
|
||
logger_gui.error("Data:\n%s", pformat(data))
|
||
value = data[key]
|
||
|
||
logger_gui.debug("Key set: %s", key)
|
||
|
||
set_widget_value(widget, value)
|
||
|
||
|
||
def validate_form_data(
|
||
widget_registry: WidgetRegistry,
|
||
) -> list[str]:
|
||
errors: list[str] = []
|
||
|
||
for key, registry_entry in widget_registry.items():
|
||
error_post: bool = False
|
||
custom_formatting: bool = False
|
||
widget = registry_entry["widget"]
|
||
form_field = registry_entry["form_field"]
|
||
|
||
if not form_field.readonly:
|
||
widget.setStyleSheet("")
|
||
|
||
if not form_field.required:
|
||
continue
|
||
|
||
dynamic_list_num: str = ""
|
||
if (
|
||
form_field.parent is not None
|
||
and form_field.parent.type is FormFieldType.DYNAMIC_LIST
|
||
):
|
||
# get also number of group for enhanced error messages
|
||
matches = DYNAMIC_LIST_KEY_PATTERN.search(key)
|
||
if matches:
|
||
dynamic_list_num = matches.group(1)
|
||
|
||
if isinstance(widget, (QLineEdit, QDateEdit)):
|
||
if widget.text().strip():
|
||
continue
|
||
error_post = True
|
||
elif isinstance(widget, QPlainTextEdit):
|
||
if widget.toPlainText().strip():
|
||
continue
|
||
error_post = True
|
||
elif isinstance(widget, QComboBox):
|
||
if widget.currentData() is not None:
|
||
continue
|
||
error_post = True
|
||
elif isinstance(widget, (FlexibleDateInput, FlexibleDateTimeInput)):
|
||
valid = widget.validate()
|
||
error_post = not valid
|
||
custom_formatting = True
|
||
elif isinstance(
|
||
widget,
|
||
(
|
||
DynamicListWidget,
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
CustomFormWidget,
|
||
),
|
||
):
|
||
errors_widget = widget.validate()
|
||
if not errors_widget:
|
||
continue
|
||
errors.extend(errors_widget)
|
||
|
||
if not error_post:
|
||
continue
|
||
|
||
error = form_field.label.replace("*", "").replace(":", "")
|
||
if form_field.parent is not None:
|
||
parent_label = form_field.parent.label.replace("*", "").replace(":", "")
|
||
if dynamic_list_num:
|
||
error = f"{parent_label} → {f'{parent_label} {dynamic_list_num}'} → {error}"
|
||
else:
|
||
error = f"{parent_label} → {error}"
|
||
error = error.replace("&&", "&")
|
||
errors.append(error)
|
||
# optical feedback to highlight erroneous cells
|
||
if not custom_formatting:
|
||
widget.setStyleSheet("""
|
||
border: 1px solid #ef4444;
|
||
background-color: #ffe9e9;
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
""")
|
||
|
||
return errors
|
||
|
||
|
||
def validate_child_modules(
|
||
modules: Iterable[Module],
|
||
) -> list[str]:
|
||
errors: list[str] = []
|
||
for mod in modules:
|
||
errors.extend(mod.validate())
|
||
|
||
return errors
|
||
|
||
|
||
def lock_form(
|
||
widget_registry: WidgetRegistry,
|
||
) -> None:
|
||
for registry_entry in widget_registry.values(): # type: ignore
|
||
widget = registry_entry["widget"]
|
||
form_field = registry_entry["form_field"]
|
||
|
||
if form_field.readonly:
|
||
continue
|
||
|
||
if isinstance(
|
||
widget,
|
||
(
|
||
QLineEdit,
|
||
QPlainTextEdit,
|
||
QDateEdit,
|
||
FlexibleDateInput,
|
||
FlexibleDateTimeInput,
|
||
),
|
||
):
|
||
widget.setReadOnly(True)
|
||
elif isinstance(widget, QComboBox):
|
||
widget.setEnabled(False)
|
||
elif isinstance(
|
||
widget,
|
||
(
|
||
DynamicListWidget,
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
CustomFormWidget,
|
||
),
|
||
):
|
||
widget.lock()
|
||
|
||
widget.setProperty("styleClass", "stempel")
|
||
widget.style().unpolish(widget)
|
||
widget.style().polish(widget)
|
||
widget.update()
|
||
|
||
|
||
def unlock_form(
|
||
widget_registry: WidgetRegistry,
|
||
) -> None:
|
||
for registry_entry in widget_registry.values(): # type: ignore
|
||
widget = registry_entry["widget"]
|
||
form_field = registry_entry["form_field"]
|
||
|
||
if form_field.readonly:
|
||
continue
|
||
|
||
if isinstance(
|
||
widget,
|
||
(
|
||
QLineEdit,
|
||
QPlainTextEdit,
|
||
QDateEdit,
|
||
FlexibleDateInput,
|
||
FlexibleDateTimeInput,
|
||
),
|
||
):
|
||
widget.setReadOnly(False)
|
||
elif isinstance(widget, QComboBox):
|
||
widget.setEnabled(True)
|
||
elif isinstance(
|
||
widget,
|
||
(
|
||
DynamicListWidget,
|
||
DynamicDropdownWidgetNumeric,
|
||
DynamicDropdownWidgetOption,
|
||
CustomFormWidget,
|
||
),
|
||
):
|
||
widget.unlock()
|
||
|
||
widget.setProperty("styleClass", "")
|
||
widget.style().unpolish(widget)
|
||
widget.style().polish(widget)
|
||
widget.update()
|
||
|
||
|
||
class Grunderfassung_SuchWidget(CustomFormWidget):
|
||
def __init__(
|
||
self,
|
||
form_fields: Sequence[FormField],
|
||
label: str = "Suche",
|
||
prefix: str = "",
|
||
):
|
||
super().__init__(
|
||
form_fields=form_fields,
|
||
label=label,
|
||
prefix=prefix,
|
||
)
|
||
self.form_fields = form_fields
|
||
self.label = label
|
||
self.prefix = prefix
|
||
self.widget_registry: WidgetRegistry = {}
|
||
self.export_data: dict[str, Any] = {}
|
||
self.PROPERTY_MA_ID = "user_ma_id"
|
||
|
||
main_layout = QVBoxLayout(self)
|
||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.form_layout = QFormLayout()
|
||
self.form_layout.setSpacing(10)
|
||
main_layout.addLayout(self.form_layout)
|
||
|
||
_build_ui_recursively(
|
||
self.form_fields,
|
||
self.form_layout,
|
||
self.widget_registry,
|
||
self.prefix,
|
||
)
|
||
|
||
self.COMPANY_SEARCH_INPUT_KEY = "un_suche"
|
||
lookup = search_widgets_by_key(self.widget_registry, self.COMPANY_SEARCH_INPUT_KEY)
|
||
assert len(lookup) == 1
|
||
self.company_search_input = cast(QComboBox, lookup[0]["widget"])
|
||
self.company_search_input.currentIndexChanged.connect(self._selected_company)
|
||
|
||
self.PERSON_SEARCH_INPUT_KEY = "person_suche"
|
||
lookup = search_widgets_by_key(self.widget_registry, self.PERSON_SEARCH_INPUT_KEY)
|
||
assert len(lookup) == 1
|
||
self.person_search_input = cast(QComboBox, lookup[0]["widget"])
|
||
self.person_search_input.currentIndexChanged.connect(self._selected_person)
|
||
|
||
self.CHANNEL_AWARENESS_KEY = "kanal_aufmerksamkeit"
|
||
lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_AWARENESS_KEY)
|
||
assert len(lookup) == 1
|
||
self.channel_awareness = cast(QComboBox, lookup[0]["widget"])
|
||
|
||
self.DATA_EXPORT_FIELDS = (
|
||
self.COMPANY_SEARCH_INPUT_KEY,
|
||
self.PERSON_SEARCH_INPUT_KEY,
|
||
self.CHANNEL_AWARENESS_KEY,
|
||
)
|
||
|
||
self.company_widgets: dict[str, QWidget] = {}
|
||
self.person_widgets: dict[str, QWidget] = {}
|
||
self.properties_company: tuple[str, ...] = (
|
||
"ma_unternehmensname",
|
||
"ma_strasse",
|
||
"ma_hausnummer",
|
||
"ma_plz",
|
||
"ma_ort",
|
||
)
|
||
self.properties_person: tuple[str, ...] = (
|
||
"an_titel",
|
||
"an_anrede",
|
||
"an_nachname",
|
||
"an_vorname",
|
||
"an_festnetz",
|
||
"an_mobil",
|
||
"an_mail",
|
||
"an_position",
|
||
)
|
||
|
||
for entry in self.widget_registry.values():
|
||
field = entry["form_field"]
|
||
widget = entry["widget"]
|
||
|
||
if field.info in self.properties_company:
|
||
self.company_widgets[field.info] = widget
|
||
elif field.info in self.properties_person:
|
||
self.person_widgets[field.info] = widget
|
||
|
||
if DEBUG_SEARCH_WIDGET:
|
||
button = QPushButton("Print Registry")
|
||
button.clicked.connect(self.print_registry)
|
||
main_layout.addWidget(button)
|
||
button = QPushButton("Reset form")
|
||
button.clicked.connect(self.reset)
|
||
main_layout.addWidget(button)
|
||
button = QPushButton("Get form data")
|
||
button.clicked.connect(self.get_form_data)
|
||
main_layout.addWidget(button)
|
||
button = QPushButton("Set form data")
|
||
button.clicked.connect(self.set_form_data)
|
||
main_layout.addWidget(button)
|
||
button = QPushButton("Validate")
|
||
button.clicked.connect(self.validate)
|
||
main_layout.addWidget(button)
|
||
|
||
self.update_company_data()
|
||
|
||
def print_registry(self) -> None:
|
||
pprint_registry(self.widget_registry)
|
||
|
||
def fill_out_company(
|
||
self,
|
||
data: types.CompanyInfo,
|
||
) -> None:
|
||
for key, widget in self.company_widgets.items():
|
||
if key not in data:
|
||
raise KeyError(
|
||
(
|
||
f"Key: {key} not found in company info. Add the key "
|
||
f"to the info property in the form field definition"
|
||
)
|
||
)
|
||
set_widget_value(widget, data[key])
|
||
|
||
def fill_out_person(
|
||
self,
|
||
data: types.ContactPersonInfo,
|
||
) -> None:
|
||
for key, widget in self.person_widgets.items():
|
||
if key not in data:
|
||
raise KeyError(
|
||
(
|
||
f"Key: {key} not found in company info. Add the key "
|
||
f"to the info property in the form field definition"
|
||
)
|
||
)
|
||
set_widget_value(widget, data[key])
|
||
|
||
def _clear_company_fields(self) -> None:
|
||
self.person_search_input.clear()
|
||
self.channel_awareness.setCurrentIndex(0)
|
||
for widget in self.company_widgets.values():
|
||
widget = cast(QLineEdit, widget)
|
||
widget.clear()
|
||
|
||
def _clear_person_fields(self) -> None:
|
||
for widget in self.person_widgets.values():
|
||
widget = cast(QLineEdit, widget)
|
||
widget.clear()
|
||
|
||
def update_company_data(self) -> None:
|
||
self.company_search_input.clear()
|
||
self.company_search_input.addItem(DROPDOWN_DEFAULT, None)
|
||
search_choices = backend.initrec_comp_search_choices()
|
||
for item, db_index in search_choices:
|
||
self.company_search_input.addItem(item, db_index)
|
||
self.company_search_input.setCurrentIndex(-1)
|
||
|
||
def update_person_data(
|
||
self,
|
||
ma_id: int | None,
|
||
) -> None:
|
||
self.person_search_input.clear()
|
||
self.person_search_input.addItem(DROPDOWN_DEFAULT, None)
|
||
search_choices = backend.initrec_comp_contact_person_search_choices(ma_id, True)
|
||
for item, db_index in search_choices:
|
||
self.person_search_input.addItem(item, db_index)
|
||
self.person_search_input.setCurrentIndex(0)
|
||
|
||
def _selected_company(
|
||
self,
|
||
index: int,
|
||
) -> None:
|
||
ma_id = self.company_search_input.itemData(index)
|
||
if ma_id is None or index == (-1):
|
||
self._clear_company_fields()
|
||
return
|
||
data = backend.initrec_comp_search_get_info(
|
||
ma_id=ma_id,
|
||
)
|
||
self.fill_out_company(data)
|
||
self.update_person_data(ma_id)
|
||
|
||
def _selected_person(
|
||
self,
|
||
index: int,
|
||
) -> None:
|
||
an_id = self.person_search_input.itemData(index)
|
||
if an_id is None or index == (-1):
|
||
self._clear_person_fields()
|
||
return
|
||
|
||
data = backend.initrec_comp_contact_person_search_get_info(
|
||
an_id=an_id,
|
||
)
|
||
self.fill_out_person(data)
|
||
|
||
@override
|
||
def reset(self) -> None:
|
||
reset_form(self.widget_registry)
|
||
self._clear_company_fields()
|
||
self._clear_person_fields()
|
||
|
||
@override
|
||
def validate(self) -> list[str]:
|
||
errors = validate_form_data(self.widget_registry)
|
||
return errors
|
||
|
||
@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("Form data CustomWidget:\n%s", pformat(form_data))
|
||
|
||
return form_data
|
||
|
||
@override
|
||
def set_form_data(
|
||
self,
|
||
data: dict[str, Any],
|
||
) -> None:
|
||
if DEBUG_SEARCH_WIDGET:
|
||
if not self.export_data:
|
||
raise RuntimeError("No data. Get form data first.")
|
||
data = self.export_data
|
||
|
||
set_form_data(self.widget_registry, data, filter_keys=self.DATA_EXPORT_FIELDS)
|
||
|
||
@override
|
||
def lock(
|
||
self,
|
||
) -> None:
|
||
lock_form(self.widget_registry)
|
||
|
||
@override
|
||
def unlock(
|
||
self,
|
||
) -> None:
|
||
unlock_form(self.widget_registry)
|
||
|
||
|
||
def enhanced_label(
|
||
base_label: str,
|
||
add_text: str,
|
||
add_colon: bool = False,
|
||
) -> str:
|
||
label = base_label.strip().replace("*", "").replace(":", "")
|
||
if add_text:
|
||
label = label + f" {add_text}"
|
||
if add_colon and not label.endswith(":"):
|
||
label += ":"
|
||
|
||
return label
|
||
|
||
|
||
class WidgetRegistryEntry(TypedDict):
|
||
widget: QWidget
|
||
form_field: FormField
|
||
|
||
|
||
WidgetRegistry: TypeAlias = dict[str, WidgetRegistryEntry]
|
||
|
||
|
||
def search_widgets_by_key(
|
||
widget_registry: WidgetRegistry,
|
||
key_part: str,
|
||
) -> list[WidgetRegistryEntry]:
|
||
"""
|
||
needed for custom logic of auto-built forms,
|
||
search for specific keys and obtain the widget to assign
|
||
special logic or callbacks to them
|
||
"""
|
||
hits: list[WidgetRegistryEntry] = []
|
||
for key, entry in widget_registry.items():
|
||
if key_part in key:
|
||
hits.append(entry)
|
||
|
||
return hits
|
||
|
||
|
||
class InitRecForm(QWidget):
|
||
"""a widget, which is managed by a code-defined field definition collection"""
|
||
|
||
update_triggered = Signal() # formular saved (data changed for front page)
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_InitRec_Form_State,
|
||
) -> None:
|
||
super().__init__()
|
||
self.STATE = state
|
||
self.SESSION_USER_KEY: Final[str] = "Metadaten_nutzer"
|
||
self.cfg = state.cfg
|
||
self.setStyleSheet("""
|
||
QGroupBox {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
border: 1px solid #cbd5e1; /* Heller, moderner Rahmen */
|
||
border-radius: 8px; /* Abgerundete Ecken */
|
||
margin-top: 15px; /* Platz für die Überschrift schaffen */
|
||
padding-top: 15px; /* Abstand zwischen Rahmen und erstem Feld */
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
subcontrol-position: top left; /* Überschrift oben links */
|
||
padding: 0 5px; /* Etwas Luft links und rechts vom Text */
|
||
color: #334155; /* Dunkelgraue Schrift */
|
||
}
|
||
QComboBox {
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 4px;
|
||
padding: 5px;
|
||
}
|
||
QComboBox:disabled {
|
||
background-color: #f1f5f9; /* Helles System-Grau */
|
||
color: #333D4B; /* Gut lesbare, aber gedeckte Schrift */
|
||
border: 1px dashed #cbd5e1; /* Gestrichelter Rand wie beim Datum */
|
||
}
|
||
QComboBox::drop-down:disabled {
|
||
border: none;
|
||
image: none;
|
||
}
|
||
""")
|
||
# --- special funtionality ---
|
||
self.no_scroll_filter = NoScrollFilter(self)
|
||
|
||
# --- LAYOUT ---
|
||
self.main_layout = QVBoxLayout(self)
|
||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
if DEBUG:
|
||
separator1 = QFrame()
|
||
separator1.setFrameShape(QFrame.Shape.HLine)
|
||
separator1.setFrameShadow(QFrame.Shadow.Sunken)
|
||
self.main_layout.addWidget(separator1)
|
||
|
||
self.registry_button = QPushButton("Ausgabe Widget Registry")
|
||
self.registry_button.clicked.connect(self._print_registry)
|
||
self.registry_button.setFixedHeight(35)
|
||
self.main_layout.addWidget(self.registry_button)
|
||
|
||
# self.test_button = QPushButton("Initialisiere Laden")
|
||
# self.test_button.clicked.connect(self.load_from_ids)
|
||
# self.test_button.setFixedHeight(35)
|
||
# self.test_button.setSizePolicy(
|
||
# QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
# )
|
||
# self.main_layout.addWidget(self.test_button)
|
||
|
||
button_get = QPushButton("GET DATA")
|
||
button_get.setFixedHeight(35)
|
||
button_get.clicked.connect(self._get_data)
|
||
self.main_layout.addWidget(button_get)
|
||
button_set = QPushButton("SET DATA")
|
||
button_set.setFixedHeight(35)
|
||
button_set.clicked.connect(self._set_data)
|
||
self.main_layout.addWidget(button_set)
|
||
|
||
id_field_layout = QHBoxLayout()
|
||
id_field_layout.setContentsMargins(0, 0, 0, 0)
|
||
id_field_layout.setSpacing(5)
|
||
id_field_label = QLabel("ID Datenbank:")
|
||
self.id_field_input = QLineEdit()
|
||
id_field_layout.addWidget(id_field_label)
|
||
id_field_layout.addWidget(self.id_field_input)
|
||
self.main_layout.addLayout(id_field_layout)
|
||
button_db_index = QPushButton("Setze DB Index")
|
||
button_db_index.setFixedHeight(35)
|
||
button_db_index.clicked.connect(self._set_db_index)
|
||
self.main_layout.addWidget(button_db_index)
|
||
|
||
separator2 = QFrame()
|
||
separator2.setFrameShape(QFrame.Shape.HLine)
|
||
separator2.setFrameShadow(QFrame.Shadow.Sunken)
|
||
self.main_layout.addWidget(separator2)
|
||
self.main_layout.addSpacing(5)
|
||
|
||
self.main_layout.addSpacing(10)
|
||
|
||
# buttons (save and reset)
|
||
self.add_buttons = self.cfg.add_buttons
|
||
self.edit_buttons: EditButtons | None = None
|
||
if self.add_buttons:
|
||
self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=True)
|
||
modify_btn = self.edit_buttons.modify_button
|
||
assert modify_btn
|
||
self.modify_btn = modify_btn
|
||
reset_btn = self.edit_buttons.reset_btn
|
||
assert reset_btn
|
||
self.reset_btn = reset_btn
|
||
self.save_btn = self.edit_buttons.save_btn
|
||
self.delete_btn = self.edit_buttons.delete_btn
|
||
|
||
self.modify_btn.modify_activated.connect(self.unlock)
|
||
self.modify_btn.modify_deactivated.connect(self.lock)
|
||
# self.save_btn.clicked.connect(self.save_data)
|
||
self.reset_btn.clicked.connect(self.reset_form)
|
||
# self.delete_btn.clicked.connect(self._delete_data)
|
||
|
||
self.main_layout.addWidget(self.edit_buttons)
|
||
|
||
self.top_level_form_layout = QFormLayout()
|
||
self.main_layout.addLayout(self.top_level_form_layout)
|
||
self.top_level_form_layout.setSpacing(10)
|
||
|
||
self.form_fields = self.cfg.form_fields
|
||
self.widget_registry: WidgetRegistry = {}
|
||
|
||
_build_ui_recursively(
|
||
self.form_fields,
|
||
self.top_level_form_layout,
|
||
self.widget_registry,
|
||
)
|
||
|
||
self.debug_form_data: dict[str, Any] = {}
|
||
|
||
logger_initrecform.debug(
|
||
"[InitRecForm] Initialised Auto Form Widget. Registry:%s",
|
||
pformat_registry(self.widget_registry),
|
||
)
|
||
|
||
def _print_registry(self) -> None:
|
||
pprint_registry(self.widget_registry)
|
||
|
||
def _set_db_index(self) -> None:
|
||
try:
|
||
index = int(self.id_field_input.text())
|
||
except ValueError:
|
||
index = None
|
||
self.STATE.ids.un_id = index
|
||
self.STATE.ids.pers_id = index
|
||
logger_gui.debug("[InitRecForm] Set index to %s, new state: %s", index, self.STATE)
|
||
|
||
# def _disable_save(self) -> None:
|
||
# assert self.save_btn is not None
|
||
# assert self.edit_buttons is not None
|
||
# self.save_btn.setEnabled(False)
|
||
# self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled)
|
||
|
||
# def _enable_save(
|
||
# self,
|
||
# timeout: int = 3000,
|
||
# ) -> None:
|
||
# assert self.save_btn is not None
|
||
# assert self.edit_buttons is not None
|
||
# QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
|
||
# QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
|
||
# self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled)
|
||
|
||
def relevant_ids_set(self) -> bool:
|
||
return self.STATE.ids.valid(self.STATE.ent_type)
|
||
|
||
# def _activate_delete(self) -> None:
|
||
# if self.STATE.ids.valid():
|
||
# self.delete_btn.setEnabled(True)
|
||
# else:
|
||
# self.delete_btn.setEnabled(False)
|
||
|
||
# def _delete_data(self) -> None:
|
||
# assert self.STATE.ids.valid(), "deletion initialised despite IDs are not properly set"
|
||
# confirm = get_user_confirmation(
|
||
# self,
|
||
# "Löschen bestätigen",
|
||
# "Soll der Eintrag wirklich gelöscht werden?",
|
||
# )
|
||
# if not confirm:
|
||
# return
|
||
|
||
# self.STATE.geloescht = True
|
||
# self.save_data()
|
||
# self.update_triggered.emit()
|
||
|
||
# !! must be part of the managing page
|
||
# def load_from_ids(
|
||
# self,
|
||
# ids: NewEntityIds,
|
||
# ) -> None:
|
||
# if DEBUG_NO_DATABASE:
|
||
# return
|
||
|
||
# logger_initrecform.info("[InitRecForm] Load data method...")
|
||
# logger_initrecform.debug("[InitRecForm] Lookup ID: %s", ids)
|
||
# self.reset_form()
|
||
|
||
# if ids.empty(self.STATE.recording_type):
|
||
# logger_initrecform.debug("[InitRecForm] Loading: Lookup ID NULL. Do nothing!")
|
||
# return
|
||
# logger_initrecform.debug("Load from DB:")
|
||
# from_db = self.cfg.from_db(ids)
|
||
|
||
# # TODO add path to avoid unnecessary loading (where the same ID is loaded
|
||
# # twice in a row) -- use an internal marker (not state-related)
|
||
|
||
# # logger_auto_form.debug(
|
||
# # "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data)
|
||
# # )
|
||
# logger_initrecform.debug(
|
||
# "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...",
|
||
# pformat(from_db.db_data),
|
||
# )
|
||
# model = self.cfg.pydantic_model(**from_db.db_data)
|
||
# logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.")
|
||
# logger_initrecform.debug("[InitRecForm] Convert to GUI structure...")
|
||
# form_data = model.to_gui()
|
||
# logger_initrecform.debug("[InitRecForm] Set form data...")
|
||
# logger_initrecform.debug("[InitRecForm] Form data:\n%s", pformat(form_data))
|
||
# self._set_data(form_data)
|
||
# # self.STATE.ids.set_id(id_, self.STATE.ent_type)
|
||
# self.STATE.ids.update(from_db.ids)
|
||
# self.STATE.form_data = self._get_data()
|
||
# logger_initrecform.debug(
|
||
# "[InitRecForm] Model dump vs. get form data:\n%s\n\n%s",
|
||
# pformat(model.model_dump(mode="python")),
|
||
# pformat(self.STATE.form_data),
|
||
# )
|
||
|
||
def _load_state_form_data(self) -> None:
|
||
logger_initrecform.info("[InitRecForm] Load data method...")
|
||
logger_initrecform.debug(
|
||
"[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...",
|
||
pformat(self.STATE.form_data),
|
||
)
|
||
# TODO error catching?
|
||
model = self.cfg.pydantic_model(**self.STATE.form_data)
|
||
logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.")
|
||
logger_initrecform.debug("[InitRecForm] Convert to GUI structure...")
|
||
form_data_gui = model.to_gui()
|
||
self._set_data(form_data_gui)
|
||
|
||
def reset_form(self) -> None:
|
||
reset_form(self.widget_registry)
|
||
self.STATE.ids.update(EntityIds())
|
||
self.STATE.form_data.clear()
|
||
self.STATE.geloescht = False
|
||
|
||
def lock(self) -> None:
|
||
self.STATE.locked = True
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def unlock(self) -> None:
|
||
self.STATE.locked = False
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def _get_data(self) -> dict[str, Any]:
|
||
form_data = get_form_data(self.widget_registry)
|
||
logger_initrecform.debug("[InitRecForm] Call get form data:")
|
||
logger_initrecform.debug("[InitRecForm] Form Data:\n%s", pformat(form_data))
|
||
|
||
if DEBUG_GET_SET:
|
||
self.debug_form_data = form_data
|
||
|
||
return form_data
|
||
|
||
def _set_data(
|
||
self,
|
||
data: dict[str, Any],
|
||
) -> None:
|
||
logger_initrecform.debug("[InitRecForm] Call set form data...")
|
||
if DEBUG_GET_SET:
|
||
data = self.debug_form_data
|
||
|
||
set_form_data(self.widget_registry, data)
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
section: Literal["form", "locking"] | None,
|
||
) -> None:
|
||
# TODO this should be initialised by states analogous to consulting sessions
|
||
# !! loading from IDs is only performed on top-level modules aka pages
|
||
if section is None or section == "form":
|
||
if not self.STATE.geloescht:
|
||
self._load_state_form_data()
|
||
else:
|
||
self.reset_form()
|
||
|
||
if section is None or section == "locking":
|
||
if self.STATE.locked:
|
||
if self.edit_buttons is not None:
|
||
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
|
||
lock_form(self.widget_registry)
|
||
else:
|
||
if self.edit_buttons is not None:
|
||
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
|
||
unlock_form(self.widget_registry)
|
||
# self._activate_delete()
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
form_data = self._get_data()
|
||
# TODO change usage of user ID (needs changes in model and backend)
|
||
if self.SESSION_USER_KEY in form_data:
|
||
form_data[self.SESSION_USER_KEY] = self.STATE.session.user_name
|
||
|
||
for form_key, id_key in self.STATE.cfg.id_mapping.items():
|
||
self.STATE.ids.set_by_name(id_key, form_data[form_key])
|
||
|
||
self.STATE.form_data = form_data
|
||
|
||
def validate(self) -> list[str]:
|
||
return validate_form_data(self.widget_registry)
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_InitRec_Form_State,
|
||
) -> None:
|
||
set_page_state(self.STATE, new_state)
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def get_state(self) -> Page_InitRec_Form_State:
|
||
self._sync_GUI_to_state()
|
||
|
||
return self.STATE
|
||
|
||
# def _load_from_pydantic(
|
||
# self,
|
||
# data: InitRec,
|
||
# ) -> None:
|
||
# # ent_type = self.STATE.recording_type
|
||
# # self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type)
|
||
# self.STATE.ids.update(data.ids)
|
||
# # self.STATE.rec_id = data.rec_id
|
||
# self.STATE.form_data.clear()
|
||
# self.STATE.geloescht = True if data.geloescht else False
|
||
|
||
# self._sync_state_to_GUI(None) # TODO change to correct loading
|
||
|
||
# !! must be part of the managing page
|
||
def save_data(self) -> None:
|
||
pass
|
||
|
||
# def save_data(self) -> None:
|
||
# if self.STATE.ids.empty() and self.STATE.geloescht:
|
||
# logger_initrecform.debug(
|
||
# "[InitRecForm] This was never saved in the database "
|
||
# "and is marked for deletion - ignore."
|
||
# )
|
||
# return
|
||
# elif not self.STATE.ids.empty() and self.STATE.geloescht:
|
||
# logger_initrecform.debug(
|
||
# "[InitRecForm] This was saved in the database "
|
||
# "and is marked for deletion - fast path."
|
||
# )
|
||
# deleted_datetime = datetime.datetime.now(datetime.UTC)
|
||
# assert self.STATE.form_data, "cannot delete entry with uninitialised form data"
|
||
# to_db = InitRec(
|
||
# ids=self.STATE.ids,
|
||
# geloescht=deleted_datetime,
|
||
# db_data={},
|
||
# )
|
||
# returned = self.cfg.to_db(to_db)
|
||
# self._load_from_pydantic(returned)
|
||
# msg_box = get_message_box(
|
||
# QMessageBox.Icon.Information,
|
||
# "Löschen erfolgreich",
|
||
# "Der Eintrag wurde erfolgreich gelöscht",
|
||
# )
|
||
# msg_box.exec()
|
||
|
||
# return
|
||
|
||
# self._disable_save()
|
||
# errors = self.validate()
|
||
|
||
# if errors:
|
||
# logger_initrecform.error(
|
||
# "[InitRecForm] Error during GUI validation phase:\n%s", pformat(errors)
|
||
# )
|
||
# GUI_pydantic_validation_error_handling(errors)
|
||
# self._enable_save()
|
||
# return
|
||
|
||
# logger_initrecform.info("[InitRecForm] Success! All data is valid")
|
||
# logger_initrecform.info("[InitRecForm] Get form data call...")
|
||
# state = self.get_state()
|
||
# form_data = state.form_data
|
||
# assert form_data, "form data not set"
|
||
|
||
# logger_initrecform.debug("[InitRecForm] Get form data\n%s", pformat(form_data))
|
||
|
||
# try:
|
||
# logger_initrecform.debug("[InitRecForm] Call Pydantic...")
|
||
# validated_data = self.cfg.pydantic_model(**form_data)
|
||
# except ValidationError as e:
|
||
# logger_initrecform.error(
|
||
# "[InitRecForm] Error during Pydantic validation phase:\n%s",
|
||
# pformat(e.errors()),
|
||
# )
|
||
# pydantic_validation_error_handling(e)
|
||
# else:
|
||
# if DEBUG_NO_DATABASE:
|
||
# return
|
||
# # !! this code is only called if the 'try' block was successful
|
||
# # save data to database
|
||
# db_data = validated_data.to_db(exclude=self.cfg.ignored_keys)
|
||
# logger_initrecform.debug(
|
||
# "[InitRecForm] Form data with 'exlude' (must be saved in the database):\n%s",
|
||
# pformat(db_data),
|
||
# )
|
||
# deleted_datetime: datetime.datetime | None = None
|
||
# if self.STATE.geloescht:
|
||
# deleted_datetime = datetime.datetime.now(datetime.UTC)
|
||
# try:
|
||
# # TODO change to result pattern
|
||
# to_db = InitRec(
|
||
# ids=self.STATE.ids,
|
||
# # rec_id=self.STATE.rec_id,
|
||
# geloescht=deleted_datetime,
|
||
# db_data=db_data,
|
||
# )
|
||
# returned = self.cfg.to_db(to_db)
|
||
|
||
# self._load_from_pydantic(returned)
|
||
# self.update_triggered.emit()
|
||
# except Exception as err:
|
||
# msg_box = get_message_box(
|
||
# QMessageBox.Icon.Warning,
|
||
# "Speichern fehlgeschlagen",
|
||
# (
|
||
# "Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind "
|
||
# "unten angefügt."
|
||
# ),
|
||
# detailed_text=str(err),
|
||
# )
|
||
# msg_box.exec()
|
||
# else:
|
||
# logger_initrecform.info("[InitRecForm] Data saved successfully")
|
||
# msg_box = get_message_box(
|
||
# QMessageBox.Icon.Information,
|
||
# "Speichern erfolgreich",
|
||
# "Die Daten wurden erfolgreich gespeichert",
|
||
# )
|
||
# msg_box.exec()
|
||
|
||
# finally:
|
||
# # always re-enable save, even if error occurred
|
||
# self._enable_save()
|
||
|
||
|
||
class FlexibleDateInput(QWidget):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._read_only: bool = False
|
||
self._required: bool = False
|
||
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# simple text field allows for easy editing and deletion
|
||
self.line_edit = QLineEdit()
|
||
self.line_edit.setPlaceholderText("TT.MM.JJJJ")
|
||
self.line_edit.setInputMask("99.99.9999;_") # force date structure
|
||
|
||
self.button = QPushButton("📅")
|
||
self.button.setFixedWidth(30)
|
||
|
||
self.line_edit.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||
)
|
||
self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||
|
||
layout.addWidget(self.line_edit)
|
||
layout.addWidget(self.button)
|
||
|
||
self.calendar = QCalendarWidget()
|
||
self.calendar.setWindowFlags(Qt.WindowType.Popup) # calendar as free-floating window
|
||
self.calendar.setFirstDayOfWeek(Qt.DayOfWeek.Monday)
|
||
self.calendar.setGridVisible(True)
|
||
self.calendar.setStyleSheet("""
|
||
QCalendarWidget QAbstractItemView {
|
||
selection-background-color: #2980b9;
|
||
selection-color: white;
|
||
}
|
||
""")
|
||
|
||
self.button.clicked.connect(self.show_calendar)
|
||
self.calendar.activated.connect(self.set_date)
|
||
# signal to validate
|
||
self.line_edit.editingFinished.connect(self._validate_input)
|
||
self.line_edit.installEventFilter(self)
|
||
|
||
self.setReadOnly(False)
|
||
|
||
def eventFilter(
|
||
self,
|
||
watched: QObject,
|
||
event: QEvent,
|
||
):
|
||
# press enter: direct validation
|
||
if watched == self.line_edit:
|
||
if event.type() == QEvent.Type.KeyPress:
|
||
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # type: ignore
|
||
self._validate_input()
|
||
return True # mark event as processed
|
||
elif event.type() == QEvent.Type.FocusOut:
|
||
self._validate_input()
|
||
# no return to not block focus change
|
||
return super().eventFilter(watched, event)
|
||
|
||
def show_calendar(self) -> None:
|
||
if self._read_only:
|
||
return
|
||
pos = self.line_edit.mapToGlobal(self.line_edit.rect().bottomLeft())
|
||
self.calendar.move(pos)
|
||
self.calendar.show()
|
||
|
||
def isRequired(self) -> bool:
|
||
return self._required
|
||
|
||
def setRequired(
|
||
self,
|
||
required: bool,
|
||
) -> None:
|
||
self._required = required
|
||
|
||
def isReadOnly(self) -> bool:
|
||
return self._read_only
|
||
|
||
def setReadOnly(
|
||
self,
|
||
read_only: bool,
|
||
style_class_readonly: str = DEFAULT_READONLY_STYLECLASS,
|
||
):
|
||
self._read_only = read_only
|
||
self.line_edit.setReadOnly(read_only)
|
||
self.button.setEnabled(not read_only)
|
||
|
||
current_text = self.line_edit.text()
|
||
|
||
if read_only:
|
||
self.line_edit.setInputMask("")
|
||
self.line_edit.setPlaceholderText("kein Datum eingetragen")
|
||
pure = (
|
||
current_text.replace(".", "")
|
||
.replace(":", "")
|
||
.replace(" ", "")
|
||
.replace("_", "")
|
||
.strip()
|
||
)
|
||
cleaned_date = current_text if pure else ""
|
||
self.line_edit.setText(cleaned_date)
|
||
self.line_edit.setProperty("styleClass", style_class_readonly)
|
||
else:
|
||
self.line_edit.setInputMask("99.99.9999;_")
|
||
self.line_edit.setPlaceholderText("TT.MM.JJJJ")
|
||
self.line_edit.setProperty("styleClass", "")
|
||
|
||
self.line_edit.style().unpolish(self.line_edit)
|
||
self.line_edit.style().polish(self.line_edit)
|
||
self.line_edit.update()
|
||
|
||
def _validate_input(
|
||
self,
|
||
) -> None:
|
||
logger_gui.debug("[Widget -- FlexibleDateInput] Trigger input validation")
|
||
if self._read_only:
|
||
return
|
||
|
||
focused_widget = QApplication.focusWidget()
|
||
if focused_widget == self.button or focused_widget == self.calendar:
|
||
return
|
||
|
||
text = self.line_edit.text()
|
||
# field empty
|
||
pure_text = text.replace(".", "").replace("_", "").strip()
|
||
if not self._required and not pure_text:
|
||
self.line_edit.setStyleSheet("")
|
||
return
|
||
|
||
# if user wrote input
|
||
date = QDate.fromString(text, "dd.MM.yyyy")
|
||
|
||
if not date.isValid():
|
||
self.line_edit.setStyleSheet("border: 1px solid red; background-color: #FFEEEE;")
|
||
self.line_edit.selectAll()
|
||
# optional: keep focus in field to directly correct entry
|
||
# self.line_edit.setFocus()
|
||
else:
|
||
self.line_edit.setStyleSheet("")
|
||
|
||
def validate(
|
||
self,
|
||
) -> bool:
|
||
logger_custom_fields.debug("[CustomField - FlexDate]: Call validate...")
|
||
self._validate_input()
|
||
date = self.get_date()
|
||
if date and date.isValid():
|
||
logger_custom_fields.debug("[CustomField - FlexDate]: returning valid")
|
||
return True
|
||
logger_custom_fields.debug("[CustomField - FlexDate]: returning invalid")
|
||
return False
|
||
|
||
def set_date(
|
||
self,
|
||
date: QDate,
|
||
) -> None:
|
||
if self._read_only:
|
||
return
|
||
|
||
if date and date.isValid():
|
||
self.line_edit.setText(date.toString("dd.MM.yyyy"))
|
||
else:
|
||
self.line_edit.clear()
|
||
|
||
self.line_edit.setText(date.toString("dd.MM.yyyy"))
|
||
self.calendar.hide()
|
||
|
||
def set_pydate(
|
||
self,
|
||
date: datetime.datetime,
|
||
raise_error: bool = False,
|
||
) -> None:
|
||
date_to_set = QDate(date.year, date.month, date.day)
|
||
if raise_error and not date_to_set.isValid():
|
||
raise ValueError("Could not parse date object correctly")
|
||
|
||
self.set_date(date_to_set)
|
||
|
||
def get_date(self) -> QDate:
|
||
# valid or invalid QDate
|
||
return QDate.fromString(self.line_edit.text(), "dd.MM.yyyy")
|
||
|
||
def get_pydate(self) -> datetime.date | None:
|
||
# valid or invalid QDate
|
||
date = self.get_date()
|
||
if not date.isValid():
|
||
return None
|
||
py_date = cast(datetime.date, date.toPython())
|
||
|
||
return py_date
|
||
|
||
def reset(self) -> None:
|
||
self.calendar.hide()
|
||
self.line_edit.clear()
|
||
|
||
|
||
class FlexibleDateTimeInput(QWidget):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._read_only: bool = False
|
||
self._required: bool = False
|
||
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# Simple text field allows for easy editing and deletion (with Time)
|
||
self.line_edit = QLineEdit()
|
||
self.line_edit.setPlaceholderText("TT.MM.JJJJ hh:mm")
|
||
self.line_edit.setInputMask("99.99.9999 99:99;_") # force datetime structure
|
||
|
||
self.button = QPushButton("📅")
|
||
self.button.setFixedWidth(30)
|
||
|
||
self.line_edit.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||
)
|
||
self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||
|
||
layout.addWidget(self.line_edit)
|
||
layout.addWidget(self.button)
|
||
|
||
self.calendar = QCalendarWidget()
|
||
self.calendar.setWindowFlags(Qt.WindowType.Popup) # calendar as free-floating window
|
||
self.calendar.setFirstDayOfWeek(Qt.DayOfWeek.Monday)
|
||
self.calendar.setGridVisible(True)
|
||
self.calendar.setStyleSheet("""
|
||
QCalendarWidget QAbstractItemView {
|
||
selection-background-color: #2980b9;
|
||
selection-color: white;
|
||
}
|
||
""")
|
||
|
||
self.button.clicked.connect(self.show_calendar)
|
||
self.calendar.activated.connect(self.set_date_from_calendar)
|
||
|
||
# signal to validate
|
||
self.line_edit.editingFinished.connect(self._validate_input)
|
||
self.line_edit.installEventFilter(self)
|
||
|
||
self.setReadOnly(False)
|
||
|
||
def eventFilter(
|
||
self,
|
||
watched: QObject,
|
||
event: QEvent,
|
||
):
|
||
# press enter: direct validation
|
||
if watched == self.line_edit:
|
||
if event.type() == QEvent.Type.KeyPress:
|
||
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # type: ignore
|
||
self._validate_input()
|
||
return True # mark event as processed
|
||
elif event.type() == QEvent.Type.FocusOut:
|
||
self._validate_input()
|
||
# no return to not block focus change
|
||
return super().eventFilter(watched, event)
|
||
|
||
def show_calendar(self) -> None:
|
||
if self._read_only:
|
||
return
|
||
pos = self.line_edit.mapToGlobal(self.line_edit.rect().bottomLeft())
|
||
self.calendar.move(pos)
|
||
self.calendar.show()
|
||
|
||
def isRequired(self) -> bool:
|
||
return self._required
|
||
|
||
def setRequired(
|
||
self,
|
||
required: bool,
|
||
) -> None:
|
||
self._required = required
|
||
|
||
def isReadOnly(self) -> bool:
|
||
return self._read_only
|
||
|
||
def setReadOnly(
|
||
self,
|
||
read_only: bool,
|
||
style_class_readonly: str = DEFAULT_READONLY_STYLECLASS,
|
||
):
|
||
self._read_only = read_only
|
||
self.line_edit.setReadOnly(read_only)
|
||
self.button.setEnabled(not read_only)
|
||
|
||
current_text = self.line_edit.text()
|
||
|
||
if read_only:
|
||
self.line_edit.setInputMask("")
|
||
self.line_edit.setPlaceholderText("kein Zeitstempel eingetragen")
|
||
pure = (
|
||
current_text.replace(".", "")
|
||
.replace(":", "")
|
||
.replace(" ", "")
|
||
.replace("_", "")
|
||
.strip()
|
||
)
|
||
cleaned_datetime = current_text if pure else ""
|
||
self.line_edit.setText(cleaned_datetime)
|
||
self.line_edit.setProperty("styleClass", style_class_readonly)
|
||
else:
|
||
self.line_edit.setInputMask("99.99.9999 99:99;_")
|
||
self.line_edit.setPlaceholderText("TT.MM.JJJJ hh:mm")
|
||
self.line_edit.setProperty("styleClass", "")
|
||
|
||
self.line_edit.style().unpolish(self.line_edit)
|
||
self.line_edit.style().polish(self.line_edit)
|
||
self.line_edit.update()
|
||
|
||
def _validate_input(
|
||
self,
|
||
) -> None:
|
||
logger_gui.debug("[Widget -- FlexibleDateTimeInput] Trigger input validation")
|
||
if self._read_only:
|
||
return
|
||
|
||
focused_widget = QApplication.focusWidget()
|
||
if focused_widget == self.button or focused_widget == self.calendar:
|
||
return
|
||
|
||
text = self.line_edit.text()
|
||
# field empty (ignoring dots, colons, spaces, underscores)
|
||
pure_text = (
|
||
text.replace(".", "").replace(":", "").replace(" ", "").replace("_", "").strip()
|
||
)
|
||
if not self._required and not pure_text:
|
||
self.line_edit.setStyleSheet("")
|
||
return
|
||
|
||
# check logic for QDateTime
|
||
dt = QDateTime.fromString(text, "dd.MM.yyyy hh:mm")
|
||
|
||
if not dt.isValid():
|
||
self.line_edit.setStyleSheet("border: 1px solid red; background-color: #FFEEEE;")
|
||
self.line_edit.selectAll()
|
||
else:
|
||
self.line_edit.setStyleSheet("")
|
||
|
||
def validate(
|
||
self,
|
||
) -> bool:
|
||
logger_custom_fields.debug("[CustomField - FlexDatetime]: Call validate...")
|
||
self._validate_input()
|
||
datetime = self.get_datetime()
|
||
if datetime and datetime.isValid():
|
||
logger_custom_fields.debug("[CustomField - FlexDatetime]: returning valid")
|
||
return True
|
||
logger_custom_fields.debug("[CustomField - FlexDatetime]: returning invalid")
|
||
return False
|
||
|
||
def set_date_from_calendar(
|
||
self,
|
||
date: QDate,
|
||
) -> None:
|
||
if self._read_only:
|
||
return
|
||
|
||
# save time user already provided it
|
||
current_text = self.line_edit.text()
|
||
time_part = "00:00"
|
||
|
||
# extract time portion (last 5 characters of "dd.MM.yyyy hh:mm")
|
||
if len(current_text) >= 16:
|
||
potential_time = current_text[11:16]
|
||
if "_" not in potential_time: # only if time was fully provided
|
||
time_part = potential_time
|
||
|
||
if date and date.isValid():
|
||
formatted_date = date.toString("dd.MM.yyyy")
|
||
self.line_edit.setText(f"{formatted_date} {time_part}")
|
||
self.line_edit.setStyleSheet("")
|
||
else:
|
||
self.line_edit.clear()
|
||
|
||
self.calendar.hide()
|
||
|
||
def set_datetime(
|
||
self,
|
||
dt: QDateTime,
|
||
) -> None:
|
||
if dt and dt.isValid():
|
||
self.line_edit.setText(dt.toString("dd.MM.yyyy hh:mm"))
|
||
self.line_edit.setStyleSheet("")
|
||
else:
|
||
self.line_edit.clear()
|
||
|
||
def set_pydatetime(
|
||
self,
|
||
dt: datetime.datetime,
|
||
raise_error: bool = False,
|
||
) -> None:
|
||
dt_to_set = QDateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0)
|
||
if raise_error and not dt_to_set.isValid():
|
||
raise ValueError("Could not parse datetime object correctly")
|
||
|
||
self.set_datetime(dt_to_set)
|
||
|
||
def get_datetime(self) -> QDateTime:
|
||
# valid or invalid QDateTime
|
||
return QDateTime.fromString(self.line_edit.text(), "dd.MM.yyyy hh:mm")
|
||
|
||
def get_pydatetime(self) -> datetime.datetime | None:
|
||
"""returns current value of the field as UTC datetime
|
||
|
||
Returns
|
||
-------
|
||
datetime.datetime
|
||
UTC datetime object
|
||
"""
|
||
# valid or invalid datetime
|
||
dt = self.get_datetime()
|
||
if not dt.isValid():
|
||
return None
|
||
py_dt = cast(datetime.datetime, dt.toPython())
|
||
py_dt = py_dt.replace(tzinfo=TIMEZONE_CEST)
|
||
|
||
return py_dt.astimezone(datetime.UTC)
|
||
|
||
def reset(self) -> None:
|
||
self.calendar.hide()
|
||
self.line_edit.clear()
|
||
|
||
|
||
class DynamicListWidget(QWidget):
|
||
"""
|
||
a widget, which can generate and manage an arbitrary number of sub forms.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
form_fields: Sequence[FormField],
|
||
label: str = "Eintrag",
|
||
prefix: str = "",
|
||
add_empty_entry: bool = False,
|
||
):
|
||
super().__init__()
|
||
self.form_fields = form_fields
|
||
self.label = label
|
||
self.base_label = enhanced_label(label, add_text="")
|
||
self.prefix = prefix
|
||
self.widget_registry: WidgetRegistry = {}
|
||
|
||
# layout for group component
|
||
self.group_box = QGroupBox(label)
|
||
self.main_layout = QVBoxLayout(self)
|
||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||
# inner layout which contains sub forms and buttons
|
||
self.inner_layout = QVBoxLayout()
|
||
self.main_layout.addWidget(self.group_box)
|
||
self.group_box.setLayout(self.inner_layout)
|
||
# sub forms
|
||
self.sub_forms: list[SubForm] = []
|
||
# button to add more sub forms
|
||
self.add_btn = QPushButton("+ Hinzufügen")
|
||
self.add_btn.setProperty("styleClass", "outlineAddButton")
|
||
# self.add_btn.setStyleSheet(
|
||
# "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)
|
||
|
||
# add empty sub form as initial value
|
||
self.add_empty_entry = add_empty_entry
|
||
if self.add_empty_entry:
|
||
self.add_entry()
|
||
|
||
def add_entry(self):
|
||
number_form = len(self.sub_forms) + 1
|
||
entry_box = QGroupBox(f"{self.label} {number_form}")
|
||
entry_layout = QHBoxLayout(entry_box)
|
||
sub_form = SubForm(entry_box, prefix_parent=self.prefix, index=number_form)
|
||
# widget_registry: WidgetRegistry = {}
|
||
|
||
form_layout = QFormLayout()
|
||
_build_ui_recursively(
|
||
schema=self.form_fields,
|
||
parent_layout=form_layout,
|
||
widget_registry=sub_form.registry,
|
||
# prefix=f"{self.prefix}-[{number_form}]",
|
||
prefix="",
|
||
)
|
||
self.widget_registry.update(sub_form.registry)
|
||
# sub_form.registry = widget_registry
|
||
|
||
del_btn = QPushButton("🗑️")
|
||
del_btn.setFixedSize(30, 30)
|
||
# Lambda with default parameter to delete exactly this(!) box
|
||
del_btn.clicked.connect(lambda checked=False, form=sub_form: self.remove_entry(form))
|
||
|
||
entry_layout.addLayout(form_layout)
|
||
entry_layout.addWidget(del_btn)
|
||
|
||
self.sub_forms.append(sub_form)
|
||
self.inner_layout.insertWidget(self.inner_layout.count() - 1, entry_box)
|
||
self.update_sub_forms()
|
||
|
||
def remove_entry(
|
||
self,
|
||
subform_to_remove: SubForm,
|
||
):
|
||
self.inner_layout.removeWidget(subform_to_remove.entry_box)
|
||
subform_to_remove.entry_box.deleteLater()
|
||
self.sub_forms.remove(subform_to_remove)
|
||
self.update_sub_forms()
|
||
|
||
def update_sub_forms(self):
|
||
update_sub_forms(
|
||
self.widget_registry,
|
||
sub_forms=self.sub_forms,
|
||
base_label=self.base_label,
|
||
)
|
||
|
||
def reset(self) -> None:
|
||
while self.sub_forms:
|
||
self.remove_entry(self.sub_forms[0])
|
||
|
||
if self.add_empty_entry:
|
||
self.add_entry()
|
||
|
||
def validate(self) -> list[str]:
|
||
errors = validate_form_data(self.widget_registry)
|
||
for form in self.sub_forms:
|
||
errors.extend(validate_form_data(form.registry))
|
||
|
||
return errors
|
||
|
||
def get_form_data(self) -> list[dict[str, Any]]:
|
||
logger_get_data.info("DynamicListWidget - Get Data")
|
||
form_data = [get_form_data(sub.registry) for sub in self.sub_forms]
|
||
logger_get_data.debug("DynamicListWidget - Form data:\n%s", pformat(form_data))
|
||
|
||
return form_data
|
||
|
||
def set_form_data(
|
||
self,
|
||
data: list[dict[str, Any]],
|
||
) -> None:
|
||
|
||
while self.sub_forms:
|
||
self.remove_entry(self.sub_forms[0])
|
||
|
||
# empty default row
|
||
if not data and self.add_empty_entry:
|
||
self.add_entry()
|
||
return
|
||
|
||
for sub_form_data in data:
|
||
self.add_entry()
|
||
current_sub_form = self.sub_forms[-1]
|
||
set_form_data(current_sub_form.registry, sub_form_data)
|
||
|
||
def lock(self) -> None:
|
||
lock_form(self.widget_registry)
|
||
self.add_btn.setEnabled(False)
|
||
|
||
def unlock(self) -> None:
|
||
unlock_form(self.widget_registry)
|
||
self.add_btn.setEnabled(True)
|
||
|
||
|
||
class DynamicDropdownWidgetNumeric(QWidget):
|
||
"""
|
||
A Widget, which can generate and manage an arbitrary number of sub forms with additional
|
||
information on a combobox selection (combobox).
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
form_fields: Sequence[FormField],
|
||
label_add_info: str = "Eintrag",
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
|
||
# form_fields = form_fields.children
|
||
if len(form_fields) == 0 or len(form_fields) > 1:
|
||
raise ValueError(
|
||
"Dynamic Dropdown Widget must have only one child, which is a dropdown widget"
|
||
)
|
||
|
||
self.combobox_field = form_fields[0]
|
||
assigned_form_fields = self.combobox_field.children
|
||
if len(assigned_form_fields) == 0 or len(assigned_form_fields) > 1:
|
||
raise ValueError(
|
||
(
|
||
"Dynamic Dropdown Widget's dropdown element must have only one "
|
||
"child, which is a single field definition"
|
||
)
|
||
)
|
||
self.type = type
|
||
self.assigned_form_field = assigned_form_fields[0]
|
||
|
||
self.label_add_info = label_add_info
|
||
self.prefix = prefix
|
||
self.widget_registry: WidgetRegistry = {}
|
||
|
||
# layout for group component
|
||
self.main_layout = QVBoxLayout(self)
|
||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.form_layout = QFormLayout()
|
||
self.main_layout.addLayout(self.form_layout)
|
||
|
||
self.full_keys = _build_ui_recursively(
|
||
[self.combobox_field],
|
||
self.form_layout,
|
||
self.widget_registry,
|
||
prefix=f"{self.prefix}",
|
||
)
|
||
dropdown_widget_entry = tuple(self.widget_registry.values())[0]
|
||
dropdown_widget = dropdown_widget_entry["widget"]
|
||
assert isinstance(dropdown_widget, QComboBox)
|
||
self.dropdown_widget = dropdown_widget
|
||
|
||
self.rows_container = QWidget()
|
||
self.rows_layout = QVBoxLayout(self.rows_container)
|
||
self.rows_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.main_layout.addWidget(self.rows_container)
|
||
|
||
self.sub_forms: list[SubForm] = []
|
||
|
||
self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed)
|
||
|
||
def _selection_changed_changed(
|
||
self,
|
||
text: str,
|
||
) -> None:
|
||
current_count = len(self.sub_forms)
|
||
value: int
|
||
if text == DROPDOWN_DEFAULT:
|
||
value = 0
|
||
else:
|
||
value = int(text)
|
||
|
||
if value > current_count:
|
||
diff = value - current_count
|
||
for _ in range(diff):
|
||
self._add_row()
|
||
elif value < current_count:
|
||
diff = current_count - value
|
||
for _ in range(diff):
|
||
self._remove_row()
|
||
|
||
def _add_row(self) -> None:
|
||
number_form = len(self.sub_forms) + 1
|
||
container = QWidget()
|
||
container.setContentsMargins(0, 0, 0, 0)
|
||
form_layout = QFormLayout(container)
|
||
form_layout.setContentsMargins(10, 0, 0, 0)
|
||
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
|
||
|
||
form_field_def = copy.copy(self.assigned_form_field)
|
||
form_field_def.label = form_field_def.enhanced_label(f"{number_form}")
|
||
|
||
sub_form.full_keys = _build_ui_recursively(
|
||
schema=[form_field_def],
|
||
parent_layout=form_layout,
|
||
widget_registry=sub_form.registry,
|
||
prefix=f"{self.prefix}",
|
||
)
|
||
self.rows_layout.addWidget(container)
|
||
|
||
self.sub_forms.append(sub_form)
|
||
self.update_sub_forms()
|
||
|
||
def _remove_row(self) -> None:
|
||
last_form = self.sub_forms.pop()
|
||
box_to_remove = last_form.entry_box
|
||
|
||
self.rows_layout.removeWidget(box_to_remove)
|
||
box_to_remove.deleteLater()
|
||
self.update_sub_forms()
|
||
|
||
def update_sub_forms(self) -> None:
|
||
update_sub_forms(
|
||
self.widget_registry,
|
||
sub_forms=self.sub_forms,
|
||
)
|
||
|
||
def reset(self) -> None:
|
||
# resets dynamic content when dropdown is set back to default value
|
||
self.dropdown_widget.setCurrentIndex(0)
|
||
|
||
def validate(self) -> list[str]:
|
||
errors = validate_form_data(self.widget_registry)
|
||
for form in self.sub_forms:
|
||
errors.extend(validate_form_data(form.registry))
|
||
|
||
return errors
|
||
|
||
def get_form_data(self) -> dict[str, Any]:
|
||
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("Form data DynamicDropdownWidget:\n%s", pformat(form_data))
|
||
|
||
return form_data
|
||
|
||
def set_form_data(
|
||
self,
|
||
data: dict[str, Any],
|
||
) -> None:
|
||
# data is data of sub forms
|
||
# delete all rows
|
||
while self.sub_forms:
|
||
self._remove_row()
|
||
|
||
# fill in value of combobox field
|
||
assert len(self.full_keys) == 1
|
||
num_subforms: int = -1
|
||
for key in self.full_keys:
|
||
widget = self.widget_registry[key]["widget"]
|
||
value = data[key]
|
||
set_widget_value(widget, value)
|
||
if value is None:
|
||
num_subforms = 0
|
||
else:
|
||
num_subforms = value
|
||
del data[key]
|
||
|
||
logger_get_data.debug(">>>>>>>>> Call set_form_data for DynamicDropdown")
|
||
logger_get_data.debug("Data before unmerge:%s", pformat(data))
|
||
|
||
assert len(self.sub_forms) == num_subforms
|
||
if not self.sub_forms:
|
||
return
|
||
|
||
relevant_keys = self.sub_forms[-1].full_keys # all keys of sub forms are the same
|
||
sub_data = {key: data[key] for key in relevant_keys}
|
||
logger_get_data.debug(">>>> DynamicDropdownWidget. Sub data:\n%s", pformat(sub_data))
|
||
if not sub_data:
|
||
return
|
||
|
||
sub_data = unmerge_dict_to_list(sub_data)
|
||
logger_get_data.debug(">>>> DynamicDropdownWidget. Sub data:\n%s", pformat(sub_data))
|
||
assert len(sub_data) == len(self.sub_forms)
|
||
|
||
for data, sub_form in zip(sub_data, self.sub_forms):
|
||
set_form_data(sub_form.registry, data)
|
||
|
||
def lock(self) -> None:
|
||
lock_form(self.widget_registry)
|
||
for sub_form in self.sub_forms:
|
||
lock_form(sub_form.registry)
|
||
|
||
def unlock(self) -> None:
|
||
unlock_form(self.widget_registry)
|
||
for sub_form in self.sub_forms:
|
||
unlock_form(sub_form.registry)
|
||
|
||
|
||
class DynamicDropdownWidgetOption(QWidget):
|
||
"""
|
||
A Widget, which can generate and manage an arbitrary number of sub forms with additional
|
||
information on a combobox selection (combobox).
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
form_fields: Sequence[FormField],
|
||
trigger_value: str,
|
||
label_add_info: str = "Eintrag",
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
|
||
# form_fields = form_fields.children
|
||
if len(form_fields) == 0 or len(form_fields) > 1:
|
||
raise ValueError(
|
||
"Dynamic Dropdown Widget must have only one child, which is a dropdown widget"
|
||
)
|
||
|
||
self.combobox_field = form_fields[0]
|
||
assigned_form_fields = self.combobox_field.children
|
||
if len(assigned_form_fields) < 1:
|
||
raise ValueError(
|
||
(
|
||
"Option Dynamic Dropdown Widget dropdown element must have at least one "
|
||
"child field definition"
|
||
)
|
||
)
|
||
self.trigger_value = trigger_value
|
||
self.assigned_form_fields = assigned_form_fields
|
||
|
||
self.label_add_info = label_add_info
|
||
self.prefix = prefix
|
||
self.widget_registry: WidgetRegistry = {}
|
||
|
||
# layout for group component
|
||
self.main_layout = QVBoxLayout(self)
|
||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.form_layout = QFormLayout()
|
||
self.main_layout.addLayout(self.form_layout)
|
||
|
||
self.full_keys = _build_ui_recursively(
|
||
[self.combobox_field],
|
||
self.form_layout,
|
||
self.widget_registry,
|
||
prefix=f"{self.prefix}",
|
||
)
|
||
dropdown_widget_entry = tuple(self.widget_registry.values())[0]
|
||
dropdown_widget = dropdown_widget_entry["widget"]
|
||
assert isinstance(dropdown_widget, QComboBox)
|
||
self.dropdown_widget = dropdown_widget
|
||
|
||
self.rows_container = QWidget()
|
||
self.rows_layout = QVBoxLayout(self.rows_container)
|
||
self.rows_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.main_layout.addWidget(self.rows_container)
|
||
|
||
self.sub_forms: list[SubForm] = []
|
||
|
||
self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed)
|
||
|
||
def _selection_changed_changed(
|
||
self,
|
||
text: str,
|
||
) -> None:
|
||
current_count = len(self.sub_forms)
|
||
value: int
|
||
|
||
if text == self.trigger_value:
|
||
value = 1
|
||
else:
|
||
value = 0
|
||
|
||
if value > current_count:
|
||
diff = value - current_count
|
||
for _ in range(diff):
|
||
self._add_row()
|
||
elif value < current_count:
|
||
diff = current_count - value
|
||
for _ in range(diff):
|
||
self._remove_row()
|
||
|
||
def _add_row(self) -> None:
|
||
number_form = len(self.sub_forms) + 1
|
||
container = QWidget()
|
||
container.setContentsMargins(0, 0, 0, 0)
|
||
form_layout = QFormLayout(container)
|
||
form_layout.setContentsMargins(10, 0, 0, 0)
|
||
sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form)
|
||
|
||
form_field_def = copy.copy(self.assigned_form_fields)
|
||
|
||
sub_form.full_keys = _build_ui_recursively(
|
||
schema=form_field_def,
|
||
parent_layout=form_layout,
|
||
widget_registry=sub_form.registry,
|
||
prefix=f"{self.prefix}",
|
||
)
|
||
self.rows_layout.addWidget(container)
|
||
|
||
self.sub_forms.append(sub_form)
|
||
self.update_sub_forms()
|
||
|
||
def _remove_row(self) -> None:
|
||
last_form = self.sub_forms.pop()
|
||
box_to_remove = last_form.entry_box
|
||
|
||
self.rows_layout.removeWidget(box_to_remove)
|
||
box_to_remove.deleteLater()
|
||
self.update_sub_forms()
|
||
|
||
def update_sub_forms(self) -> None:
|
||
update_sub_forms(
|
||
self.widget_registry,
|
||
sub_forms=self.sub_forms,
|
||
)
|
||
|
||
def _get_combined_registry(self, sub_forms_only: bool = False) -> WidgetRegistry:
|
||
whole_registry: WidgetRegistry = {}
|
||
if not sub_forms_only:
|
||
whole_registry = self.widget_registry.copy()
|
||
for sub in self.sub_forms:
|
||
whole_registry.update(sub.registry)
|
||
|
||
return whole_registry
|
||
|
||
def reset(self) -> None:
|
||
# resets dynamic content when dropdown is set back to default value
|
||
self.dropdown_widget.setCurrentIndex(0)
|
||
|
||
def validate(self) -> list[str]:
|
||
errors = validate_form_data(self.widget_registry)
|
||
for form in self.sub_forms:
|
||
errors.extend(validate_form_data(form.registry))
|
||
|
||
return errors
|
||
|
||
def get_form_data(self) -> dict[str, Any]:
|
||
whole_registry = self._get_combined_registry()
|
||
form_data = get_form_data(whole_registry)
|
||
# for sub in self.sub_forms:
|
||
# form_data.update(get_form_data(sub.registry))
|
||
logger_get_data.debug(
|
||
"Form data DynamicDropdownWidgetOption:\n%s", pformat(form_data)
|
||
)
|
||
|
||
return form_data
|
||
|
||
def set_form_data(
|
||
self,
|
||
data: dict[str, Any],
|
||
) -> None:
|
||
# delete all rows
|
||
while self.sub_forms:
|
||
self._remove_row()
|
||
|
||
# fill in value of combobox field
|
||
assert len(self.full_keys) == 1
|
||
num_subforms: int = -1
|
||
data = data.copy()
|
||
for key in self.full_keys:
|
||
widget = self.widget_registry[key]["widget"]
|
||
value = data[key]
|
||
set_widget_value(widget, value)
|
||
logger_get_data.debug("Value: %s", value)
|
||
if value is None or value != self.trigger_value:
|
||
num_subforms = 0
|
||
else:
|
||
num_subforms = 1
|
||
del data[key]
|
||
|
||
whole_registry = self._get_combined_registry(sub_forms_only=True)
|
||
logger_get_data.debug("[DynamicDD-Option] Widget registry for subforms obtained")
|
||
|
||
logger_get_data.debug(
|
||
"[DynamicDD-Option] -- set_form_data -- Data before set of subforms:\n%s",
|
||
pformat(data),
|
||
)
|
||
|
||
assert len(self.sub_forms) == num_subforms
|
||
if not self.sub_forms:
|
||
return
|
||
|
||
set_form_data(whole_registry, data)
|
||
|
||
def lock(self) -> None:
|
||
lock_form(self.widget_registry)
|
||
for sub_form in self.sub_forms:
|
||
lock_form(sub_form.registry)
|
||
|
||
def unlock(self) -> None:
|
||
unlock_form(self.widget_registry)
|
||
for sub_form in self.sub_forms:
|
||
unlock_form(sub_form.registry)
|
||
|
||
|
||
class NoScrollFilter(QObject):
|
||
"""disables scrolling in fields"""
|
||
|
||
def eventFilter(
|
||
self,
|
||
obj: QObject,
|
||
event: QEvent,
|
||
) -> bool:
|
||
if event.type() == QEvent.Type.Wheel:
|
||
# ignored Qt gives event to parent (ScrollArea)
|
||
event.ignore()
|
||
# event was handled
|
||
return True
|
||
|
||
# propagate other events
|
||
return super().eventFilter(obj, event)
|
||
|
||
|
||
class HeaderCell(QLabel):
|
||
"""basic header for table like objects"""
|
||
|
||
def __init__(
|
||
self,
|
||
text: str,
|
||
font_size: int = 12,
|
||
):
|
||
super().__init__(text)
|
||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.setStyleSheet(f"""
|
||
HeaderCell {{
|
||
background-color: #e2e8f0;
|
||
color: #475569;
|
||
font-weight: bold;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
font-size: {font_size}px;
|
||
}}
|
||
""")
|
||
|
||
|
||
class ClickableCell(QFrame):
|
||
"""basic cell which can be clicked"""
|
||
|
||
def __init__(
|
||
self,
|
||
text: str,
|
||
):
|
||
super().__init__()
|
||
self.setStyleSheet("""
|
||
ClickableCell {
|
||
background-color: white;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
}
|
||
ClickableCell:hover {
|
||
background-color: #eff6ff;
|
||
border: 1px solid #60a5fa;
|
||
}
|
||
""")
|
||
layout = QVBoxLayout(self)
|
||
label = QLabel(text)
|
||
label.setWordWrap(True)
|
||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
layout.addWidget(label)
|
||
|
||
|
||
class CompanyProfile_Consultation_ClickableCell(ClickableCell):
|
||
"""cell on the company profile for consultations"""
|
||
|
||
clicked = Signal(CompanyProfileConsultationEntry)
|
||
|
||
def __init__(
|
||
self,
|
||
text: str,
|
||
data_record: CompanyProfileConsultationEntry,
|
||
):
|
||
super().__init__(text=text)
|
||
self.data_record = data_record
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self.clicked.emit(self.data_record)
|
||
|
||
|
||
class MainPage_ClickableCell(ClickableCell):
|
||
"""cell in the table on the startup screen"""
|
||
|
||
clicked = Signal(types.MainPageEntry)
|
||
|
||
def __init__(
|
||
self,
|
||
text: str,
|
||
data_record: types.MainPageEntry,
|
||
):
|
||
super().__init__(text=text)
|
||
self.data_record = data_record
|
||
|
||
logger_gui.debug(
|
||
"Initialised ClickableCell on MainPage with data record:\n%s",
|
||
pformat(self.data_record),
|
||
)
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self.clicked.emit(self.data_record)
|
||
|
||
|
||
class Page_NewEntry(QWidget):
|
||
"""view for new entry for initial recording ("Grunderfassung")"""
|
||
|
||
back_requested = Signal() # Signal back button
|
||
# TODO re-enable with correct settings
|
||
# company_requested = Signal(Page_InitRecCompany_State) # Signal "Unternehmen"
|
||
# person_requested = Signal(Page_InitRecPerson_State) # Signal "Individualperson"
|
||
consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch"
|
||
new_initrec_requested = Signal(Page_InitRec_State) # TODO: rename
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_NewEntry_State,
|
||
):
|
||
super().__init__()
|
||
self.STATE = state
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
|
||
|
||
# Zurück-Button
|
||
back_btn = QPushButton("← Zurück zur Übersicht")
|
||
back_btn.setFixedWidth(200)
|
||
back_btn.clicked.connect(lambda: self.back_requested.emit())
|
||
|
||
# Platzhalter für die Details
|
||
self.title_label = QLabel("Wählen Sie den Typ der Grunderfassung")
|
||
self.title_label.setStyleSheet(
|
||
"font-size: 24px; font-weight: bold; margin-top: 20px;"
|
||
)
|
||
|
||
btn_company = QPushButton("Unternehmen →")
|
||
btn_company.setFixedWidth(300)
|
||
btn_company.setFixedHeight(40)
|
||
btn_company.clicked.connect(self._request_initrec_company)
|
||
|
||
btn_person = QPushButton("Individualperson →")
|
||
btn_person.setFixedWidth(300)
|
||
btn_person.setFixedHeight(40)
|
||
btn_person.clicked.connect(self._request_initrec_person)
|
||
|
||
btn_consulting = QPushButton("Beratungsgespräch →")
|
||
btn_consulting.setFixedWidth(300)
|
||
btn_consulting.setFixedHeight(40)
|
||
btn_consulting.clicked.connect(self._request_consulting)
|
||
|
||
btn_newinitrec = QPushButton("NEUE GRUNDERFASSUNG →")
|
||
btn_newinitrec.setFixedWidth(300)
|
||
btn_newinitrec.setFixedHeight(40)
|
||
btn_newinitrec.clicked.connect(self._request_new_initrec)
|
||
|
||
layout.addWidget(back_btn)
|
||
layout.addSpacing(15)
|
||
layout.addWidget(self.title_label)
|
||
layout.addSpacing(20)
|
||
layout.addWidget(btn_newinitrec)
|
||
layout.addSpacing(20)
|
||
layout.addWidget(btn_company)
|
||
layout.addWidget(btn_person)
|
||
layout.addSpacing(10)
|
||
layout.addWidget(btn_consulting)
|
||
|
||
# TODO configure correctly
|
||
def _request_initrec_company(self) -> None:
|
||
logger_gui.debug("[Page -- NewEntry] Call for company...")
|
||
# ids = EntityIds(type=RecordingType.COMPANY)
|
||
# req_state = Page_InitRecCompany_State(
|
||
# session=self.STATE.session,
|
||
# ids=ids,
|
||
# # un_id=None, # TODO remove
|
||
# locked=False,
|
||
# )
|
||
# logger_gui.debug("[Page -- InitRec] State to call: %s", req_state)
|
||
# self.company_requested.emit(req_state)
|
||
|
||
def _request_initrec_person(self) -> None:
|
||
logger_gui.debug("[Page -- NewEntry] Call for person...")
|
||
# ids = EntityIds(type=RecordingType.PERSON)
|
||
# req_state = Page_InitRecPerson_State(
|
||
# session=self.STATE.session,
|
||
# ids=ids,
|
||
# # pers_id=None, # TODO remove
|
||
# locked=False,
|
||
# )
|
||
# logger_gui.debug("[Page -- InitRec] State to call: %s", req_state)
|
||
# self.person_requested.emit(req_state)
|
||
|
||
def _request_consulting(self) -> None:
|
||
req_state = Page_Consulting_State(
|
||
session=self.STATE.session,
|
||
vorgang_id=None,
|
||
beratungs_typ=ConsultingType.PAUSCHAL,
|
||
locked=False,
|
||
)
|
||
logger_gui.debug("[Page -- InitRec] State to call: %s", req_state)
|
||
self.consulting_requested.emit(req_state)
|
||
|
||
def _request_new_initrec(self) -> None:
|
||
req_state = Page_InitRec_State(
|
||
session=self.STATE.session,
|
||
recording_type=RecordingType.WITH_COMPANY,
|
||
form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}),
|
||
locked=False,
|
||
)
|
||
logger_gui.debug("[Page -- NewInitRec] State to call (IDs): %s", req_state.ids)
|
||
self.new_initrec_requested.emit(req_state)
|
||
|
||
def _sync_state_to_GUI(self) -> None:
|
||
pass
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
pass
|
||
|
||
def validate(self) -> None:
|
||
pass
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_NewEntry_State,
|
||
) -> None:
|
||
set_page_state(self.STATE, new_state)
|
||
self._sync_state_to_GUI()
|
||
|
||
def get_state(self) -> Page_NewEntry_State:
|
||
self._sync_GUI_to_state()
|
||
return self.STATE
|
||
|
||
def save_data(self) -> None:
|
||
pass
|
||
|
||
|
||
# CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config(
|
||
# pydantic_model=Grunderfassung_Unternehmen,
|
||
# # to_db=backend.initrec_company_to_db, # TODO remove
|
||
# # from_db=backend.initrec_company_from_db, # TODO remove
|
||
# ignored_keys=(
|
||
# "Metadaten_erstellung",
|
||
# "Metadaten_aktualisierung",
|
||
# ),
|
||
# form_fields=INITREC_COMP,
|
||
# add_buttons=False,
|
||
# )
|
||
|
||
# CONFIG_GRUNDERFASSUNG_PERSONEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config(
|
||
# pydantic_model=Grunderfassung_Personen,
|
||
# # to_db=backend.initrec_person_to_db, # TODO remove
|
||
# # from_db=backend.initrec_person_from_db, # TODO remove
|
||
# ignored_keys=(
|
||
# "Metadaten_erstellung",
|
||
# "Metadaten_aktualisierung",
|
||
# ),
|
||
# form_fields=INITREC_PERSON,
|
||
# add_buttons=False,
|
||
# )
|
||
|
||
CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = {
|
||
"grunderfassung_unternehmen_suche": Grunderfassung_SuchWidget,
|
||
}
|
||
|
||
|
||
class CollapsibleBox(QWidget):
|
||
def __init__(self, title="", parent=None):
|
||
super().__init__(parent)
|
||
|
||
# 1. Header-Button mit Pfeil
|
||
self.toggle_button = QToolButton()
|
||
self.toggle_button.setText(f" {title}")
|
||
self.toggle_button.setCheckable(True)
|
||
self.toggle_button.setChecked(False)
|
||
self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||
self.toggle_button.setArrowType(Qt.ArrowType.RightArrow)
|
||
self.toggle_button.setStyleSheet(
|
||
"QToolButton { border: none; font-weight: bold; font-size: 13px; }"
|
||
)
|
||
self.toggle_button.toggled.connect(self._on_toggle)
|
||
|
||
# 2. Inhaltsbereich (standardmäßig ausgeblendet)
|
||
self.content_area = QWidget()
|
||
self.content_area.setVisible(False)
|
||
|
||
# Layout des Box-Containers
|
||
main_layout = QVBoxLayout(self)
|
||
main_layout.setContentsMargins(0, 5, 0, 5)
|
||
main_layout.addWidget(self.toggle_button)
|
||
main_layout.addWidget(self.content_area)
|
||
|
||
def _on_toggle(self, checked: bool):
|
||
# Pfeilrichtung ändern & Inhalt ein-/ausblenden
|
||
self.toggle_button.setArrowType(
|
||
Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow
|
||
)
|
||
self.content_area.setVisible(checked)
|
||
|
||
def setContentLayout(
|
||
self,
|
||
layout: QLayout,
|
||
):
|
||
self.content_area.setLayout(layout)
|
||
|
||
def setContentWidget(
|
||
self,
|
||
widget: QWidget,
|
||
):
|
||
layout = QVBoxLayout()
|
||
layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder
|
||
layout.addWidget(widget)
|
||
self.setContentLayout(layout)
|
||
|
||
|
||
class Page_InitRec(QWidget):
|
||
back_main_requested = Signal() # back to main page
|
||
back_requested = Signal() # back button
|
||
update_triggered = Signal() # form saved (data changed for front page)
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_InitRec_State,
|
||
):
|
||
super().__init__()
|
||
self.STATE = state
|
||
# Hauptlayout der Seite
|
||
outer_layout = QHBoxLayout(self)
|
||
vert_layout = QVBoxLayout()
|
||
# main_layout.setContentsMargins(0, 0, 0, 0)
|
||
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)
|
||
|
||
# --- HEADER ---
|
||
header_container = QWidget()
|
||
header_container.setMinimumWidth(700)
|
||
header_container.setMaximumWidth(1000)
|
||
header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
header_layout = QVBoxLayout(header_container)
|
||
header_layout.setContentsMargins(0, 0, 0, 10)
|
||
|
||
back_btn_main = QPushButton("← Zurück zur Übersicht")
|
||
back_btn_main.clicked.connect(lambda: self.back_main_requested.emit())
|
||
back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_main.setMinimumWidth(200)
|
||
back_btn_main.setMaximumWidth(200)
|
||
back_btn_step = QPushButton("← Zurück")
|
||
back_btn_step.clicked.connect(lambda: self.back_requested.emit())
|
||
back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_step.setMinimumWidth(200)
|
||
back_btn_step.setMaximumWidth(200)
|
||
|
||
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
|
||
title = QLabel("Grunderfassung Unternehmen")
|
||
elif self.STATE.recording_type is RecordingType.WITHOUT_COMPANY:
|
||
title = QLabel("Grunderfassung Individualperson")
|
||
else:
|
||
raise RuntimeError(f"Unspecified recording type {self.STATE.recording_type}")
|
||
title.setStyleSheet("font-size: 20px; font-weight: bold;")
|
||
|
||
header_layout.setSpacing(5)
|
||
header_layout.addWidget(back_btn_step)
|
||
header_layout.addWidget(back_btn_main)
|
||
header_layout.addWidget(title)
|
||
vert_layout.addWidget(header_container)
|
||
|
||
if DEBUG:
|
||
separator1 = QFrame()
|
||
separator1.setFrameShape(QFrame.Shape.HLine)
|
||
separator1.setFrameShadow(QFrame.Shadow.Sunken)
|
||
header_layout.addWidget(separator1)
|
||
|
||
btn_gui2state = QPushButton("Sync GUI to state")
|
||
btn_gui2state.clicked.connect(self._sync_GUI_to_state)
|
||
btn_gui2state.setFixedHeight(35)
|
||
btn_gui2state.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
header_layout.addWidget(btn_gui2state)
|
||
|
||
btn_state2gui = QPushButton("Sync state to GUI")
|
||
btn_state2gui.clicked.connect(self._sync_state_to_GUI)
|
||
btn_state2gui.setFixedHeight(35)
|
||
btn_state2gui.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
header_layout.addWidget(btn_state2gui)
|
||
|
||
btn_print_state = QPushButton("Print State")
|
||
btn_print_state.clicked.connect(self._debug_print_state)
|
||
btn_print_state.setFixedHeight(35)
|
||
btn_print_state.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
header_layout.addWidget(btn_print_state)
|
||
|
||
# self.test_button = QPushButton("Initialisiere Laden")
|
||
# self.test_button.clicked.connect(self._load_from_id)
|
||
# self.test_button.setFixedHeight(35)
|
||
# self.test_button.setSizePolicy(
|
||
# QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
# )
|
||
# header_layout.addWidget(self.test_button)
|
||
|
||
# button_get = QPushButton("GET DATA")
|
||
# button_get.setFixedHeight(35)
|
||
# button_get.clicked.connect(self._get_form_data)
|
||
# header_layout.addWidget(button_get)
|
||
# button_set = QPushButton("SET DATA")
|
||
# button_set.setFixedHeight(35)
|
||
# button_set.clicked.connect(self._set_form_data)
|
||
# header_layout.addWidget(button_set)
|
||
|
||
# id_field_layout = QHBoxLayout()
|
||
# id_field_layout.setContentsMargins(0, 0, 0, 0)
|
||
# id_field_layout.setSpacing(5)
|
||
# id_field_label = QLabel("ID Datenbank:")
|
||
# self.id_field_input = QLineEdit()
|
||
# id_field_layout.addWidget(id_field_label)
|
||
# id_field_layout.addWidget(self.id_field_input)
|
||
# header_layout.addLayout(id_field_layout)
|
||
# button_db_index = QPushButton("Setze DB Index")
|
||
# button_db_index.setFixedHeight(35)
|
||
# button_db_index.clicked.connect(self._set_db_index)
|
||
# header_layout.addWidget(button_db_index)
|
||
|
||
separator2 = QFrame()
|
||
separator2.setFrameShape(QFrame.Shape.HLine)
|
||
separator2.setFrameShadow(QFrame.Shadow.Sunken)
|
||
header_layout.addWidget(separator2)
|
||
header_layout.addSpacing(5)
|
||
|
||
# --- BANNER ---
|
||
self.info_banners_container = QWidget()
|
||
self.info_banners: list[InfoBanner] = []
|
||
self.info_banners_layout = QVBoxLayout(self.info_banners_container)
|
||
self.info_banners_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.info_banners_layout.setSpacing(6)
|
||
header_layout.addWidget(self.info_banners_container)
|
||
|
||
# --- BUTTONS ---
|
||
self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=True)
|
||
modify_btn = self.edit_buttons.modify_button
|
||
assert modify_btn
|
||
self.modify_btn = modify_btn
|
||
reset_btn = self.edit_buttons.reset_btn
|
||
assert reset_btn
|
||
self.reset_btn = reset_btn
|
||
self.save_btn = self.edit_buttons.save_btn
|
||
self.delete_btn = self.edit_buttons.delete_btn
|
||
|
||
self.modify_btn.modify_activated.connect(self.unlock)
|
||
self.modify_btn.modify_deactivated.connect(self.lock)
|
||
self.save_btn.clicked.connect(self.save_data)
|
||
self.reset_btn.clicked.connect(self._reset)
|
||
self.delete_btn.clicked.connect(self._delete_data)
|
||
header_layout.addSpacing(5)
|
||
header_layout.addWidget(self.edit_buttons)
|
||
|
||
# --- MAIN CONTENT ---
|
||
container = QWidget()
|
||
# SCROLL AREA
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setMinimumWidth(700)
|
||
scroll_area.setMaximumWidth(1000)
|
||
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
# optional: remove frame to look more modern
|
||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll_area.setWidget(container)
|
||
|
||
vert_layout.addWidget(scroll_area)
|
||
|
||
self.container_layout = QVBoxLayout(container)
|
||
self.container_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# --- AUTO FORM LAYOUT ---
|
||
# now we try to render the layout within the collapsable box
|
||
self.container_layout.addSpacing(20)
|
||
|
||
# ** person
|
||
self.initrec_form_person_box = CollapsibleBox("Angaben zur Person")
|
||
ent_type = EntityType.PERSON
|
||
self.initrec_form_person = InitRecForm(self.STATE.initrec_form_states[ent_type])
|
||
|
||
assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_person.STATE
|
||
|
||
self.STATE.child_modules.append(self.initrec_form_person)
|
||
self.initrec_form_person_box.setContentWidget(self.initrec_form_person)
|
||
self.container_layout.addWidget(self.initrec_form_person_box)
|
||
# container_layout.addSpacing(15)
|
||
# TODO trigger needed?
|
||
# self.initrec_form_person.update_triggered.connect(
|
||
# lambda: self.update_triggered.emit()
|
||
# )
|
||
# self.initrec_form_person.update_triggered.connect(self._auto_form_updated)
|
||
|
||
# --- CUSTOM LOGIC ---
|
||
# ** 'Bundesland' only if 'Inland' selected in 'Stammdaten'
|
||
person_location = search_widgets_by_key(
|
||
self.initrec_form_person.widget_registry,
|
||
f"Stammdaten{COLUMN_SEP}aufenthaltsort",
|
||
)
|
||
assert len(person_location) == 1
|
||
self.person_location = cast(QComboBox, person_location[0]["widget"])
|
||
assert isinstance(self.person_location, QComboBox)
|
||
self.person_location.currentIndexChanged.connect(self._custom_county_selection)
|
||
|
||
selection_county = search_widgets_by_key(
|
||
self.initrec_form_person.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland"
|
||
)
|
||
assert len(selection_county) == 1
|
||
self.selection_county = cast(QComboBox, selection_county[0]["widget"])
|
||
assert isinstance(self.selection_county, QComboBox)
|
||
self.selection_county.setProperty("styleClass", "stempel")
|
||
self.selection_county.setEnabled(False)
|
||
|
||
# ** company
|
||
ent_type = EntityType.COMPANY
|
||
self.initrec_form_company_box = CollapsibleBox("Angaben zum Unternehmen")
|
||
self.initrec_form_company = InitRecForm(self.STATE.initrec_form_states[ent_type])
|
||
self.STATE.child_modules.append(self.initrec_form_company)
|
||
|
||
assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_company.STATE
|
||
|
||
target_idx = self.container_layout.indexOf(self.initrec_form_person_box) - 1
|
||
self.initrec_form_company_box.setContentWidget(self.initrec_form_company)
|
||
self.container_layout.insertWidget(target_idx, self.initrec_form_company_box)
|
||
|
||
# container_layout.addSpacing(15)
|
||
# TODO trigger needed?
|
||
# self.initrec_form_person.update_triggered.connect(
|
||
# lambda: self.update_triggered.emit()
|
||
# )
|
||
# self.initrec_form_person.update_triggered.connect(self._auto_form_updated)
|
||
|
||
# --- CUSTOM LOGIC ---
|
||
# ** fill 'Kontaktperson -> Namen Unternehmen'
|
||
search_res = search_widgets_by_key(
|
||
self.initrec_form_company.widget_registry, "Partnersuche"
|
||
)
|
||
assert len(search_res) == 1
|
||
search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"])
|
||
self.search_widget_trigger = cast(
|
||
QLineEdit, search_widget.company_widgets["ma_unternehmensname"]
|
||
)
|
||
|
||
self.container_layout.addStretch()
|
||
self._update_layout()
|
||
|
||
def _debug_print_state(self) -> None:
|
||
logger_initrecform.info(
|
||
"[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids)
|
||
)
|
||
|
||
def _update_layout(self) -> None:
|
||
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
|
||
# add company
|
||
self.initrec_form_company_box.setVisible(True)
|
||
else:
|
||
self.initrec_form_company_box.setVisible(False)
|
||
|
||
# elif self.initrec_form_company_box is not None:
|
||
# self.container_layout.removeWidget(self.initrec_form_company_box)
|
||
# # ** state
|
||
# assert self.initrec_form_company
|
||
# self.STATE.child_modules.remove(self.initrec_form_company)
|
||
# del self.STATE.initrec_form_states[EntityType.COMPANY]
|
||
|
||
# self.initrec_form_company_box = None
|
||
# self.initrec_form_company = None
|
||
|
||
# def _set_new_initrec_state(
|
||
# self,
|
||
# ent_type: EntityType,
|
||
# ) -> None:
|
||
# initrec_form_state = Page_InitRec_Form_State(
|
||
# session=self.STATE.session,
|
||
# cfg=INITREC_CONFIGS[ent_type],
|
||
# ids=self.STATE.ids,
|
||
# ent_type=ent_type,
|
||
# locked=False,
|
||
# )
|
||
# self.STATE.initrec_form_states[ent_type] = initrec_form_state
|
||
|
||
# ** custom logic
|
||
def _custom_county_selection(self, idx: int) -> None:
|
||
value = self.person_location.itemData(idx)
|
||
if value == "Inland":
|
||
self.selection_county.setEnabled(True)
|
||
self.selection_county.setProperty("styleClass", "")
|
||
self.selection_county.style().unpolish(self.selection_county)
|
||
self.selection_county.style().polish(self.selection_county)
|
||
self.selection_county.update()
|
||
else:
|
||
self.selection_county.setCurrentIndex(0)
|
||
self.selection_county.setEnabled(False)
|
||
self.selection_county.setProperty("styleClass", "stempel")
|
||
self.selection_county.style().unpolish(self.selection_county)
|
||
self.selection_county.style().polish(self.selection_county)
|
||
self.selection_county.update()
|
||
|
||
# ** GUI behaviour
|
||
def _clear_info_banners(self) -> None:
|
||
while self.info_banners:
|
||
banner = self.info_banners[0]
|
||
self.info_banners_layout.removeWidget(banner)
|
||
self.info_banners.remove(banner)
|
||
banner.deleteLater()
|
||
|
||
def _add_info_banner(
|
||
self,
|
||
text: str,
|
||
button_text: str,
|
||
button_func: Callable[[], None] | None = None,
|
||
) -> None:
|
||
if button_text and button_func is None:
|
||
raise ValueError("Can not add button without calling function")
|
||
|
||
banner = InfoBanner(
|
||
text=text,
|
||
button_text=button_text,
|
||
)
|
||
if button_func is not None:
|
||
banner.btn_clicked.connect(button_func)
|
||
self.info_banners.append(banner)
|
||
self.info_banners_layout.addWidget(banner)
|
||
|
||
def _disable_save(self) -> None:
|
||
assert self.save_btn is not None
|
||
assert self.edit_buttons is not None
|
||
self.save_btn.setEnabled(False)
|
||
self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled)
|
||
|
||
def _enable_save(
|
||
self,
|
||
timeout: int = 3000,
|
||
) -> None:
|
||
assert self.save_btn is not None
|
||
assert self.edit_buttons is not None
|
||
QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
|
||
QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
|
||
self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled)
|
||
|
||
def _activate_delete(self) -> None:
|
||
if self._deletion_valid():
|
||
self.delete_btn.setEnabled(True)
|
||
else:
|
||
self.delete_btn.setEnabled(False)
|
||
|
||
def _deletion_valid(self) -> bool:
|
||
# valid_id_states: list[bool] = []
|
||
relevant_ent_types: set[EntityType] = {EntityType.METADATA, EntityType.PERSON}
|
||
if self.STATE.recording_type is RecordingType.WITH_COMPANY:
|
||
relevant_ent_types.add(EntityType.COMPANY)
|
||
|
||
valid_id_states = (
|
||
form.relevant_ids_set()
|
||
for form in self.STATE.child_modules
|
||
if form.STATE.ent_type in relevant_ent_types
|
||
)
|
||
return all(valid_id_states)
|
||
|
||
def _delete_data(self) -> None:
|
||
assert self._deletion_valid(), "deletion initialised despite IDs are not properly set"
|
||
confirm = get_user_confirmation(
|
||
self,
|
||
"Löschen bestätigen",
|
||
"Soll der Eintrag wirklich gelöscht werden?",
|
||
)
|
||
if not confirm:
|
||
return
|
||
|
||
self.STATE.geloescht = True
|
||
self.save_data()
|
||
self.update_triggered.emit()
|
||
|
||
def _auto_form_updated(self) -> None:
|
||
self._sync_GUI_to_state()
|
||
self._sync_state_to_GUI("banners")
|
||
# TODO saving logic to connect company and person if needed
|
||
|
||
def _reset(self) -> None:
|
||
# TODO change to make these calls implicit in child modules via
|
||
# TODO staes
|
||
for form in self.STATE.child_modules:
|
||
form.reset_form()
|
||
self.STATE.ids = EntityIds()
|
||
self.STATE.geloescht = False
|
||
|
||
def _load_database(
|
||
self,
|
||
ids: EntityIds,
|
||
) -> None:
|
||
logger_initrecform.info("[Page -- InitRec] Load data method...")
|
||
logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids)
|
||
self._reset()
|
||
|
||
ids = EntityIds(
|
||
# un_id=3,
|
||
# an_id=7,
|
||
pers_id=5,
|
||
)
|
||
|
||
ids = EntityIds(
|
||
link_id=7,
|
||
)
|
||
self.STATE.ids.update(ids, ignore_none=False)
|
||
|
||
if ids.empty():
|
||
logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!")
|
||
return
|
||
|
||
# TODO add loading logic with backend call
|
||
# !! just a dummy
|
||
initrec = backend.page_initrec_from_db(ids)
|
||
|
||
# TODO re-enable later
|
||
# if res.status != STATUS_HANDLER.SUCCESS:
|
||
# logger_page_consulting.error(
|
||
# (
|
||
# "[Consulting Page] There were errors during the database "
|
||
# "loading procedure."
|
||
# ),
|
||
# stack_info=True,
|
||
# )
|
||
|
||
# exc_formatted = (
|
||
# f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}"
|
||
# )
|
||
# msg_box = get_message_box(
|
||
# QMessageBox.Icon.Warning,
|
||
# "Laden fehlgeschlagen",
|
||
# (
|
||
# "Beim Laden der Daten ist ein Fehler aufgetreten. Details sind "
|
||
# "unten angefügt."
|
||
# ),
|
||
# detailed_text=exc_formatted,
|
||
# )
|
||
# msg_box.exec()
|
||
# return
|
||
|
||
self._load_from_pydantic(initrec, sync_to_GUI=False)
|
||
|
||
def _load_from_pydantic(
|
||
self,
|
||
data: InitRec,
|
||
update_only: bool = False,
|
||
sync_to_GUI: bool = True,
|
||
) -> None:
|
||
logger_initrecform.info("[Page -- InitRec] Load from Pydantic model...")
|
||
self.STATE.ids.update(data.ids)
|
||
self.STATE.recording_type = data.recording_type
|
||
self.STATE.form_data = data.form_data
|
||
self.STATE.geloescht = True if data.geloescht else False
|
||
|
||
# set form data to correct initrec form states
|
||
for ent_type, state in self.STATE.initrec_form_states.items():
|
||
state.ids.update(self.STATE.ids)
|
||
state.geloescht = self.STATE.geloescht
|
||
|
||
match ent_type:
|
||
case EntityType.COMPANY:
|
||
state.form_data = self.STATE.form_data.company
|
||
if (
|
||
not update_only
|
||
and self.STATE.recording_type is RecordingType.WITH_COMPANY
|
||
):
|
||
self.initrec_form_company.load_state(state)
|
||
case EntityType.PERSON:
|
||
state.form_data = self.STATE.form_data.person
|
||
if not update_only:
|
||
self.initrec_form_person.load_state(state)
|
||
case _:
|
||
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
||
|
||
if sync_to_GUI:
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
section: Literal["content", "banners", "locking"] | None,
|
||
) -> None:
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing state to GUI: State IDs: %s", self.STATE.ids
|
||
)
|
||
|
||
if section is None or section == "content":
|
||
if not self.STATE.geloescht:
|
||
self._load_database(self.STATE.ids)
|
||
else:
|
||
self._reset()
|
||
self._update_layout()
|
||
|
||
if section is None or section == "banners":
|
||
self._clear_info_banners()
|
||
if self.STATE.ids.pers_id is None:
|
||
self._add_info_banner(
|
||
text="⚠️ Dieser Eintrag ist noch nicht gespeichert.",
|
||
button_text="",
|
||
)
|
||
|
||
if section is None or section == "locking":
|
||
if self.STATE.locked:
|
||
for form in self.STATE.child_modules:
|
||
form.lock()
|
||
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
|
||
else:
|
||
for form in self.STATE.child_modules:
|
||
form.unlock()
|
||
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
|
||
self._activate_delete()
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
logger_page_initrec.debug("[Page -- InitRec] Syncing GUI to state...")
|
||
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: Child modules...\n%s",
|
||
pformat(self.STATE.child_modules),
|
||
)
|
||
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: Existing form states keys...\n%s",
|
||
pformat(self.STATE.initrec_form_states.keys()),
|
||
)
|
||
|
||
for form in self.STATE.child_modules:
|
||
# ?? is this the same reference to use a similar pattern as
|
||
# ?? the consulting page (initialise once with the same state
|
||
# ?? as reference which are then automatically updated)
|
||
# !! check if this is working as expected
|
||
# new entries' states are linked to this instance by the page state
|
||
form_state = form.get_state()
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: Form state ent_type %s",
|
||
form_state.ent_type,
|
||
)
|
||
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: identity of form states: %s",
|
||
(self.STATE.initrec_form_states[form_state.ent_type] is form_state),
|
||
)
|
||
assert self.STATE.initrec_form_states[form_state.ent_type] is form_state, (
|
||
"passing by reference not working"
|
||
)
|
||
self.STATE.ids.update(form_state.ids, ignore_none=True)
|
||
# self.STATE.form_data[form_state.ent_type] = form_state.form_data
|
||
match form_state.ent_type:
|
||
case EntityType.COMPANY:
|
||
self.STATE.form_data.company = form_state.form_data
|
||
case EntityType.PERSON:
|
||
self.STATE.form_data.person = form_state.form_data
|
||
case _:
|
||
raise RuntimeError(f"Unspecified entity type {form_state.ent_type}")
|
||
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: IDs after...\n%s",
|
||
pformat(self.STATE.ids),
|
||
)
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Syncing GUI to state: Form data after...\n%s",
|
||
pformat(self.STATE.form_data),
|
||
)
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_InitRec_State,
|
||
) -> None:
|
||
set_page_state(
|
||
self.STATE,
|
||
new_state,
|
||
ignore_attr={"form_data", "initrec_form_states"},
|
||
)
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def get_state(self) -> Page_InitRec_State:
|
||
self._sync_GUI_to_state()
|
||
|
||
return self.STATE
|
||
|
||
def validate(self) -> list[str]:
|
||
errors: list[str] = []
|
||
for m in self.STATE.child_modules:
|
||
errors.extend(m.validate())
|
||
|
||
return errors
|
||
|
||
def lock(self) -> None:
|
||
logger_page_initrec.debug("[Page -- InitRec] Locking...")
|
||
self.STATE.locked = True
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def unlock(self) -> None:
|
||
logger_page_initrec.debug("[Page -- InitRec] Unlocking...")
|
||
self.STATE.locked = False
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def save_data(self) -> None:
|
||
logger_page_initrec.debug("[Page -- InitRec] Save button clicked...")
|
||
|
||
logger_page_initrec.info("[Page -- InitRec] Syncing current state...")
|
||
# state = self.get_state()
|
||
self._sync_GUI_to_state()
|
||
form_data = self.STATE.form_data
|
||
assert form_data, "form data not set"
|
||
logger_page_initrec.debug("[Page -- InitRec] Get form data\n%s", pformat(form_data))
|
||
db_data = Page_InitRec_Form_Data(metadata={}, person={}, company={})
|
||
|
||
if self.STATE.ids.empty() and self.STATE.geloescht:
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] This was never saved in the database "
|
||
"and is marked for deletion - ignore."
|
||
)
|
||
return
|
||
elif not self.STATE.ids.empty() and self.STATE.geloescht:
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] This was saved in the database "
|
||
"and is marked for deletion - fast path."
|
||
)
|
||
deleted_datetime = datetime.datetime.now(datetime.UTC)
|
||
assert self.STATE.form_data, "cannot delete entry with uninitialised form data"
|
||
data_to_db = InitRec(
|
||
ids=self.STATE.ids,
|
||
recording_type=self.STATE.recording_type,
|
||
geloescht=deleted_datetime,
|
||
form_data=db_data,
|
||
)
|
||
returned = backend.page_initrec_to_db(data_to_db)
|
||
self._load_from_pydantic(returned)
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Information,
|
||
"Löschen erfolgreich",
|
||
"Der Eintrag wurde erfolgreich gelöscht",
|
||
)
|
||
msg_box.exec()
|
||
|
||
return
|
||
|
||
self._disable_save()
|
||
errors = self.validate()
|
||
|
||
if errors:
|
||
logger_page_initrec.error(
|
||
"[Page -- InitRec] Error during GUI validation phase:\n%s", pformat(errors)
|
||
)
|
||
GUI_validation_error_handling(errors)
|
||
self._enable_save()
|
||
return
|
||
|
||
logger_page_initrec.info("[Page -- InitRec] Success! All data is valid")
|
||
|
||
try:
|
||
logger_page_initrec.debug("[Page -- InitRec] Call Pydantic...")
|
||
# validated_data = self.cfg.pydantic_model(**form_data)
|
||
|
||
for ent_type, state in self.STATE.initrec_form_states.items():
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Ent type: %s, Current state CFG model: %s",
|
||
ent_type,
|
||
state.cfg.pydantic_model,
|
||
)
|
||
match ent_type:
|
||
case EntityType.COMPANY:
|
||
val_data = state.cfg.pydantic_model(**self.STATE.form_data.company)
|
||
db_data.company = val_data.to_db(exclude=state.cfg.ignored_keys)
|
||
case EntityType.PERSON:
|
||
val_data = state.cfg.pydantic_model(**self.STATE.form_data.person)
|
||
db_data.person = val_data.to_db(exclude=state.cfg.ignored_keys)
|
||
case _:
|
||
raise RuntimeError(f"Unspecified entity type {ent_type}")
|
||
|
||
except ValidationError as e:
|
||
logger_page_initrec.error(
|
||
"[Page -- InitRec] Error during Pydantic validation phase:\n%s",
|
||
pformat(e.errors()),
|
||
)
|
||
pydantic_validation_error_handling(e)
|
||
|
||
else:
|
||
if DEBUG_NO_DATABASE:
|
||
return
|
||
# !! this code is only called if the 'try' block was successful
|
||
# save data to database
|
||
# db_data = validated_data.to_db(exclude=self.cfg.ignored_keys)
|
||
logger_page_initrec.debug(
|
||
"[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s",
|
||
pformat(db_data),
|
||
)
|
||
deleted_datetime: datetime.datetime | None = None
|
||
if self.STATE.geloescht:
|
||
deleted_datetime = datetime.datetime.now(datetime.UTC)
|
||
try:
|
||
# TODO change to result pattern
|
||
data_to_db = InitRec(
|
||
ids=self.STATE.ids,
|
||
recording_type=self.STATE.recording_type,
|
||
geloescht=deleted_datetime,
|
||
form_data=db_data,
|
||
)
|
||
returned = backend.page_initrec_to_db(data_to_db)
|
||
|
||
self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False)
|
||
self.update_triggered.emit()
|
||
except Exception as err:
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Speichern fehlgeschlagen",
|
||
(
|
||
"Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind "
|
||
"unten angefügt."
|
||
),
|
||
detailed_text=str(err),
|
||
)
|
||
msg_box.exec()
|
||
else:
|
||
logger_page_initrec.info("[Page -- InitRec] Data saved successfully")
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Information,
|
||
"Speichern erfolgreich",
|
||
"Die Daten wurden erfolgreich gespeichert",
|
||
)
|
||
msg_box.exec()
|
||
|
||
finally:
|
||
# always re-enable save, even if error occurred
|
||
self._enable_save()
|
||
|
||
|
||
class Page_CompanyProfile(QWidget):
|
||
back_main_requested = Signal() # back to main page
|
||
back_requested = Signal() # back to init rec (bool: reset yes/no)
|
||
# TODO update should trigger something else
|
||
# more like listening to 'child' ("Beratungen")
|
||
# update_triggered = Signal() # form saved (data changed for front page)
|
||
consulting_requested = Signal(Page_Consulting_State)
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_CompanyProfile_State,
|
||
):
|
||
super().__init__()
|
||
self.STATE = state
|
||
|
||
# state management
|
||
self.COMPANY_FEATURES_MASTER_DATA: dict[str, str] = {
|
||
"ma_unternehmensname": "Name:",
|
||
"ma_branche": "Branche:",
|
||
"ma_strasse": "Straße:",
|
||
"ma_hausnummer": "Hausnummer:",
|
||
"ma_plz": "PLZ:",
|
||
"ma_ort": "Ort:",
|
||
"ma_website": "Webseite:",
|
||
"ma_mail": "E-Mail:",
|
||
"ma_telefonnummer": "Telefon:",
|
||
}
|
||
|
||
# Hauptlayout der Seite
|
||
outer_layout = QHBoxLayout(self)
|
||
vert_layout = QVBoxLayout()
|
||
# main_layout.setContentsMargins(0, 0, 0, 0)
|
||
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)
|
||
|
||
# --- HEADER ---
|
||
header_container = QWidget()
|
||
header_container.setMinimumWidth(700)
|
||
header_container.setMaximumWidth(1200)
|
||
header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
header_layout = QVBoxLayout(header_container)
|
||
header_layout.setContentsMargins(0, 0, 0, 10)
|
||
|
||
back_btn_main = QPushButton("← Zurück zur Übersicht")
|
||
back_btn_main.clicked.connect(lambda: self.back_main_requested.emit())
|
||
back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_main.setMinimumWidth(200)
|
||
back_btn_main.setMaximumWidth(200)
|
||
back_btn_step = QPushButton("← Zurück")
|
||
# back_btn_step.clicked.connect(self._request_initrec_company) # TODO remove
|
||
back_btn_step.clicked.connect(lambda: self.back_requested.emit())
|
||
back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_step.setMinimumWidth(200)
|
||
back_btn_step.setMaximumWidth(200)
|
||
|
||
title = QLabel("Unternehmensprofil")
|
||
title.setStyleSheet("font-size: 20px; font-weight: bold;")
|
||
|
||
header_layout.setSpacing(5)
|
||
|
||
header_layout.addSpacing(15)
|
||
header_layout.addWidget(back_btn_step)
|
||
header_layout.addWidget(back_btn_main)
|
||
header_layout.addWidget(title)
|
||
vert_layout.addWidget(header_container)
|
||
|
||
# --- MAIN CONTENT ---
|
||
container = QWidget()
|
||
# SCROLL AREA
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setMinimumWidth(700)
|
||
scroll_area.setMaximumWidth(1200)
|
||
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
# optional: remove frame to look more modern
|
||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll_area.setWidget(container)
|
||
|
||
vert_layout.addWidget(scroll_area)
|
||
|
||
container_layout = QVBoxLayout(container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# --- MAIN LAYOUT ---
|
||
if DEBUG:
|
||
separator1 = QFrame()
|
||
separator1.setFrameShape(QFrame.Shape.HLine)
|
||
separator1.setFrameShadow(QFrame.Shadow.Sunken)
|
||
container_layout.addWidget(separator1)
|
||
|
||
layout_change_title = QHBoxLayout()
|
||
self.change_title_text = QLineEdit(placeholderText="Unternehmensname eingeben...")
|
||
layout_change_title.addWidget(self.change_title_text)
|
||
change_title_btn = QPushButton("ändern")
|
||
layout_change_title.addWidget(change_title_btn)
|
||
change_title_btn.clicked.connect(self.set_company_name)
|
||
container_layout.addLayout(layout_change_title)
|
||
|
||
btn_dummy_pauschal = QPushButton("Dummy Pauschalberatung hinzufügen")
|
||
btn_dummy_pauschal.clicked.connect(
|
||
lambda: self._debug_consulting_dummy(ConsultingType.PAUSCHAL)
|
||
)
|
||
container_layout.addWidget(btn_dummy_pauschal)
|
||
btn_dummy_inidividual = QPushButton("Dummy Individualberatung hinzufügen")
|
||
btn_dummy_inidividual.clicked.connect(
|
||
lambda: self._debug_consulting_dummy(ConsultingType.INDIVIDUAL)
|
||
)
|
||
container_layout.addWidget(btn_dummy_inidividual)
|
||
|
||
btn_clear_cons = QPushButton("Beratungen leeren")
|
||
btn_clear_cons.clicked.connect(self._clear_consultations)
|
||
container_layout.addWidget(btn_clear_cons)
|
||
|
||
btn_update_cons = QPushButton("Beratungen aktualisieren")
|
||
btn_update_cons.clicked.connect(self._update_consultations)
|
||
container_layout.addWidget(btn_update_cons)
|
||
|
||
separator2 = QFrame()
|
||
separator2.setFrameShape(QFrame.Shape.HLine)
|
||
separator2.setFrameShadow(QFrame.Shadow.Sunken)
|
||
container_layout.addWidget(separator2)
|
||
|
||
container_layout.addSpacing(30)
|
||
|
||
self.title_company_name = HeaderCell("Unternehmen: n.v.", font_size=16)
|
||
container_layout.addWidget(self.title_company_name)
|
||
|
||
container_layout.addSpacing(20)
|
||
|
||
# separation in left and right
|
||
main_hor_layout = QHBoxLayout()
|
||
main_hor_layout.setSpacing(20)
|
||
# main_hor_layout.setContentsMargins(20, 20, 20, 20)
|
||
|
||
# left column
|
||
left_frame = QFrame()
|
||
left_frame.setObjectName("Segment")
|
||
left_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
|
||
left_layout = QVBoxLayout(left_frame)
|
||
main_hor_layout.addWidget(left_frame, stretch=1, alignment=Qt.AlignmentFlag.AlignTop)
|
||
master_data_label = HeaderCell("Stammdaten & Vermerke")
|
||
master_data_label.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
left_layout.addWidget(master_data_label)
|
||
|
||
default_labels = tuple(self.COMPANY_FEATURES_MASTER_DATA.values())
|
||
self.table_master_data = MasterDataFormWidget(default_labels)
|
||
left_layout.addWidget(self.table_master_data)
|
||
left_layout.addWidget(QLabel("PLATZHALTER Vermerke: Welche Notizen?"))
|
||
|
||
# right column
|
||
right_column_layout = QVBoxLayout()
|
||
right_column_layout.setSpacing(20)
|
||
main_hor_layout.addLayout(right_column_layout, stretch=1)
|
||
|
||
# top right area
|
||
top_right_frame = QFrame()
|
||
top_right_frame.setObjectName("Segment")
|
||
top_right_frame.setSizePolicy(
|
||
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
|
||
)
|
||
self.layout_cons_pauschal = QVBoxLayout(top_right_frame)
|
||
self.cons_pauschal: list[CompanyProfile_Consultation_ClickableCell] = []
|
||
common_consultancy_label = HeaderCell("Pauschalberatung")
|
||
common_consultancy_label.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
self.layout_cons_pauschal.addWidget(common_consultancy_label)
|
||
self.add_btn_pauschal = QPushButton("+ Hinzufügen")
|
||
self.add_btn_pauschal.clicked.connect(
|
||
lambda: self._request_new_consulting(ConsultingType.PAUSCHAL)
|
||
)
|
||
self.add_btn_pauschal.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
self.add_btn_pauschal.setMinimumHeight(30)
|
||
self.add_btn_pauschal.setProperty("styleClass", "flatAddButton")
|
||
self.layout_cons_pauschal.addWidget(self.add_btn_pauschal)
|
||
|
||
# bottom right area
|
||
bottom_right_frame = QFrame()
|
||
bottom_right_frame.setObjectName("Segment")
|
||
bottom_right_frame.setSizePolicy(
|
||
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
|
||
)
|
||
self.layout_cons_indiv = QVBoxLayout(bottom_right_frame)
|
||
self.cons_indiv: list[CompanyProfile_Consultation_ClickableCell] = []
|
||
individual_consultancy_label = HeaderCell("Individualberatung")
|
||
individual_consultancy_label.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
self.layout_cons_indiv.addWidget(individual_consultancy_label)
|
||
self.add_btn_individual = QPushButton("+ Hinzufügen")
|
||
self.add_btn_individual.clicked.connect(
|
||
lambda: self._request_new_consulting(ConsultingType.INDIVIDUAL)
|
||
)
|
||
self.add_btn_individual.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
self.add_btn_individual.setMinimumHeight(30)
|
||
self.add_btn_individual.setProperty("styleClass", "flatAddButton")
|
||
self.layout_cons_indiv.addWidget(self.add_btn_individual)
|
||
|
||
right_column_layout.addWidget(top_right_frame)
|
||
right_column_layout.addWidget(bottom_right_frame)
|
||
right_column_layout.addStretch(1)
|
||
|
||
container_layout.addLayout(main_hor_layout)
|
||
container_layout.addStretch()
|
||
|
||
# --- STYLE ---
|
||
self.setStyleSheet("""
|
||
QFrame#Segment {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 3px;
|
||
}
|
||
""")
|
||
|
||
# self.setStyleSheet("""
|
||
# /* Hintergrund des Hauptfensters */
|
||
# QWidget {
|
||
# background-color: #f1f5f9;
|
||
# }
|
||
|
||
# /* Styling für unsere 3 Segmente */
|
||
# QFrame#Segment {
|
||
# background-color: #ffffff;
|
||
# border: 1px solid #e2e8f0;
|
||
# border-radius: 12px;
|
||
# padding: 15px;
|
||
# }
|
||
|
||
# /* Text-Styling */
|
||
# QLabel {
|
||
# color: #334155;
|
||
# font-size: 16px;
|
||
# font-weight: bold;
|
||
# }
|
||
# """)
|
||
|
||
def _debug_consulting_dummy(
|
||
self,
|
||
type: ConsultingType,
|
||
):
|
||
if type is ConsultingType.PAUSCHAL:
|
||
dummy_click_cell = CompanyProfile_Consultation_ClickableCell(
|
||
"Dummy Pauschal",
|
||
None, # type: ignore
|
||
)
|
||
target_layout = self.layout_cons_pauschal
|
||
self.cons_pauschal.append(dummy_click_cell)
|
||
else:
|
||
dummy_click_cell = CompanyProfile_Consultation_ClickableCell(
|
||
"Dummy Individual",
|
||
None, # type: ignore
|
||
)
|
||
target_layout = self.layout_cons_indiv
|
||
self.cons_indiv.append(dummy_click_cell)
|
||
|
||
target_idx = target_layout.count() - 1
|
||
target_layout.insertWidget(target_idx, dummy_click_cell)
|
||
|
||
# def _request_initrec_company(self) -> None:
|
||
# req_state = Page_InitRecCompany_State(
|
||
# session=self.STATE.session,
|
||
# un_id=self.STATE.rec_id,
|
||
# locked=True,
|
||
# )
|
||
# logger_gui.debug("[Page Company Profile] State to call: %s", req_state)
|
||
# self.back_requested.emit(req_state)
|
||
|
||
def _request_new_consulting(
|
||
self,
|
||
cons_type: ConsultingType,
|
||
) -> None:
|
||
assert self.STATE.ids.un_id is not None, (
|
||
"tried to call consulting from comp profile without set ID"
|
||
)
|
||
new_state = Page_Consulting_State(
|
||
session=self.STATE.session,
|
||
vorgang_id=None,
|
||
un_id=self.STATE.ids.un_id,
|
||
beratungs_typ=cons_type,
|
||
locked=False,
|
||
)
|
||
self.consulting_requested.emit(new_state)
|
||
|
||
def _request_existing_consulting(
|
||
self,
|
||
data: CompanyProfileConsultationEntry,
|
||
) -> None:
|
||
assert self.STATE.ids.un_id is not None, (
|
||
"tried to call consulting from comp profile without set ID"
|
||
)
|
||
new_state = Page_Consulting_State(
|
||
session=self.STATE.session,
|
||
vorgang_id=data.cons_id,
|
||
un_id=self.STATE.ids.un_id,
|
||
beratungs_typ=data.cons_type,
|
||
locked=True,
|
||
)
|
||
self.consulting_requested.emit(new_state)
|
||
|
||
def _clear_consultations(self) -> None:
|
||
logger_gui.debug("[Page Company Profile] Clear consultations...")
|
||
|
||
while self.cons_pauschal:
|
||
widget = self.cons_pauschal[0]
|
||
self.layout_cons_pauschal.removeWidget(widget)
|
||
self.cons_pauschal.remove(widget)
|
||
widget.deleteLater()
|
||
|
||
while self.cons_indiv:
|
||
widget = self.cons_indiv[0]
|
||
self.layout_cons_indiv.removeWidget(widget)
|
||
self.cons_indiv.remove(widget)
|
||
widget.deleteLater()
|
||
|
||
def _update_consultations(self) -> None:
|
||
if self.STATE.ids.un_id is None:
|
||
return
|
||
consultations = backend.companyprofile_page_get_consultations(
|
||
un_id=self.STATE.ids.un_id
|
||
)
|
||
|
||
logger_gui.debug(
|
||
"[Page -- Company Profile] Consultations: %s", pformat(consultations)
|
||
)
|
||
|
||
self._clear_consultations()
|
||
|
||
for entry in consultations.pauschal:
|
||
date_formatted = entry.date_updated.strftime("%d.%m.%Y %H:%M")
|
||
display_title = f"Pauschalberatung ({date_formatted}): {entry.title}"
|
||
click_cell = CompanyProfile_Consultation_ClickableCell(display_title, entry)
|
||
click_cell.clicked.connect(self._request_existing_consulting)
|
||
target_idx = self.layout_cons_pauschal.count() - 1
|
||
self.layout_cons_pauschal.insertWidget(target_idx, click_cell)
|
||
self.cons_pauschal.append(click_cell)
|
||
|
||
for entry in consultations.individual:
|
||
date_formatted = entry.date_updated.strftime("%d.%m.%Y %H:%M")
|
||
display_title = f"Pauschalberatung ({date_formatted}): {entry.title}"
|
||
click_cell = CompanyProfile_Consultation_ClickableCell(display_title, entry)
|
||
click_cell.clicked.connect(self._request_existing_consulting)
|
||
target_idx = self.layout_cons_indiv.count() - 1
|
||
self.layout_cons_indiv.insertWidget(target_idx, click_cell)
|
||
self.cons_indiv.append(click_cell)
|
||
|
||
def update_current_page(self) -> None:
|
||
self._update_consultations()
|
||
|
||
def _disable_add_btns(self) -> None:
|
||
self.add_btn_pauschal.setEnabled(False)
|
||
self.add_btn_individual.setEnabled(False)
|
||
|
||
def _enable_add_btns(self) -> None:
|
||
self.add_btn_pauschal.setEnabled(True)
|
||
self.add_btn_individual.setEnabled(True)
|
||
|
||
def _sync_state_to_GUI(self) -> None:
|
||
if self.STATE.ids.un_id is None:
|
||
# just clear and do nothing else
|
||
logger_gui.debug("just reset, ID is None")
|
||
self.table_master_data.reset()
|
||
self.set_company_name("")
|
||
self._disable_add_btns()
|
||
return
|
||
|
||
self._enable_add_btns()
|
||
# TODO change to correct backend function
|
||
raise RuntimeError("TODOs to implement")
|
||
init_rec = backend.initrec_company_get_initial_recording(self.STATE.ids.un_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)
|
||
|
||
data: dict[str, Any] = {}
|
||
|
||
for feature, value in comp_info.items():
|
||
if feature in self.COMPANY_FEATURES_MASTER_DATA:
|
||
data[self.COMPANY_FEATURES_MASTER_DATA[feature]] = value
|
||
|
||
self.table_master_data.load_data(data)
|
||
self.set_company_name(data["Name:"])
|
||
self.update_current_page()
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
pass
|
||
|
||
def validate(self) -> None:
|
||
pass
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_CompanyProfile_State,
|
||
) -> None:
|
||
logger_gui.debug("Called function to load with IDs: %s", pformat(new_state.ids))
|
||
self.STATE.ids = new_state.ids
|
||
|
||
self._sync_state_to_GUI()
|
||
|
||
def get_state(self) -> Any: ...
|
||
|
||
def save_data(self) -> None: ...
|
||
|
||
def set_company_name(self, name: str) -> None:
|
||
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)
|
||
|
||
|
||
class MasterDataTable(QWidget):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("Stammdaten Anzeige")
|
||
self.resize(450, 400)
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(15, 15, 15, 15)
|
||
|
||
# 1. Tabelle erstellen (0 Zeilen initial, 2 Spalten für Merkmal & Wert)
|
||
self.table = QTableWidget(0, 2)
|
||
|
||
# --- DER TRICK FÜR DEN CLEANEN LOOK ---
|
||
self.table.horizontalHeader().hide() # Spalten-Header ausblenden
|
||
self.table.verticalHeader().hide() # Zeilen-Header (Nummerierung) ausblenden
|
||
self.table.setShowGrid(False) # Gitternetzlinien deaktivieren
|
||
|
||
# Verhalten der Tabelle anpassen
|
||
self.table.setSelectionMode(
|
||
QAbstractItemView.SelectionMode.NoSelection
|
||
) # Nicht auswählbar
|
||
self.table.setEditTriggers(
|
||
QAbstractItemView.EditTrigger.NoEditTriggers
|
||
) # Schreibgeschützt
|
||
|
||
# Spaltenbreite steuern:
|
||
# Linke Spalte passt sich dem Text an, rechte Spalte füllt den Rest des Fensters
|
||
self.table.horizontalHeader().setSectionResizeMode(
|
||
0, QHeaderView.ResizeMode.ResizeToContents
|
||
)
|
||
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||
|
||
layout.addWidget(self.table)
|
||
|
||
# Daten laden
|
||
self.load_data()
|
||
self.style_table()
|
||
|
||
def load_data(self):
|
||
|
||
daten = [
|
||
("Anwendungsname", "MyQtApp Pro"),
|
||
("Version", "2.4.1-beta"),
|
||
("Datenbank-Pfad", "/var/lib/sqlite/data.db"),
|
||
("Letzter Login", "2026-06-25 09:30"),
|
||
("Benutzer-ID", "USR-88291"),
|
||
("Verschlüsselung", "AES-256-GCM"),
|
||
]
|
||
|
||
# Tabelle befüllen
|
||
self.table.setRowCount(len(daten))
|
||
for zeile_index, (merkmal, wert) in enumerate(daten):
|
||
# Merkmal-Zelle (Links)
|
||
item_merkmal = QTableWidgetItem(merkmal)
|
||
item_merkmal.setTextAlignment(
|
||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||
)
|
||
self.table.setItem(zeile_index, 0, item_merkmal)
|
||
|
||
# Wert-Zelle (Rechts)
|
||
item_wert = QTableWidgetItem(wert)
|
||
item_wert.setTextAlignment(
|
||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||
)
|
||
self.table.setItem(zeile_index, 1, item_wert)
|
||
|
||
def style_table(self):
|
||
self.setStyleSheet("""
|
||
QWidget {
|
||
background-color: transparent;
|
||
}
|
||
QTableWidget {
|
||
border: none;
|
||
background-color: transparent;
|
||
}
|
||
QTableWidget::item {
|
||
padding: 8px;
|
||
color: #334155;
|
||
font-size: 14px;
|
||
}
|
||
QTableWidget::item:first-column {
|
||
font-weight: bold;
|
||
color: #64748b;
|
||
padding-right: 20px; /* Mehr Abstand zum Wert */
|
||
}
|
||
""")
|
||
|
||
|
||
class MasterDataFormWidget(QWidget):
|
||
def __init__(
|
||
self,
|
||
default_labels: Sequence[str],
|
||
):
|
||
super().__init__()
|
||
self.default_labels = default_labels
|
||
self.default_data = {label: None for label in self.default_labels}
|
||
|
||
# main layout
|
||
main_layout = QVBoxLayout(self)
|
||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# form in frame
|
||
segment_frame = QFrame()
|
||
segment_frame.setObjectName("Segment")
|
||
# form layout
|
||
self.form_layout = QFormLayout(segment_frame)
|
||
self.form_layout.setSpacing(15)
|
||
self.form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||
|
||
main_layout.addWidget(segment_frame)
|
||
self.style_ui()
|
||
self.reset()
|
||
|
||
def load_data(
|
||
self,
|
||
data: dict[str, Any],
|
||
) -> None:
|
||
self.clear_current_layout()
|
||
|
||
for feature, value in data.items():
|
||
label_feature = QLabel(f"{feature}")
|
||
label_feature.setObjectName("Merkmal")
|
||
|
||
if value is None:
|
||
value = ""
|
||
|
||
label_value = QLabel(str(value))
|
||
label_value.setObjectName("Wert")
|
||
label_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||
|
||
self.form_layout.addRow(label_feature, label_value)
|
||
|
||
def clear_current_layout(self):
|
||
while self.form_layout.count() > 0:
|
||
item = self.form_layout.takeAt(0)
|
||
assert item is not None
|
||
widget = item.widget()
|
||
if widget is not None:
|
||
widget.deleteLater()
|
||
|
||
def reset(self) -> None:
|
||
self.load_data(self.default_data)
|
||
|
||
def style_ui(self):
|
||
self.setStyleSheet("""
|
||
/* Hintergrund des Fensters */
|
||
QWidget {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* Unser optisch abgetrenntes Segment */
|
||
QFrame#Segment {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 5px;
|
||
}
|
||
|
||
/* Textgröße für alle Elemente im Formular hochsetzen */
|
||
QLabel {
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Linke Spalte (Merkmale): Grau und Fett */
|
||
QLabel#Merkmal {
|
||
font-weight: bold;
|
||
/* color: #64748b; */
|
||
color: #475569;
|
||
}
|
||
|
||
/* Rechte Spalte (Werte): Dunkel und gut lesbar */
|
||
QLabel#Wert {
|
||
color: #0f172a;
|
||
}
|
||
""")
|
||
|
||
|
||
class Page_Consulting(QWidget):
|
||
back_main_requested = Signal() # back to main page
|
||
back_requested = Signal() # back to init rec (bool: reset yes/no)
|
||
# TODO update should trigger something else
|
||
# more like listening to 'child' ("Beratungen")
|
||
update_triggered = Signal() # form saved (data changed for front page)
|
||
|
||
def __init__(
|
||
self,
|
||
session: Session,
|
||
):
|
||
super().__init__()
|
||
# state management
|
||
self.STATE = Page_Consulting_State(
|
||
session=session,
|
||
vorgang_id=None,
|
||
beratungs_typ=ConsultingType.PAUSCHAL,
|
||
locked=False,
|
||
)
|
||
|
||
# Hauptlayout der Seite
|
||
outer_layout = QHBoxLayout(self)
|
||
vert_layout = QVBoxLayout()
|
||
# main_layout.setContentsMargins(0, 0, 0, 0)
|
||
outer_layout.addStretch(1)
|
||
outer_layout.addLayout(vert_layout, stretch=100)
|
||
outer_layout.addStretch(1)
|
||
# Optional: Damit der Container oben am Rand klebt
|
||
outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||
|
||
# --- HEADER ---
|
||
header_container = QWidget()
|
||
header_container.setMinimumWidth(700)
|
||
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)
|
||
|
||
back_btn_main = QPushButton("← Zurück zur Übersicht")
|
||
back_btn_main.clicked.connect(lambda: self.back_main_requested.emit())
|
||
back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_main.setMinimumWidth(200)
|
||
back_btn_main.setMaximumWidth(200)
|
||
back_btn_step = QPushButton("← Zurück")
|
||
back_btn_step.clicked.connect(lambda: self.back_requested.emit())
|
||
back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||
back_btn_step.setMinimumWidth(200)
|
||
back_btn_step.setMaximumWidth(200)
|
||
|
||
title = QLabel("Beratungsgespräch")
|
||
title.setStyleSheet("font-size: 20px; font-weight: bold;")
|
||
|
||
header_layout.setSpacing(5)
|
||
|
||
header_layout.addSpacing(15)
|
||
header_layout.addWidget(back_btn_step)
|
||
header_layout.addWidget(back_btn_main)
|
||
header_layout.addWidget(title)
|
||
vert_layout.addWidget(header_container)
|
||
|
||
# --- MAIN CONTENT ---
|
||
container = QWidget()
|
||
# SCROLL AREA
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setMinimumWidth(700)
|
||
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)
|
||
scroll_area.setWidget(container)
|
||
|
||
vert_layout.addWidget(scroll_area)
|
||
|
||
container_layout = QVBoxLayout(container)
|
||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# --- MAIN LAYOUT ---
|
||
if DEBUG:
|
||
separator1 = QFrame()
|
||
separator1.setFrameShape(QFrame.Shape.HLine)
|
||
separator1.setFrameShadow(QFrame.Shadow.Sunken)
|
||
container_layout.addWidget(separator1)
|
||
|
||
btn_get_data_table = QPushButton("Debug call 'get_state' from table and page")
|
||
btn_get_data_table.clicked.connect(self._debug_get_state)
|
||
container_layout.addWidget(btn_get_data_table)
|
||
btn_validate_data_table = QPushButton("Debug call 'validate' from table and page")
|
||
btn_validate_data_table.clicked.connect(self._debug_validate)
|
||
container_layout.addWidget(btn_validate_data_table)
|
||
|
||
db_index_layout = QHBoxLayout()
|
||
self.db_index_field = QLineEdit(placeholderText="setze Index für VorgangID")
|
||
self.db_index_field.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||
)
|
||
db_index_set_button = QPushButton("setze und lade Vorgang-ID (Datenbankindex)")
|
||
db_index_set_button.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||
)
|
||
db_index_set_button.clicked.connect(self._debug_load_database)
|
||
|
||
lock_button = QPushButton("Seite sperren")
|
||
lock_button.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||
)
|
||
lock_button.clicked.connect(self._debug_load_database)
|
||
|
||
db_index_layout.addWidget(self.db_index_field)
|
||
db_index_layout.addWidget(db_index_set_button)
|
||
container_layout.addLayout(db_index_layout)
|
||
|
||
linking_button = QPushButton("Verlinkungsdialog öffnen")
|
||
linking_button.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||
)
|
||
linking_button.clicked.connect(self._open_linking_dialogue)
|
||
container_layout.addWidget(linking_button)
|
||
|
||
separator2 = QFrame()
|
||
separator2.setFrameShape(QFrame.Shape.HLine)
|
||
separator2.setFrameShadow(QFrame.Shadow.Sunken)
|
||
container_layout.addWidget(separator2)
|
||
|
||
container_layout.addSpacing(30)
|
||
|
||
# --- BANNER ---
|
||
self.info_banners_container = QWidget()
|
||
self.info_banners: list[InfoBanner] = []
|
||
self.info_banners_layout = QVBoxLayout(self.info_banners_container)
|
||
self.info_banners_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.info_banners_layout.setSpacing(6)
|
||
container_layout.addWidget(self.info_banners_container)
|
||
|
||
# --- BUTTONS ---
|
||
self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=False)
|
||
modify_btn = self.edit_buttons.modify_button
|
||
assert modify_btn
|
||
self.modify_btn = modify_btn
|
||
# reset_btn = self.edit_buttons.reset_btn
|
||
# assert reset_btn
|
||
self.save_btn = self.edit_buttons.save_btn
|
||
# self.reset_btn = reset_btn
|
||
self.delete_btn = self.edit_buttons.delete_btn
|
||
|
||
self.modify_btn.modify_activated.connect(self.unlock)
|
||
self.modify_btn.modify_deactivated.connect(self.lock)
|
||
self.save_btn.clicked.connect(self.save_data)
|
||
# self.reset_btn.clicked.connect(self.reset_form)
|
||
self.delete_btn.clicked.connect(self._delete_data)
|
||
container_layout.addWidget(self.edit_buttons)
|
||
|
||
type_group_box = QGroupBox("Typ")
|
||
type_layout = QVBoxLayout(type_group_box)
|
||
container_layout.addWidget(type_group_box)
|
||
self.type_pauschal_btn = QRadioButton("Pauschalberatung")
|
||
self.type_individual_btn = QRadioButton("Individualberatung")
|
||
|
||
self.type_pauschal_btn.setChecked(True)
|
||
|
||
self.type_group = QButtonGroup()
|
||
self.type_group.addButton(self.type_pauschal_btn, 0)
|
||
self.type_group.addButton(self.type_individual_btn, 1)
|
||
self.type_group.idClicked.connect(self.type_consulting_changed)
|
||
|
||
type_layout.addWidget(self.type_pauschal_btn)
|
||
type_layout.addWidget(self.type_individual_btn)
|
||
|
||
# container_layout.addSpacing(50)
|
||
table_state = Page_Consulting_Table_State(
|
||
session=self.STATE.session,
|
||
locked=self.STATE.locked,
|
||
)
|
||
self.table_sessions = Page_Consulting_Table(state=table_state)
|
||
self.STATE.child_modules.append(self.table_sessions)
|
||
container_layout.addWidget(self.table_sessions)
|
||
|
||
# --- STYLE ---
|
||
self.setStyleSheet("""
|
||
QFrame#Segment {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 3px;
|
||
}
|
||
""")
|
||
|
||
# self.setStyleSheet("""
|
||
# /* Hintergrund des Hauptfensters */
|
||
# QWidget {
|
||
# background-color: #f1f5f9;
|
||
# }
|
||
|
||
# /* Styling für unsere 3 Segmente */
|
||
# QFrame#Segment {
|
||
# background-color: #ffffff;
|
||
# border: 1px solid #e2e8f0;
|
||
# border-radius: 12px;
|
||
# padding: 15px;
|
||
# }
|
||
|
||
# /* Text-Styling */
|
||
# QLabel {
|
||
# color: #334155;
|
||
# font-size: 16px;
|
||
# font-weight: bold;
|
||
# }
|
||
# """)
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def _lock_page(self) -> None:
|
||
pass
|
||
|
||
def _delete_data(self) -> None:
|
||
logger_page_consulting.debug("[Consulting Page] Call data delete...")
|
||
assert self.STATE.vorgang_id is not None, "deletion initialised despite no index set"
|
||
confirm = get_user_confirmation(
|
||
self,
|
||
"Löschen bestätigen",
|
||
"Soll der Eintrag wirklich gelöscht werden?",
|
||
)
|
||
if not confirm:
|
||
return
|
||
|
||
self.STATE.geloescht = True
|
||
self._delete_entry()
|
||
self.update_triggered.emit()
|
||
|
||
def _disable_save(self) -> None:
|
||
self.save_btn.setEnabled(False)
|
||
self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled)
|
||
|
||
def _activate_delete(self) -> None:
|
||
# if self.STATE.un_id is not None or self.STATE.pers_id is not None:
|
||
if self.STATE.vorgang_id is not None:
|
||
self.delete_btn.setEnabled(True)
|
||
else:
|
||
self.delete_btn.setEnabled(False)
|
||
|
||
def _enable_save(
|
||
self,
|
||
timeout: int = 3000,
|
||
) -> None:
|
||
QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True))
|
||
QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S"))
|
||
self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled)
|
||
|
||
def type_consulting_changed(
|
||
self,
|
||
button_id: int,
|
||
) -> None:
|
||
logger_page_consulting.debug("[Consulting Page] Trigger type buttons...")
|
||
|
||
cons_type: ConsultingType
|
||
if button_id == 0:
|
||
cons_type = ConsultingType.PAUSCHAL
|
||
elif button_id == 1:
|
||
cons_type = ConsultingType.INDIVIDUAL
|
||
else:
|
||
raise RuntimeError(f"No consulting type defined for button id: {button_id}")
|
||
|
||
logger_page_consulting.debug("[Consulting Page] Selected cons_type: %s", cons_type)
|
||
self.STATE.beratungs_typ = cons_type
|
||
self._sync_state_to_GUI("cons_type")
|
||
|
||
def _debug_get_state(self) -> None:
|
||
logger_page_consulting.debug("[Consulting Page] Call get state on table...")
|
||
_ = self.table_sessions.get_state()
|
||
logger_page_consulting.debug("[Consulting Page] Call get state on page...")
|
||
state = self.get_state()
|
||
logger_page_consulting.debug("[Consulting Page] Page state:\n%s", state)
|
||
|
||
def _debug_validate(self) -> None:
|
||
logger_page_consulting.debug("[Consulting Page] Call validate data on table...")
|
||
errors = self.table_sessions.validate()
|
||
logger_page_consulting.debug("[Consulting Page] Table errors:\n%s", pformat(errors))
|
||
logger_page_consulting.debug("[Consulting Page] Call get state on page...")
|
||
errors = self.validate()
|
||
logger_page_consulting.debug("[Consulting Page] Page errors:\n%s", pformat(errors))
|
||
|
||
def _debug_load_database(self) -> None:
|
||
value = self.db_index_field.text().strip()
|
||
if not value:
|
||
return
|
||
|
||
try:
|
||
db_index = int(value)
|
||
except ValueError:
|
||
return
|
||
|
||
self._load_database(db_index)
|
||
|
||
def _open_linking_dialogue(self) -> None:
|
||
logger_page_consulting.info("[Consulting Page] Linking dialogue triggered")
|
||
|
||
# Dialog erstellen und anzeigen
|
||
dialog_state = Page_Consulting_Linking_State(
|
||
session=self.STATE.session,
|
||
un_id=self.STATE.un_id,
|
||
pers_id=self.STATE.pers_id,
|
||
beratungs_typ=self.STATE.beratungs_typ,
|
||
)
|
||
dialog = Page_Consulting_LinkingDialog(dialog_state, parent=self)
|
||
|
||
# .exec() blocks main window
|
||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||
# Hier hast du am Ende beide (oder nur eine) Entitäten auswertbar vorliegen:
|
||
dialog_state = dialog.get_state()
|
||
self.STATE.un_id = dialog_state.un_id
|
||
self.STATE.pers_id = dialog_state.pers_id
|
||
|
||
assert self.STATE.vorgang_id is not None, (
|
||
"cannot link consulting process which is not already saved"
|
||
)
|
||
# TODO error handling
|
||
backend.page_consulting_eastablish_link(
|
||
vorgang_id=self.STATE.vorgang_id,
|
||
un_id=self.STATE.un_id,
|
||
pers_id=self.STATE.pers_id,
|
||
)
|
||
logger_page_consulting.info(
|
||
"[Consulting Page] Company selected (ID): %s, Person selected (ID): %s",
|
||
self.STATE.un_id,
|
||
self.STATE.pers_id,
|
||
)
|
||
else:
|
||
logger_page_consulting.info("[Consulting Page] Verknüpfung abgebrochen.")
|
||
|
||
self._sync_state_to_GUI("banners")
|
||
|
||
def _clear_info_banners(self) -> None:
|
||
while self.info_banners:
|
||
banner = self.info_banners[0]
|
||
self.info_banners_layout.removeWidget(banner)
|
||
self.info_banners.remove(banner)
|
||
banner.deleteLater()
|
||
|
||
def _add_info_banner(
|
||
self,
|
||
text: str,
|
||
button_text: str,
|
||
button_func: Callable[[], None] | None = None,
|
||
) -> None:
|
||
if button_text and button_func is None:
|
||
raise ValueError("Can not add button without calling function")
|
||
|
||
banner = InfoBanner(
|
||
text=text,
|
||
button_text=button_text,
|
||
)
|
||
if button_func is not None:
|
||
banner.btn_clicked.connect(button_func)
|
||
self.info_banners.append(banner)
|
||
self.info_banners_layout.addWidget(banner)
|
||
|
||
def _reset(self) -> None:
|
||
self.STATE.vorgang_id = None
|
||
self.STATE.cons_sessions.clear()
|
||
self.STATE.geloescht = False
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
section: Literal["content", "cons_type", "banners", "table", "locking"] | None,
|
||
) -> None:
|
||
if section is None or section == "content":
|
||
if not self.STATE.geloescht:
|
||
self._load_database(self.STATE.vorgang_id)
|
||
else:
|
||
self._reset()
|
||
|
||
if section is None or section == "cons_type" or section == "locking":
|
||
match self.STATE.beratungs_typ:
|
||
case ConsultingType.PAUSCHAL:
|
||
self.type_pauschal_btn.setChecked(True)
|
||
case ConsultingType.INDIVIDUAL:
|
||
self.type_individual_btn.setChecked(True)
|
||
|
||
# deactivate if at least one ID is set (type is pre-defined by how
|
||
# the site is called)
|
||
if self.STATE.un_id or self.STATE.pers_id or self.STATE.locked:
|
||
self.type_pauschal_btn.setEnabled(False)
|
||
self.type_individual_btn.setEnabled(False)
|
||
else:
|
||
self.type_pauschal_btn.setEnabled(True)
|
||
self.type_individual_btn.setEnabled(True)
|
||
|
||
# info banners needed
|
||
if section is None or section == "banners":
|
||
self._clear_info_banners()
|
||
if self.STATE.vorgang_id is None:
|
||
self._add_info_banner(
|
||
text="⚠️ Dieser Eintrag ist noch nicht gespeichert.",
|
||
button_text="",
|
||
)
|
||
else:
|
||
if self.STATE.un_id is None and self.STATE.pers_id is None:
|
||
self._add_info_banner(
|
||
text="⚠️ Dieser Eintrag ist noch nicht mit einer Entität verknüpft.",
|
||
button_text="Jetzt verknüpfen",
|
||
button_func=self._open_linking_dialogue,
|
||
)
|
||
elif (
|
||
self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL
|
||
and self.STATE.pers_id is None
|
||
):
|
||
self._add_info_banner(
|
||
text=(
|
||
"⚠️ Dies ist eine Individualberatung, aber die Verknüpfgung zu "
|
||
"einer Person fehlt."
|
||
),
|
||
button_text="Jetzt verknüpfen",
|
||
button_func=self._open_linking_dialogue,
|
||
)
|
||
|
||
# child modules
|
||
# call to child modules' syncing method not needed because these are initialised by
|
||
# their own "load_state" method which again calls the modules sync method
|
||
if section is None or section == "table":
|
||
table_state = Page_Consulting_Table_State(
|
||
session=self.STATE.session,
|
||
row_states=self.STATE.cons_sessions,
|
||
locked=self.STATE.locked,
|
||
)
|
||
self.table_sessions.load_state(table_state)
|
||
|
||
if section is None or section == "locking":
|
||
if self.STATE.locked:
|
||
self.table_sessions.lock()
|
||
self.edit_buttons.set_modify_state(False, set_delete_btn=True)
|
||
else:
|
||
self.table_sessions.unlock()
|
||
self.edit_buttons.set_modify_state(True, set_delete_btn=False)
|
||
self._activate_delete()
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
# call to child modules' syncing method not needed because their syncing method is
|
||
# called when the "get_state" method is called --> this has to be managed within
|
||
# this method
|
||
table_state = self.table_sessions.get_state()
|
||
self.STATE.cons_sessions = table_state.row_states
|
||
self._activate_delete()
|
||
|
||
def validate(self) -> list[str]:
|
||
errors: list[str] = []
|
||
errors.extend(self.table_sessions.validate())
|
||
|
||
return errors
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_Consulting_State,
|
||
) -> None:
|
||
logger_page_consulting.debug("[Consulting] Loading with request:\n%s", new_state)
|
||
set_page_state(self.STATE, new_state)
|
||
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def _load_database(
|
||
self,
|
||
cons_id: ConsId | None,
|
||
) -> None:
|
||
# load data model from database
|
||
logger_page_consulting.info("[Consulting Page] Load data method...")
|
||
logger_page_consulting.debug("[Consulting Page] Lookup ID: %s", cons_id)
|
||
self._reset()
|
||
|
||
if cons_id is None:
|
||
logger_page_consulting.debug(
|
||
"[Consulting Page] Loading: Lookup ID NULL. Do nothing!"
|
||
)
|
||
return
|
||
|
||
res = backend.page_consulting_from_db(cons_id)
|
||
if res.status != STATUS_HANDLER.SUCCESS:
|
||
logger_page_consulting.error(
|
||
(
|
||
"[Consulting Page] There were errors during the database "
|
||
"loading procedure."
|
||
),
|
||
stack_info=True,
|
||
)
|
||
|
||
exc_formatted = (
|
||
f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}"
|
||
)
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Laden fehlgeschlagen",
|
||
(
|
||
"Beim Laden der Daten ist ein Fehler aufgetreten. Details sind "
|
||
"unten angefügt."
|
||
),
|
||
detailed_text=exc_formatted,
|
||
)
|
||
msg_box.exec()
|
||
return
|
||
|
||
# instantiate state from data model
|
||
consulting_process = res.unwrap()
|
||
self._load_from_pydantic(consulting_process, sync_to_GUI=False)
|
||
|
||
def _load_from_pydantic(
|
||
self,
|
||
data: Beratungsgespraech_Vorgang,
|
||
update_only: bool = False,
|
||
sync_to_GUI: bool = True,
|
||
) -> None:
|
||
self.STATE.vorgang_id = data.vorgang_id
|
||
self.STATE.beratungs_typ = ConsultingType(data.beratungs_typ)
|
||
self.STATE.un_id = data.un_id
|
||
self.STATE.pers_id = data.pers_id
|
||
self.STATE.titel = data.titel
|
||
self.STATE.geloescht = data.geloescht is not None
|
||
self.STATE.locked = True
|
||
|
||
if update_only:
|
||
relevant_gui_states = [s for s in self.STATE.cons_sessions if not s.ist_geloescht]
|
||
rel_cons_session_states = [s for s in data.beratungen if s.geloescht is None]
|
||
assert len(relevant_gui_states) == len(rel_cons_session_states)
|
||
for gui_state, pydantic_state in zip(
|
||
relevant_gui_states, rel_cons_session_states
|
||
):
|
||
gui_state.beratung_id = pydantic_state.beratung_id
|
||
else:
|
||
self.STATE.cons_sessions.clear()
|
||
for pydantic_state in data.beratungen:
|
||
# Deleted sessions are not part of the retrieved data from the backend if this
|
||
# page is initialised from a fresh backend call to get data. (Updates do
|
||
# indeed have data which must be filtered -- see above)
|
||
cons_session_state = Page_Consulting_ConsultingSession_State(
|
||
session=self.STATE.session,
|
||
nutzer_id=pydantic_state.nutzer_id,
|
||
nutzer_name=pydantic_state.nutzer_name,
|
||
beratung_id=pydantic_state.beratung_id,
|
||
zeitstempel=pydantic_state.zeitstempel,
|
||
ansprechpartner=pydantic_state.ansprechpartner,
|
||
kommunikationsweg=pydantic_state.kommunikationsweg,
|
||
thema_crm_matrix=pydantic_state.thema_crm_matrix,
|
||
anmerkungen=pydantic_state.anmerkungen,
|
||
rueckmeldung=pydantic_state.rueckmeldung,
|
||
locked=self.STATE.locked,
|
||
)
|
||
self.STATE.cons_sessions.append(cons_session_state)
|
||
|
||
if sync_to_GUI:
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def get_state(self) -> Page_Consulting_State:
|
||
self._sync_GUI_to_state()
|
||
|
||
return self.STATE
|
||
|
||
def _delete_entry(self) -> None:
|
||
assert self.STATE.vorgang_id is not None and self.STATE.geloescht
|
||
logger_page_consulting.debug(
|
||
"[Consulting Page] This was saved in the database "
|
||
"and is marked for deletion - fast path."
|
||
)
|
||
res = backend.page_consulting_delete(self.STATE.vorgang_id)
|
||
if res.status != STATUS_HANDLER.SUCCESS:
|
||
logger_page_consulting.error(
|
||
(
|
||
"[Consulting Page] There were errors during the database "
|
||
"deletion procedure."
|
||
),
|
||
stack_info=True,
|
||
)
|
||
|
||
exc_formatted = (
|
||
f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}"
|
||
)
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Löschen fehlgeschlagen",
|
||
(
|
||
"Beim Löschen der Daten ist ein Fehler aufgetreten. Details sind "
|
||
"unten angefügt."
|
||
),
|
||
detailed_text=exc_formatted,
|
||
)
|
||
msg_box.exec()
|
||
return
|
||
|
||
logger_page_consulting.info("[Consulting Page] Data deleted successfully")
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Information,
|
||
"Löschen erfolgreich",
|
||
"Die Daten wurden erfolgreich gelöscht",
|
||
)
|
||
msg_box.exec()
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def save_data(self) -> None:
|
||
logger_page_consulting.debug("[Consulting Page] Call saving data...")
|
||
if self.STATE.vorgang_id is None and self.STATE.geloescht:
|
||
logger_page_consulting.debug(
|
||
"[Consulting Page] This was never saved in the database "
|
||
"and is marked for deletion - ignore."
|
||
)
|
||
return
|
||
|
||
self._disable_save()
|
||
errors = self.validate()
|
||
|
||
if errors:
|
||
logger_page_consulting.error(
|
||
"[Consulting Page] Error during GUI validation phase:\n%s", pformat(errors)
|
||
)
|
||
GUI_validation_error_handling(errors)
|
||
self._enable_save()
|
||
return
|
||
|
||
logger_page_consulting.info("[Consulting Page] Success! All data is valid")
|
||
logger_page_consulting.info("[Consulting Page] Get form data call...")
|
||
self._sync_GUI_to_state()
|
||
|
||
logger_page_consulting.debug("[Consulting Page] Current state:\n%s", self.STATE)
|
||
|
||
try:
|
||
logger_page_consulting.debug("[Consulting Page] Call Pydantic...")
|
||
|
||
pydantic_sessions: list[Beratungsgespraech_Einzelgespraech] = []
|
||
|
||
timestamp_deleted: datetime.datetime | None = None
|
||
if self.STATE.geloescht:
|
||
timestamp_deleted = datetime.datetime.now(datetime.UTC)
|
||
|
||
for row in self.STATE.cons_sessions:
|
||
logger_page_consulting.debug("[Consulting Page] Row state:\n%s", row)
|
||
if row.beratung_id is None and row.ist_geloescht:
|
||
logger_page_consulting.debug(
|
||
(
|
||
"[Consulting Page] Session which was never saved in the database "
|
||
"and is marked for deletion - ignore."
|
||
)
|
||
)
|
||
continue
|
||
|
||
timestamp_deleted_session: datetime.datetime | None = None
|
||
if timestamp_deleted is not None:
|
||
# if process is deleted, all sessions must be marked as deleted
|
||
timestamp_deleted_session = timestamp_deleted
|
||
elif row.ist_geloescht:
|
||
timestamp_deleted_session = datetime.datetime.now(datetime.UTC)
|
||
|
||
cons_session_dict = dc.asdict(row)
|
||
cons_session = Beratungsgespraech_Einzelgespraech(
|
||
vorgang_id=self.STATE.vorgang_id,
|
||
geloescht=timestamp_deleted_session,
|
||
**cons_session_dict,
|
||
)
|
||
pydantic_sessions.append(cons_session)
|
||
|
||
consulting_process = Beratungsgespraech_Vorgang(
|
||
vorgang_id=self.STATE.vorgang_id,
|
||
un_id=self.STATE.un_id,
|
||
pers_id=self.STATE.pers_id,
|
||
titel=self.STATE.titel,
|
||
beratungs_typ=self.STATE.beratungs_typ, # type: ignore
|
||
geloescht=timestamp_deleted,
|
||
beratungen=pydantic_sessions,
|
||
)
|
||
|
||
except ValidationError as e:
|
||
logger_page_consulting.error(
|
||
"[Consulting Page] Error during Pydantic validation phase:\n%s",
|
||
pformat(e.errors()),
|
||
)
|
||
pydantic_validation_error_handling(e)
|
||
else:
|
||
if DEBUG_NO_DATABASE:
|
||
return
|
||
logger_page_consulting.debug("[Consulting Page] Validated Pydantic successfully.")
|
||
logger_page_consulting.debug(
|
||
"[Consulting Page] Pydantic model:\n%s", consulting_process
|
||
)
|
||
|
||
res = backend.page_consulting_to_db(consulting_process)
|
||
if res.status != STATUS_HANDLER.SUCCESS:
|
||
logger_page_consulting.error(
|
||
(
|
||
"[Consulting Page] There were errors during the database "
|
||
"saving/updating procedure."
|
||
),
|
||
stack_info=True,
|
||
)
|
||
|
||
exc_formatted = (
|
||
f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}"
|
||
)
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Speichern fehlgeschlagen",
|
||
(
|
||
"Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind "
|
||
"unten angefügt."
|
||
),
|
||
detailed_text=exc_formatted,
|
||
)
|
||
msg_box.exec()
|
||
return
|
||
|
||
updated_consulting_process = res.unwrap()
|
||
logger_page_consulting.info(
|
||
"[Consulting Page] Load state from modified Pydantic model"
|
||
)
|
||
self._load_from_pydantic(updated_consulting_process, update_only=True)
|
||
self.update_triggered.emit()
|
||
|
||
logger_page_consulting.info("[Consulting Page] Data saved successfully")
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Information,
|
||
"Speichern erfolgreich",
|
||
"Die Daten wurden erfolgreich gespeichert",
|
||
)
|
||
msg_box.exec()
|
||
|
||
# self._activate_delete()
|
||
finally:
|
||
# always re-enable save, even if error occurred
|
||
self._enable_save()
|
||
|
||
def lock(self) -> None:
|
||
self.STATE.locked = True
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def unlock(self) -> None:
|
||
self.STATE.locked = False
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
|
||
class Page_Consulting_LinkingDialog(QDialog):
|
||
def __init__(
|
||
self,
|
||
state: Page_Consulting_Linking_State,
|
||
parent: QWidget | None = None,
|
||
):
|
||
super().__init__(parent)
|
||
self.STATE = state
|
||
|
||
self.setWindowTitle("Beziehung verknüpfen")
|
||
self.resize(700, 450)
|
||
|
||
# firmen_aus_db = [{"id": 1, "name": "Siemens AG"}, {"id": 2, "name": "Bosch GmbH"}]
|
||
# personen_aus_db = [
|
||
# {
|
||
# "id": 50,
|
||
# "name": "Max Mustermann",
|
||
# "unternehmen_id": 1,
|
||
# "firma_name": "Siemens AG",
|
||
# },
|
||
# {
|
||
# "id": 51,
|
||
# "name": "Erika Muster",
|
||
# "unternehmen_id": 1,
|
||
# "firma_name": "Siemens AG",
|
||
# },
|
||
# {
|
||
# "id": 52,
|
||
# "name": "Peter Müller",
|
||
# "unternehmen_id": 2,
|
||
# "firma_name": "Bosch GmbH",
|
||
# },
|
||
# {
|
||
# "id": 53,
|
||
# "name": "Freiberufler Frank",
|
||
# "unternehmen_id": None,
|
||
# "firma_name": None,
|
||
# },
|
||
# ]
|
||
|
||
# self.all_unternehmen = firmen_aus_db
|
||
# self.all_personen = personen_aus_db
|
||
|
||
# # Rückgabewerte
|
||
# self.gewaehltes_unternehmen = None
|
||
# self.gewaehlte_person = None
|
||
|
||
# --- UI LAYOUT ---
|
||
main_layout = QVBoxLayout(self)
|
||
column_layout = QHBoxLayout()
|
||
|
||
# left column: companies
|
||
self.vbox_companies = QVBoxLayout()
|
||
self.vbox_companies.addWidget(QLabel("<b>Unternehmen wählen (Optional):</b>"))
|
||
self.btn_clear_company = QPushButton("Auswahl aufheben")
|
||
self.list_companies = QListWidget()
|
||
self.vbox_companies.addWidget(self.list_companies)
|
||
self.vbox_companies.addWidget(self.btn_clear_company)
|
||
|
||
# right column: persons
|
||
self.vbox_persons = QVBoxLayout()
|
||
self.vbox_persons.addWidget(QLabel("<b>Person wählen (Optional):</b>"))
|
||
self.btn_clear_person = QPushButton("Auswahl aufheben")
|
||
self.list_persons = QListWidget()
|
||
self.vbox_persons.addWidget(self.list_persons)
|
||
self.vbox_persons.addWidget(self.btn_clear_person)
|
||
|
||
column_layout.addLayout(self.vbox_companies)
|
||
column_layout.addLayout(self.vbox_persons)
|
||
main_layout.addLayout(column_layout)
|
||
|
||
# Unten: OK / Abbrechen Buttons
|
||
self.button_box = QDialogButtonBox(
|
||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||
)
|
||
self.ok_btn = self.button_box.button(QDialogButtonBox.StandardButton.Ok)
|
||
self.ok_btn.setText("Verknüpfung speichern")
|
||
self.ok_btn.setEnabled(False)
|
||
self.button_box.button(QDialogButtonBox.StandardButton.Cancel).setText("Abbrechen")
|
||
main_layout.addWidget(self.button_box)
|
||
|
||
# initial loading
|
||
# self._load_companies()
|
||
# self._load_persons()
|
||
|
||
# list_style = """
|
||
# QListWidget::item:selected {
|
||
# background-color: #0078D4; /* Blau, wenn die Liste aktiv fokussiert ist */
|
||
# color: white;
|
||
# }
|
||
# QListWidget::item:selected:!active {
|
||
# background-color: #CCE4F7; /* Gut sichtbares Hellblau, wenn die Liste den Fokus verliert */
|
||
# color: black;
|
||
# }
|
||
# QListWidget::item:hover {
|
||
# background-color: #F0F0F0; /* Dezentes Grau beim Drüberfahren */
|
||
# }
|
||
# """
|
||
# self.list_companies.setStyleSheet(list_style)
|
||
# self.list_persons.setStyleSheet(list_style)
|
||
|
||
# signals
|
||
self.list_companies.itemSelectionChanged.connect(self._company_selected)
|
||
self.list_persons.itemSelectionChanged.connect(self._person_selected)
|
||
|
||
self.btn_clear_company.clicked.connect(self._clear_company_selection)
|
||
self.btn_clear_person.clicked.connect(self._clear_person_selection)
|
||
|
||
self.button_box.accepted.connect(self.accept)
|
||
self.button_box.rejected.connect(self.reject)
|
||
|
||
self._sync_state_to_GUI(reset=True)
|
||
|
||
def _enable_person_selection(
|
||
self,
|
||
enable: bool,
|
||
) -> None:
|
||
logger_page_consulting.debug(">>>>>>> CALL ENABLE: %s", enable)
|
||
for i in range(self.vbox_persons.count()):
|
||
item = self.vbox_persons.itemAt(i)
|
||
if not item:
|
||
continue
|
||
widget = item.widget()
|
||
logger_page_consulting.debug(">>>>>>> Type widget: %s", type(widget))
|
||
if isinstance(widget, QWidget):
|
||
logger_page_consulting.debug(">>>>>>> Widget (enable=%s): %s", enable, widget)
|
||
widget.setEnabled(enable)
|
||
|
||
def _reset(self) -> None:
|
||
self._clear_company_selection()
|
||
self._clear_person_selection()
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
reset: bool,
|
||
section: Literal["person_selection", "validation"] | None = None,
|
||
) -> None:
|
||
if reset:
|
||
self._reset()
|
||
self._load_companies()
|
||
|
||
if section is None or section == "person_selection":
|
||
if self.STATE.beratungs_typ is ConsultingType.PAUSCHAL:
|
||
self._enable_person_selection(False)
|
||
elif self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL:
|
||
self._enable_person_selection(True)
|
||
self.list_persons.blockSignals(True)
|
||
self._load_persons(self.STATE.un_id)
|
||
self.list_persons.blockSignals(False)
|
||
|
||
if section is None or section == "validation":
|
||
self._validate()
|
||
|
||
def _validate(self) -> None:
|
||
valid: bool = False
|
||
|
||
if self.STATE.beratungs_typ is ConsultingType.PAUSCHAL:
|
||
assert self.STATE.pers_id is None
|
||
if self.STATE.un_id is not None:
|
||
valid = True
|
||
elif self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL:
|
||
if self.STATE.pers_id is not None:
|
||
valid = True
|
||
else:
|
||
RuntimeError("Undefined consulting type")
|
||
|
||
if valid:
|
||
self.ok_btn.setEnabled(True)
|
||
else:
|
||
self.ok_btn.setEnabled(False)
|
||
|
||
def _load_companies(self) -> None:
|
||
self.list_companies.clear()
|
||
companies = backend.page_consulting_linking_companies(un_id=self.STATE.un_id)
|
||
logger_page_consulting.debug(
|
||
"[Page Consulting -- Linking] Retrieved company entries:\n%s", companies
|
||
)
|
||
|
||
for company in companies:
|
||
item = QListWidgetItem(company.display_name)
|
||
item.setData(Qt.ItemDataRole.UserRole, company)
|
||
self.list_companies.addItem(item)
|
||
|
||
def _load_persons(
|
||
self,
|
||
un_id: RecId | None,
|
||
) -> None:
|
||
"""Lädt Personen. Wenn firma_id übergeben wird, wird gefiltert."""
|
||
self.list_persons.clear()
|
||
|
||
persons = backend.page_consulting_linking_persons(un_id)
|
||
logger_page_consulting.debug(
|
||
"[Page Consulting -- Linking] Retrieved person entries:\n%s", persons
|
||
)
|
||
|
||
for person in persons:
|
||
item = QListWidgetItem(person.display_name)
|
||
item.setData(Qt.ItemDataRole.UserRole, person)
|
||
self.list_persons.addItem(item)
|
||
|
||
# --- LOGIK ---
|
||
def _company_selected(self) -> None:
|
||
items = self.list_companies.selectedItems()
|
||
if not items:
|
||
return
|
||
|
||
company = cast(
|
||
"LinkingConsultationsEntryCompany", items[0].data(Qt.ItemDataRole.UserRole)
|
||
)
|
||
self.STATE.un_id = company.un_id
|
||
|
||
logger_page_consulting.debug(
|
||
"[Page Consulting -- Linking] Company selected. State is:\n%s",
|
||
pformat(self.STATE),
|
||
)
|
||
self._sync_state_to_GUI(reset=False)
|
||
# if self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL:
|
||
# self.list_persons.blockSignals(True)
|
||
# self._load_persons(un_id=self.STATE.un_id)
|
||
# self.list_persons.blockSignals(False)
|
||
|
||
def _person_selected(self) -> None:
|
||
items = self.list_persons.selectedItems()
|
||
if not items:
|
||
return
|
||
|
||
person = cast(
|
||
"LinkingConsultationsEntryPerson", items[0].data(Qt.ItemDataRole.UserRole)
|
||
)
|
||
self.STATE.pers_id = person.pers_id
|
||
self._sync_state_to_GUI(reset=False, section="validation")
|
||
|
||
# TODO this backlink is a really good idea and should be implemented
|
||
# if person["unternehmen_id"]:
|
||
# self.list_companies.blockSignals(True)
|
||
#
|
||
# for i in range(self.list_companies.count()):
|
||
# item = self.list_companies.item(i)
|
||
# firma_data = item.data(Qt.ItemDataRole.UserRole)
|
||
# if firma_data["id"] == person["unternehmen_id"]:
|
||
# item.setSelected(True)
|
||
# self.gewaehltes_unternehmen = firma_data
|
||
# break
|
||
# self.list_companies.blockSignals(False)
|
||
|
||
def _clear_company_selection(self) -> None:
|
||
self.list_companies.clearSelection()
|
||
self.STATE.un_id = None
|
||
self._sync_state_to_GUI(reset=False)
|
||
|
||
def _clear_person_selection(self) -> None:
|
||
self.list_persons.clearSelection()
|
||
self.STATE.pers_id = None
|
||
self._sync_state_to_GUI(reset=False)
|
||
|
||
def get_state(self) -> Page_Consulting_Linking_State:
|
||
return self.STATE
|
||
|
||
|
||
class Page_Consulting_ConsultingSession(QWidget):
|
||
"""child of 'Page_Consulting_DynamicTable'"""
|
||
|
||
delete_request = Signal(QWidget)
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_Consulting_ConsultingSession_State,
|
||
) -> None:
|
||
super().__init__()
|
||
assert isinstance(state, Page_Consulting_ConsultingSession_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
|
||
|
||
self.widget_registry: WidgetRegistry = {}
|
||
|
||
user_field_def = FormField[QLineEdit](
|
||
"Nutzername",
|
||
FormFieldType.TEXT,
|
||
readonly=True,
|
||
placeholder="wird über aktiven Nutzer bei Anlage gesetzt",
|
||
enable_uuid_key=True,
|
||
)
|
||
_, self.user_field = build_and_add_widget(user_field_def, layout)
|
||
|
||
# ?? date or datetime?
|
||
timestamp_def = FormField[FlexibleDateTimeInput](
|
||
"Zeitstempel",
|
||
FormFieldType.DATETIME_FLEXIBLE,
|
||
required=True,
|
||
key="zeitstempel",
|
||
)
|
||
_, self.timestamp = build_and_add_widget(
|
||
timestamp_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
self.timestamp.set_pydatetime(datetime.datetime.now(TIMEZONE_CEST))
|
||
|
||
contact_def = FormField[QLineEdit](
|
||
"Kontaktperson",
|
||
FormFieldType.TEXT,
|
||
placeholder="Freitext",
|
||
required=True,
|
||
key="ansprechpartner",
|
||
)
|
||
_, self.contact = build_and_add_widget(
|
||
contact_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
|
||
contact_type_def = FormField[QComboBox](
|
||
"Kontakttyp",
|
||
FormFieldType.DROPDOWN,
|
||
options=[("Persönlich", None), ("Telefon", None), ("Mail", None)],
|
||
required=True,
|
||
key="kommunikationsweg",
|
||
)
|
||
_, self.contact_type = build_and_add_widget(
|
||
contact_type_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
|
||
crm_link_def = FormField[QLineEdit](
|
||
"CRM-Verknüpfung",
|
||
FormFieldType.TEXT,
|
||
placeholder="CRM Links",
|
||
required=True,
|
||
key="thema_crm_matrix",
|
||
)
|
||
_, self.crm_link = build_and_add_widget(
|
||
crm_link_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
|
||
plain_text_def = FormField[QPlainTextEdit](
|
||
"Anmerkungen",
|
||
FormFieldType.LONGTEXT,
|
||
key="anmerkungen",
|
||
)
|
||
_, self.plain_text = build_and_add_widget(
|
||
plain_text_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
|
||
response_def = FormField[QPlainTextEdit](
|
||
"Rückmeldung",
|
||
FormFieldType.LONGTEXT,
|
||
key="rueckmeldung",
|
||
)
|
||
_, self.response = build_and_add_widget(
|
||
response_def,
|
||
layout,
|
||
widget_registry=self.widget_registry,
|
||
)
|
||
|
||
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(self._delete_request)
|
||
self.btn_delete.setFixedWidth(30)
|
||
layout.addWidget(self.btn_delete)
|
||
|
||
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(None)
|
||
|
||
def _delete_request(self) -> None:
|
||
confirm = get_user_confirmation(
|
||
self,
|
||
"Löschen bestätigen",
|
||
"Soll der Eintrag wirklich gelöscht werden?",
|
||
)
|
||
if confirm:
|
||
self.STATE.ist_geloescht = True
|
||
self.delete_request.emit(self)
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
section: Literal["values", "locking"] | None,
|
||
) -> None:
|
||
logger_page_consulting.debug(
|
||
"[Page Consulting -- Consulting Session]: State = %s", self.STATE
|
||
)
|
||
if section is None or section == "values":
|
||
set_widget_value(self.user_field, self.STATE.nutzer_name)
|
||
if self.STATE.zeitstempel is not None:
|
||
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.kommunikationsweg)
|
||
set_widget_value(self.crm_link, self.STATE.thema_crm_matrix)
|
||
set_widget_value(self.plain_text, self.STATE.anmerkungen)
|
||
set_widget_value(self.response, self.STATE.rueckmeldung)
|
||
|
||
if section is None or section == "locking":
|
||
if self.STATE.locked:
|
||
lock_form(self.widget_registry)
|
||
self.btn_delete.setEnabled(False)
|
||
else:
|
||
unlock_form(self.widget_registry)
|
||
self.btn_delete.setEnabled(False)
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
self.STATE.zeitstempel = self.timestamp.get_pydatetime()
|
||
self.STATE.ansprechpartner = get_widget_value(self.contact)
|
||
self.STATE.kommunikationsweg = get_widget_value(self.contact_type)
|
||
self.STATE.thema_crm_matrix = 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_state(self) -> Page_Consulting_ConsultingSession_State:
|
||
self._sync_GUI_to_state()
|
||
|
||
return self.STATE
|
||
|
||
def validate(self) -> list[str]:
|
||
logger_page_consulting.debug(
|
||
">>>>> Call validate on Session. Widget registry:\n%s",
|
||
pformat_registry(self.widget_registry),
|
||
)
|
||
return validate_form_data(self.widget_registry)
|
||
|
||
def save_data(self) -> None:
|
||
pass
|
||
|
||
def load_state(
|
||
self,
|
||
new_state: Page_Consulting_ConsultingSession_State,
|
||
) -> None:
|
||
set_page_state(self.STATE, new_state)
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def lock(self) -> None:
|
||
self.STATE.locked = True
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def unlock(self) -> None:
|
||
self.STATE.locked = False
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
|
||
class Page_Consulting_Table(QWidget):
|
||
"""child of 'Page_Consulting'"""
|
||
|
||
def __init__(
|
||
self,
|
||
state: Page_Consulting_Table_State,
|
||
) -> None:
|
||
super().__init__()
|
||
self.STATE = state
|
||
|
||
self.setWindowTitle("Native Dynamische Liste")
|
||
self.resize(600, 400)
|
||
|
||
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.setProperty("styleClass", "addButton")
|
||
main_layout.addWidget(self.btn_add)
|
||
self.btn_add.clicked.connect(lambda: self.add_row(None, False))
|
||
|
||
# --- 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;
|
||
# }
|
||
# """)
|
||
|
||
def add_row(
|
||
self,
|
||
row_state: Page_Consulting_ConsultingSession_State | None,
|
||
state_exists: bool,
|
||
) -> None:
|
||
if row_state is None:
|
||
row_state = Page_Consulting_ConsultingSession_State(
|
||
session=self.STATE.session,
|
||
beratung_id=None,
|
||
nutzer_id=self.STATE.session.user_id,
|
||
nutzer_name=self.STATE.session.user_name,
|
||
locked=False,
|
||
)
|
||
|
||
row = Page_Consulting_ConsultingSession(state=row_state)
|
||
row.delete_request.connect(self.remove_row)
|
||
|
||
index = self.container_layout.count() - 1
|
||
self.container_layout.insertWidget(index, row)
|
||
self.STATE.child_modules.append(row)
|
||
if not state_exists:
|
||
self.STATE.row_states.append(row.get_state())
|
||
|
||
def remove_row(
|
||
self,
|
||
table_row_widget: Page_Consulting_ConsultingSession,
|
||
) -> None:
|
||
self.container_layout.removeWidget(table_row_widget)
|
||
self.STATE.child_modules.remove(table_row_widget)
|
||
# do not remove the corresponding state since it is tracked
|
||
# to be post-processed when the database saving operation is triggered
|
||
table_row_widget.deleteLater()
|
||
|
||
def remove_all_rows(self) -> None:
|
||
while self.STATE.child_modules:
|
||
self.remove_row(self.STATE.child_modules[0])
|
||
|
||
def _sync_state_to_GUI(
|
||
self,
|
||
section: Literal["rows", "locking"] | None,
|
||
) -> None:
|
||
self.setUpdatesEnabled(False)
|
||
|
||
if section is None or section == "rows":
|
||
self.remove_all_rows()
|
||
# states_to_show = [s for s in self.STATE.row_states if not s.ist_geloescht]
|
||
# for row_state in states_to_show:
|
||
for row_state in self.STATE.row_states:
|
||
if not row_state.ist_geloescht:
|
||
self.add_row(row_state, True)
|
||
|
||
if section is None or section == "locking":
|
||
if self.STATE.locked:
|
||
self.btn_add.setEnabled(False)
|
||
for m in self.STATE.child_modules:
|
||
m.lock()
|
||
else:
|
||
self.btn_add.setEnabled(True)
|
||
for m in self.STATE.child_modules:
|
||
m.unlock()
|
||
|
||
self.setUpdatesEnabled(True)
|
||
|
||
def _sync_GUI_to_state(self) -> None:
|
||
logger_page_consulting.debug("[Page -- Consulting] Table: Call syncing GUI to state")
|
||
logger_page_consulting.debug(
|
||
(
|
||
"[Page -- Consulting] Table: Row data is managed when entries are added. "
|
||
"There is no action needed. All states are obtained by reference and set "
|
||
"by their row components/widgets. Therefore, this method calls the "
|
||
"method `get_state` of all ACTIVE components to update their respective "
|
||
"states with the current user inputs."
|
||
)
|
||
)
|
||
|
||
logger_page_consulting.debug(
|
||
"[Page -- Consulting] Table - Child modules: %s",
|
||
pformat(self.STATE.child_modules),
|
||
)
|
||
# Child modules are ACTIVE elements, viewable and changeable by the user.
|
||
# The states are already part of this widget and linked by reference, but
|
||
# the current user input must be synced with these states. Therefore, the
|
||
# `get_state` method of these children is called, which implicitly updates
|
||
# their state. The returned state reference is not needed.
|
||
for row in self.STATE.child_modules:
|
||
_ = row.get_state()
|
||
|
||
def get_state(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 load_state(
|
||
self,
|
||
new_state: Page_Consulting_Table_State,
|
||
) -> None:
|
||
logger_page_consulting.debug(
|
||
"[Page -- Consulting] Table: Call loading from state, %s", new_state
|
||
)
|
||
set_page_state(self.STATE, new_state)
|
||
self._sync_state_to_GUI(None)
|
||
|
||
def validate(self) -> list[str]:
|
||
errors: list[str] = []
|
||
for m in self.STATE.child_modules:
|
||
errors.extend(m.validate())
|
||
|
||
return errors
|
||
|
||
def save_data(self) -> None:
|
||
pass
|
||
|
||
def lock(self) -> None:
|
||
self.STATE.locked = True
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
def unlock(self) -> None:
|
||
self.STATE.locked = False
|
||
self._sync_state_to_GUI("locking")
|
||
|
||
|
||
class InfoBanner(QFrame):
|
||
btn_clicked = Signal()
|
||
|
||
def __init__(
|
||
self,
|
||
text: str,
|
||
button_text: str,
|
||
parent: QWidget | None = None,
|
||
):
|
||
super().__init__(parent)
|
||
|
||
self.setStyleSheet("""
|
||
InfoBanner {
|
||
background-color: #FFF3CD;
|
||
border: 1px solid #FFEBAA;
|
||
border-radius: 6px;
|
||
}
|
||
QLabel {
|
||
color: #856404;
|
||
font-size: 13px;
|
||
}
|
||
QPushButton {
|
||
background-color: #856404;
|
||
color: white;
|
||
border-radius: 4px;
|
||
padding: 4px 10px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #6C5204;
|
||
}
|
||
""")
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(15, 10, 15, 10)
|
||
|
||
self.label = QLabel(text)
|
||
layout.addWidget(self.label)
|
||
layout.addStretch()
|
||
|
||
if button_text:
|
||
self.button = QPushButton(button_text)
|
||
layout.addWidget(self.button)
|
||
self.button.clicked.connect(self.btn_clicked.emit)
|
||
|
||
|
||
# // test components
|
||
|
||
|
||
####################################################################
|
||
|
||
|
||
def clear_layout(
|
||
layout: QLayout | None,
|
||
) -> None:
|
||
if layout is None:
|
||
return
|
||
|
||
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())
|
||
|
||
|
||
def get_message_box(
|
||
msg_type: QMessageBox.Icon,
|
||
title: str,
|
||
message: str,
|
||
detailed_text: str | None = None,
|
||
) -> QMessageBox:
|
||
msg_box = QMessageBox()
|
||
msg_box.setIcon(msg_type)
|
||
msg_box.setWindowTitle(title)
|
||
msg_box.setText(message)
|
||
if detailed_text is not None:
|
||
msg_box.setDetailedText(detailed_text)
|
||
|
||
return msg_box
|
||
|
||
|
||
def get_user_confirmation(
|
||
parent_widget: QWidget,
|
||
title: str,
|
||
message: str,
|
||
) -> bool:
|
||
response = QMessageBox.question(
|
||
parent_widget,
|
||
title,
|
||
message,
|
||
buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No),
|
||
defaultButton=QMessageBox.StandardButton.No,
|
||
)
|
||
|
||
if response == QMessageBox.StandardButton.Yes:
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
def pydantic_validation_error_handling(
|
||
val_error: ValidationError,
|
||
) -> None:
|
||
error_texts: list[str] = []
|
||
translated_errors = translate_pydantic_errors(val_error.errors())
|
||
|
||
for error in translated_errors:
|
||
error_field = str(error["loc"][0])
|
||
reason = error["msg"]
|
||
path = " → ".join(error["loc"][:-1]) # type: ignore
|
||
|
||
error_texts.append(f"- {error_field}: {reason}, (Pfad: {path})")
|
||
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Fehler bei der Validierung der Eingabedaten",
|
||
(
|
||
"Bei der Validierung der Eingaben ist ein Fehler aufgetreten. Details "
|
||
"sind unten angefügt."
|
||
),
|
||
"\n".join(error_texts),
|
||
)
|
||
msg_box.exec()
|
||
|
||
|
||
def GUI_validation_error_handling(
|
||
errors: list[str],
|
||
) -> None:
|
||
error_text = "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + "\n▸ ".join(
|
||
errors
|
||
)
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Warning,
|
||
"Fehlende oder fehlerhafte Angaben",
|
||
error_text,
|
||
)
|
||
msg_box.exec()
|
||
|
||
|
||
class _ModifyButton(QPushButton):
|
||
modify_activated = Signal()
|
||
modify_deactivated = Signal()
|
||
|
||
def __init__(
|
||
self,
|
||
external_state_management: bool,
|
||
txt_activate: str = "Bearbeitung aktivieren (Strg + B)",
|
||
txt_deactivate: str = "Bearbeitung deaktivieren (Strg + B)",
|
||
shortcut: str = "Ctrl+B",
|
||
parent: QWidget | None = None,
|
||
) -> None:
|
||
super().__init__(txt_activate, parent=parent)
|
||
self.txt_activate = txt_activate
|
||
self.txt_deactivate = txt_deactivate
|
||
self.custom_shortcut: (
|
||
QKeySequence | QKeyCombination | QKeySequence.StandardKey | str | int
|
||
) = shortcut
|
||
self.is_modify_state = False
|
||
self.external_state_management = external_state_management
|
||
|
||
self.setShortcut(self.custom_shortcut)
|
||
|
||
self.clicked.connect(self._clicked)
|
||
|
||
def set_modify_state(
|
||
self,
|
||
enable_modification: bool,
|
||
) -> None:
|
||
if enable_modification:
|
||
self.setText(self.txt_deactivate)
|
||
else:
|
||
self.setText(self.txt_activate)
|
||
|
||
self.setShortcut(self.custom_shortcut)
|
||
self.is_modify_state = enable_modification
|
||
|
||
def _clicked(self) -> None:
|
||
if self.is_modify_state:
|
||
self.setText(self.txt_activate)
|
||
self.modify_deactivated.emit()
|
||
else:
|
||
self.setText(self.txt_deactivate)
|
||
self.modify_activated.emit()
|
||
|
||
self.setShortcut(self.custom_shortcut)
|
||
if not self.external_state_management:
|
||
self.is_modify_state = not self.is_modify_state
|
||
|
||
@override
|
||
def setEnabled(
|
||
self,
|
||
enabled: bool,
|
||
) -> None:
|
||
super().setEnabled(enabled)
|
||
if enabled:
|
||
self.setShortcut(self.custom_shortcut)
|
||
|
||
@override
|
||
def setShortcut(
|
||
self,
|
||
key: QKeySequence | QKeyCombination | QKeySequence.StandardKey | str | int,
|
||
) -> None:
|
||
self.custom_shortcut = key
|
||
super().setShortcut(key)
|
||
|
||
|
||
class EditButtons(QWidget):
|
||
def __init__(
|
||
self,
|
||
add_modify_btn: bool,
|
||
add_reset_btn: bool,
|
||
) -> None:
|
||
super().__init__()
|
||
|
||
layout_btn = QHBoxLayout(self)
|
||
layout_btn.setContentsMargins(0, 0, 0, 0)
|
||
# modify
|
||
self.modify_button: _ModifyButton | None = None
|
||
if add_modify_btn:
|
||
self.modify_button = _ModifyButton(external_state_management=True)
|
||
self.modify_button.setFixedHeight(50)
|
||
self.modify_button.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
layout_btn.addWidget(self.modify_button)
|
||
|
||
# save
|
||
self.save_btn_txt_enabled = "Speichern (Strg + S)"
|
||
self.save_btn_txt_disabled = "Wird gespeichert..."
|
||
self.save_btn = QPushButton(self.save_btn_txt_enabled)
|
||
self.save_btn.setShortcut("Ctrl+S")
|
||
self.save_btn.setFixedHeight(50)
|
||
self.save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
layout_btn.addWidget(self.save_btn)
|
||
# reset
|
||
self.reset_button: QPushButton | None = None
|
||
if add_reset_btn:
|
||
self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)")
|
||
self.reset_btn.setShortcut("Ctrl+Z")
|
||
self.reset_btn.setFixedHeight(50)
|
||
self.reset_btn.setSizePolicy(
|
||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
||
)
|
||
layout_btn.addWidget(self.reset_btn)
|
||
# delete
|
||
self.delete_btn = QPushButton("Eintrag löschen (Strg + L)")
|
||
self.delete_btn.setShortcut("Ctrl+L")
|
||
self.delete_btn.setFixedHeight(50)
|
||
self.delete_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
layout_btn.addWidget(self.delete_btn)
|
||
|
||
def set_modify_state(
|
||
self,
|
||
enable_modification: bool,
|
||
set_delete_btn: bool = True,
|
||
) -> None:
|
||
if self.modify_button is not None:
|
||
self.modify_button.set_modify_state(enable_modification)
|
||
self.save_btn.setEnabled(enable_modification)
|
||
if set_delete_btn:
|
||
self.delete_btn.setEnabled(enable_modification)
|
||
|
||
|
||
class MainWindow(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
# STATE MANAGEMENT
|
||
session = Session(user_id=42, user_name="TEST-USER")
|
||
self.STATE = Page_MainPage_State(session=session)
|
||
self._history_stack: list[int] = []
|
||
|
||
self.setWindowTitle("Master")
|
||
self.resize(1800, 1000)
|
||
|
||
# MENU
|
||
self.create_menu()
|
||
|
||
# STACK: stack to change between 'sites'
|
||
self.stack = QStackedWidget()
|
||
self.setCentralWidget(self.stack)
|
||
|
||
# MAIN PAGE: table view
|
||
self.main_page = self.setup_main_page()
|
||
self.stack.addWidget(self.main_page)
|
||
|
||
# SITE: add new entries for 'Grunderfassung'
|
||
new_initrec_select_state = Page_NewEntry_State(session=self.STATE.session)
|
||
self.new_entry_select = Page_NewEntry(new_initrec_select_state)
|
||
self.new_entry_select.back_requested.connect(self.show_main_page)
|
||
# self.new_initrec_select.company_requested.connect(self.show_page_initrec_company)
|
||
# self.new_initrec_select.person_requested.connect(self.show_page_initrec_person)
|
||
self.new_entry_select.consulting_requested.connect(self.show_page_consulting)
|
||
self.new_entry_select.new_initrec_requested.connect(self.show_page_newinitrec)
|
||
self.stack.addWidget(self.new_entry_select)
|
||
|
||
# SITE: 'Grunderfassung'
|
||
newinitrec_state = Page_InitRec_State(
|
||
session=session,
|
||
ids=EntityIds(),
|
||
recording_type=RecordingType.WITH_COMPANY,
|
||
form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}),
|
||
locked=False,
|
||
)
|
||
self.newinitrec = Page_InitRec(newinitrec_state)
|
||
self.newinitrec.back_main_requested.connect(self.show_main_page)
|
||
self.newinitrec.back_requested.connect(self.simple_go_back)
|
||
# self.newinitrec.update_main_page.connect(self.update_grid)
|
||
self.stack.addWidget(self.newinitrec)
|
||
|
||
# SITE: 'Grunderfassung Unternehmen'
|
||
# ids = EntityIds(type=RecordingType.COMPANY)
|
||
# initrec_company_state = Page_InitRecCompany_State(
|
||
# session=self.STATE.session, ids=ids, locked=False
|
||
# )
|
||
# self.initrec_company = Page_InitRecCompany(initrec_company_state)
|
||
# self.initrec_company.back_main_requested.connect(self.show_main_page)
|
||
# self.initrec_company.back_requested.connect(self.simple_go_back)
|
||
# self.initrec_company.update_main_page.connect(self.update_grid)
|
||
# self.initrec_company.initrec_person_requested.connect(self.show_page_initrec_person)
|
||
# self.initrec_company.company_profile_requested.connect(self.show_page_company_profile)
|
||
# self.stack.addWidget(self.initrec_company)
|
||
# SITE: 'Grunderfassung Person'
|
||
# ids = EntityIds(type=RecordingType.PERSON)
|
||
# initrec_person_state = Page_InitRecPerson_State(
|
||
# session=self.STATE.session, ids=ids, locked=False
|
||
# )
|
||
# self.initrec_person = Page_InitRecPerson(initrec_person_state)
|
||
# self.initrec_person.back_main_requested.connect(self.show_main_page)
|
||
# self.initrec_person.back_requested.connect(self.simple_go_back)
|
||
# self.initrec_person.update_triggered.connect(self.update_grid)
|
||
# self.stack.addWidget(self.initrec_person)
|
||
# SITE: 'Unternehmensprofil'
|
||
comp_profile_state = Page_CompanyProfile_State(
|
||
session=self.STATE.session, ids=EntityIds()
|
||
)
|
||
self.company_profile = Page_CompanyProfile(comp_profile_state)
|
||
self.company_profile.back_main_requested.connect(self.show_main_page)
|
||
# TODO remove
|
||
# self.company_profile.back_requested.connect(self.show_page_initrec_company)
|
||
self.company_profile.back_requested.connect(self.simple_go_back)
|
||
self.company_profile.consulting_requested.connect(self.show_page_consulting)
|
||
self.stack.addWidget(self.company_profile)
|
||
# SITE: 'Beratungsgespräch'
|
||
self.consulting_page = Page_Consulting(self.STATE.session)
|
||
self.consulting_page.back_main_requested.connect(self.show_main_page)
|
||
self.consulting_page.back_requested.connect(self.simple_go_back)
|
||
self.consulting_page.update_triggered.connect(self.update_page_company_profile)
|
||
self.stack.addWidget(self.consulting_page)
|
||
|
||
def setup_main_page(self):
|
||
# QMainWindow defines frame --> container widget needed for the middle
|
||
main_widget = QWidget()
|
||
outer_layout = QHBoxLayout(main_widget)
|
||
vert_layout = QVBoxLayout()
|
||
|
||
# add buttons
|
||
new_btn = QPushButton("Neu →")
|
||
new_btn.setFixedWidth(100)
|
||
new_btn.setFixedHeight(40)
|
||
new_btn.clicked.connect(self.show_new_entry_select)
|
||
vert_layout.addWidget(new_btn)
|
||
|
||
if DEBUG:
|
||
# TODO reactivate
|
||
# 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)
|
||
# vert_layout.addWidget(update_btn)
|
||
# vert_layout.addWidget(clear_btn)
|
||
separator = QFrame()
|
||
separator.setFrameShape(QFrame.Shape.HLine)
|
||
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
||
vert_layout.addWidget(separator)
|
||
|
||
# container for table or grid
|
||
container = QWidget()
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setMinimumWidth(700)
|
||
scroll_area.setMaximumWidth(1500)
|
||
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
# delete frame of ScrollArea to get a flat look
|
||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll_area.setWidget(container)
|
||
|
||
vert_layout.addSpacing(20)
|
||
vert_layout.addWidget(scroll_area)
|
||
# springs left and right of container to center the it
|
||
outer_layout.addStretch(1)
|
||
outer_layout.addLayout(vert_layout, stretch=100)
|
||
outer_layout.addStretch(1)
|
||
outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||
|
||
# Wir geben dem Container ein vertikales Layout
|
||
container_layout = QVBoxLayout(container)
|
||
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)
|
||
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)
|
||
|
||
# add grid to vertical layout
|
||
container_layout.addLayout(self.header_grid)
|
||
container_layout.addLayout(self.grid)
|
||
# spring below the layout to push it to top
|
||
container_layout.addStretch()
|
||
|
||
self.current_row = 0
|
||
headers = [
|
||
"UN/ NWP/ Kontaktperson",
|
||
"Individualberatung",
|
||
"Pauschalberatung",
|
||
"Verlaufsprotokoll",
|
||
"Datum",
|
||
]
|
||
for col_idx, title in enumerate(headers):
|
||
self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx)
|
||
|
||
# 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 = backend.main_page_get_entries()
|
||
|
||
# for entry in data:
|
||
# self.add_row_to_grid(entry)
|
||
|
||
# TODO rework
|
||
# def add_row_to_grid(
|
||
# self,
|
||
# entry: types.MainPageEntry,
|
||
# ):
|
||
# row = self.current_row
|
||
|
||
# # NAME
|
||
# cell_name = MainPage_ClickableCell(entry.display_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 = MainPage_ClickableCell(cell_date_name, entry)
|
||
# cell_date.clicked.connect(self.goto_initial_recording)
|
||
# self.grid.addWidget(cell_date, row, 4)
|
||
|
||
# self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen
|
||
|
||
# TODO rework
|
||
# def goto_initial_recording(
|
||
# self,
|
||
# data: types.MainPageEntry,
|
||
# ):
|
||
# target: QWidget
|
||
# if data.type is types.RecordingType.COMPANY:
|
||
# ids = EntityIds(un_id=data.rec_id, type=RecordingType.COMPANY)
|
||
# new_state = Page_InitRecCompany_State(
|
||
# session=self.STATE.session,
|
||
# ids=ids,
|
||
# # un_id=data.rec_id, # TODO remove
|
||
# locked=True,
|
||
# )
|
||
# self.initrec_company.load_state(new_state)
|
||
# target = self.initrec_company
|
||
# elif data.type is types.RecordingType.PERSON:
|
||
# ids = EntityIds(pers_id=data.rec_id, type=RecordingType.PERSON)
|
||
# new_state = Page_InitRecPerson_State(
|
||
# session=self.STATE.session,
|
||
# ids=ids,
|
||
# # pers_id=data.rec_id, # TODO remove
|
||
# locked=True,
|
||
# )
|
||
# self.initrec_person.load_state(new_state)
|
||
# target = self.initrec_person
|
||
# else:
|
||
# raise TypeError(f"Unknown init rec type: {data.type}")
|
||
|
||
# self._navigate_to_target(target)
|
||
|
||
def _navigate_to_target(
|
||
self,
|
||
widget: QWidget,
|
||
) -> None:
|
||
curr_idx = self.stack.currentIndex()
|
||
|
||
if curr_idx != -1:
|
||
self._history_stack.append(curr_idx)
|
||
|
||
self.stack.setCurrentWidget(widget)
|
||
|
||
def simple_go_back(self) -> None:
|
||
if not self._history_stack:
|
||
logger_gui.info("[main program] Called >back< on empty history")
|
||
|
||
prev_idx = self._history_stack.pop()
|
||
self.stack.setCurrentIndex(prev_idx)
|
||
|
||
def show_main_page(self):
|
||
self._history_stack.clear()
|
||
self._navigate_to_target(self.main_page)
|
||
|
||
def show_new_entry_select(self):
|
||
self._navigate_to_target(self.new_entry_select)
|
||
|
||
def show_page_newinitrec(
|
||
self,
|
||
new_state: Page_InitRec_State,
|
||
):
|
||
self.newinitrec.load_state(new_state)
|
||
self._navigate_to_target(self.newinitrec)
|
||
|
||
# def show_page_initrec_company(
|
||
# self,
|
||
# new_state: Page_InitRecCompany_State,
|
||
# ):
|
||
# self.initrec_company.load_state(new_state)
|
||
# self._navigate_to_target(self.initrec_company)
|
||
|
||
# def show_page_initrec_person(
|
||
# self,
|
||
# new_state: Page_InitRecPerson_State,
|
||
# ):
|
||
# self.initrec_person.load_state(new_state)
|
||
# self._navigate_to_target(self.initrec_person)
|
||
|
||
def show_page_company_profile(
|
||
self,
|
||
new_state: Page_CompanyProfile_State,
|
||
) -> None:
|
||
self.company_profile.load_state(new_state)
|
||
self._navigate_to_target(self.company_profile)
|
||
|
||
def update_page_company_profile(self) -> None:
|
||
self.company_profile.update_current_page()
|
||
|
||
def show_page_consulting(
|
||
self,
|
||
new_state: Page_Consulting_State,
|
||
) -> None:
|
||
self.consulting_page.load_state(new_state)
|
||
self._navigate_to_target(self.consulting_page)
|
||
|
||
# --- MENÜ LOGIK ---
|
||
def create_menu(self):
|
||
menu_bar = self.menuBar()
|
||
# file menu
|
||
file_menu = menu_bar.addMenu("Datei")
|
||
exit_action = QAction("Beenden", self)
|
||
exit_action.setShortcut("Ctrl+Q")
|
||
exit_action.triggered.connect(self.close)
|
||
file_menu.addAction(exit_action)
|
||
# help menu
|
||
help_menu = menu_bar.addMenu("Hilfe")
|
||
about_action = QAction("Über", self)
|
||
help_menu.addAction(about_action)
|
||
|
||
|
||
# ** global exception handling
|
||
def global_exception_handler(exc_type, exc_value, exc_traceback):
|
||
"""catches all unhandled errors"""
|
||
|
||
# format error
|
||
error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
||
logger_gui.critical(f"UNEXPECTED ERROR:\n{error_msg}")
|
||
|
||
# message to user
|
||
# check if QApplication exists otherwise crashing of crash handler possible)
|
||
if QApplication.instance():
|
||
msg_box = get_message_box(
|
||
QMessageBox.Icon.Critical,
|
||
"Kritischer Fehler",
|
||
"Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert.",
|
||
error_msg,
|
||
)
|
||
msg_box.exec()
|
||
|
||
sys.exit(1)
|
||
|
||
|
||
def qt_message_handler(mode, context, message):
|
||
"""forwards internal Qt C++ warnings to Python loggers"""
|
||
if mode == QtMsgType.QtInfoMsg:
|
||
logger_gui.info(message)
|
||
elif mode == QtMsgType.QtWarningMsg:
|
||
logger_gui.warning(message)
|
||
elif mode == QtMsgType.QtCriticalMsg:
|
||
logger_gui.error(message)
|
||
elif mode == QtMsgType.QtFatalMsg:
|
||
logger_gui.critical(message)
|
||
|
||
|
||
if TYPE_CHECKING:
|
||
# TODO add all relevant modules
|
||
# enable static type checking for the protocol
|
||
_m1: type[Module] = Page_Consulting
|
||
_m2: type[Module] = Page_Consulting_Table
|
||
_m3: type[Module] = Page_Consulting_ConsultingSession
|
||
_m4: type[Module] = Page_InitRec
|
||
_m5: type[Module] = InitRecForm
|
||
# _w1: type[WrapperModule] = Page_InitRecCompany
|
||
# _w2: type[WrapperModule] = Page_InitRecPerson
|
||
_f1: type[CustomForm] = DynamicListWidget
|
||
_f2: type[CustomForm] = DynamicDropdownWidgetNumeric
|
||
_f3: type[CustomForm] = DynamicDropdownWidgetOption
|
||
|
||
|
||
def main() -> None:
|
||
sys.excepthook = global_exception_handler
|
||
qInstallMessageHandler(qt_message_handler)
|
||
|
||
try:
|
||
app = QApplication(sys.argv)
|
||
app.setStyleSheet(QSS)
|
||
|
||
translator = QTranslator()
|
||
translation_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||
if translator.load(QLocale.system(), "qtbase", "_", translation_path):
|
||
app.installTranslator(translator)
|
||
|
||
window = MainWindow()
|
||
window.show()
|
||
sys.exit(app.exec())
|
||
except Exception as err:
|
||
logger_gui.critical("Fehler beim Starten der Anwendung:\n%s", str(err), exc_info=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|