diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 05bc1f1..f4e2dce 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -102,13 +102,13 @@ def set_page_state( class WrapperModule(Protocol): - def _sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI(self, *args, **kwargs) -> None: """build states for all child modules and use their `load_state` method to initialise them with the new data """ ... - def _sync_GUI_to_state(self) -> None: + def _sync_GUI_to_state(self, *args, **kwargs) -> None: """call `get_state` method on all child modules and assign respective properties to state of parent module """ @@ -127,6 +127,10 @@ class WrapperModule(Protocol): """ ... + def lock(self) -> None: ... + + def unlock(self) -> None: ... + class Module(WrapperModule, Protocol): def validate(self) -> list[str]: @@ -199,6 +203,7 @@ class Page_NewInitRec_State(PageState[Module]): class Page_InitRecCompany_State(PageState[Module]): session: Session un_id: RecId | None = None + locked: bool @dc.dataclass(slots=True, kw_only=True) @@ -219,12 +224,14 @@ class AutoForm_State(PageState[Module]): cfg: AutoFormConfig rec_id: RecId | None = None form_data: dict[str, Any] | None = None + locked: bool @dc.dataclass(slots=True, kw_only=True) class Page_InitRecPerson_State(PageState[Module]): session: Session pers_id: RecId | None = None + locked: bool @dc.dataclass(slots=True, kw_only=True) @@ -239,6 +246,7 @@ class Page_Consulting_State(PageState[Module]): default_factory=list ) ist_geloescht: bool = False + locked: bool @dc.dataclass(slots=True, kw_only=True) @@ -257,12 +265,14 @@ class Page_Consulting_ConsultingSession_State(PageState[Module]): anmerkungen: str | None = None rueckmeldung: str | None = None ist_geloescht: bool = False + locked: bool @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"]): session: Session row_states: list[Page_Consulting_ConsultingSession_State] = dc.field(default_factory=list) + locked: bool @dc.dataclass(slots=True, kw_only=True) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 1b279d9..026ae14 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -30,6 +30,7 @@ from PySide6.QtCore import ( QDate, QDateTime, QEvent, + QKeyCombination, QLibraryInfo, QLocale, QObject, @@ -40,7 +41,7 @@ from PySide6.QtCore import ( Signal, qInstallMessageHandler, ) -from PySide6.QtGui import QAction, QFont +from PySide6.QtGui import QAction, QFont, QKeySequence from PySide6.QtWidgets import ( QAbstractItemView, QApplication, @@ -145,7 +146,7 @@ QSS = """ } *[styleClass="stempel"]:focus { - border: 1px dashed #cbd5e1; + border: 1px solid #cbd5e1; } QPushButton[styleClass="addButton"] { @@ -161,6 +162,11 @@ QPushButton[styleClass="addButton"]:hover { background-color: #0098ff; } +QPushButton[styleClass="addButton"]:disabled { + background-color: #cbd5e1; + color: #94a3b8; +} + QPushButton[styleClass="outlineAddButton"] { background-color: transparent; color: #007acc; @@ -175,6 +181,13 @@ QPushButton[styleClass="outlineAddButton"]:hover { color: white; } +QPushButton[styleClass="outlineAddButton"]:disabled { + /* background-color: #cbd5e1; */ + background-color: transparent; + color: #94a3b8; + border: none; +} + QPushButton[styleClass="softAddButton"] { background-color: #e0f2fe; color: #0369a1; @@ -206,6 +219,7 @@ 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( @@ -254,16 +268,20 @@ def pformat_registry(widget_registry: WidgetRegistry) -> str: class CustomForm(Protocol): - def get_form_data(self) -> dict[str, Any]: ... + def get_form_data(self) -> dict[str, Any] | list[dict[str, Any]]: ... def set_form_data(self, data: Any) -> None: ... - def reset_form(self) -> None: ... + def reset(self) -> None: ... - def validate_form_data(self) -> list[str]: ... + def validate(self) -> list[str]: ... + + def lock(self) -> None: ... + + def unlock(self) -> None: ... -class CustomWidget(QWidget): +class CustomFormWidget(QWidget): def __init__( self, form_fields: Sequence[FormField], @@ -279,9 +297,13 @@ class CustomWidget(QWidget): def set_form_data(self, data: Any) -> None: ... - def reset_form(self) -> None: ... + def reset(self) -> None: ... - def validate_form_data(self) -> list[str]: ... + def validate(self) -> list[str]: ... + + def lock(self) -> None: ... + + def unlock(self) -> None: ... def _add_widget_to_layout( @@ -636,11 +658,11 @@ def reset_form( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, - CustomWidget, + CustomFormWidget, ), ): # custom widget classes manage their widgets on their own - widget.reset_form() + widget.reset() widget.setStyleSheet("") @@ -781,7 +803,7 @@ def get_widget_value( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, - CustomWidget, + CustomFormWidget, ), ): # this is a special data structure with some assumptions of the widget's internals @@ -936,10 +958,10 @@ def validate_form_data( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, - CustomWidget, + CustomFormWidget, ), ): - errors_widget = widget.validate_form_data() + errors_widget = widget.validate() if not errors_widget: continue errors.extend(errors_widget) @@ -968,7 +990,9 @@ def validate_form_data( return errors -def validate_child_modules(modules: Iterable[Module]) -> list[str]: +def validate_child_modules( + modules: Iterable[Module], +) -> list[str]: errors: list[str] = [] for mod in modules: errors.extend(mod.validate()) @@ -976,7 +1000,87 @@ def validate_child_modules(modules: Iterable[Module]) -> list[str]: return errors -class Grunderfassung_SuchWidget(CustomWidget): +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], @@ -1065,7 +1169,7 @@ class Grunderfassung_SuchWidget(CustomWidget): button.clicked.connect(self.print_registry) main_layout.addWidget(button) button = QPushButton("Reset form") - button.clicked.connect(self.reset_form) + button.clicked.connect(self.reset) main_layout.addWidget(button) button = QPushButton("Get form data") button.clicked.connect(self.get_form_data) @@ -1074,7 +1178,7 @@ class Grunderfassung_SuchWidget(CustomWidget): button.clicked.connect(self.set_form_data) main_layout.addWidget(button) button = QPushButton("Validate") - button.clicked.connect(self.validate_form_data) + button.clicked.connect(self.validate) main_layout.addWidget(button) self.update_company_data() @@ -1170,13 +1274,13 @@ class Grunderfassung_SuchWidget(CustomWidget): self.fill_out_person(data) @override - def reset_form(self) -> None: + def reset(self) -> None: reset_form(self.widget_registry) self._clear_company_fields() self._clear_person_fields() @override - def validate_form_data(self) -> list[str]: + def validate(self) -> list[str]: errors = validate_form_data(self.widget_registry) return errors @@ -1199,6 +1303,18 @@ class Grunderfassung_SuchWidget(CustomWidget): 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, @@ -1338,6 +1454,28 @@ class AutoForm(QWidget): 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_form) + self.modify_btn.modify_deactivated.connect(self.lock_form) + 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) @@ -1351,23 +1489,7 @@ class AutoForm(QWidget): self.widget_registry, ) - # buttons (save and reset) - self.add_buttons = self.cfg.add_buttons - self.debug_form_data: dict[str, Any] = {} - if self.add_buttons: - self.edit_buttons = EditButtons(add_reset_btn=True) - 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.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) logger_auto_form.debug( "[Auto-Form] Initialised Auto Form Widget. Registry:%s", @@ -1386,6 +1508,8 @@ class AutoForm(QWidget): logger_gui.debug("[Auto-Form] 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) @@ -1393,6 +1517,8 @@ class AutoForm(QWidget): 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) @@ -1424,6 +1550,8 @@ class AutoForm(QWidget): if lookup_id is not None: logger_auto_form.debug("Load from DB:") loaded_data = self.cfg.data_get(lookup_id) + # TODO add path to avoid unnecessary loading (where the same ID is loaded + # twice in a row) -- use an internal marker (not state-related) else: logger_auto_form.debug("[Auto-Form] Loading: Lookup ID NULL. Do nothing!") return @@ -1447,6 +1575,14 @@ class AutoForm(QWidget): self.STATE.form_data = None self._activate_delete() + def lock_form(self) -> None: + self.STATE.locked = True + self._sync_state_to_GUI("locking") + + def unlock_form(self) -> None: + self.STATE.locked = False + self._sync_state_to_GUI("locking") + def _get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) logger_auto_form.debug("[Auto-Form] Call get form data:") @@ -1467,8 +1603,22 @@ class AutoForm(QWidget): set_form_data(self.widget_registry, data) - def _sync_state_to_GUI(self) -> None: - self._load_data(self.STATE.rec_id) + def _sync_state_to_GUI( + self, + section: Literal["form", "locking"] | None, + ) -> None: + if section is None or section == "form": + self._load_data(self.STATE.rec_id) + + 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) + lock_form(self.widget_registry) + else: + if self.edit_buttons is not None: + self.edit_buttons.set_modify_state(True) + unlock_form(self.widget_registry) def _sync_GUI_to_state(self) -> None: form_data = self._get_form_data() @@ -1484,7 +1634,7 @@ class AutoForm(QWidget): new_state: AutoForm_State, ) -> None: set_page_state(self.STATE, new_state) - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def get_state(self) -> AutoForm_State: self._sync_GUI_to_state() @@ -1629,6 +1779,7 @@ class FlexibleDateInput(QWidget): def setReadOnly( self, read_only: bool, + style_class_readonly: str = DEFAULT_READONLY_STYLECLASS, ): self._read_only = read_only self.line_edit.setReadOnly(read_only) @@ -1648,10 +1799,15 @@ class FlexibleDateInput(QWidget): ) cleaned_date = current_text if pure else "" self.line_edit.setText(cleaned_date) - self.line_edit.setProperty("styleClass", "stempel") + 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, @@ -1821,6 +1977,7 @@ class FlexibleDateTimeInput(QWidget): def setReadOnly( self, read_only: bool, + style_class_readonly: str = DEFAULT_READONLY_STYLECLASS, ): self._read_only = read_only self.line_edit.setReadOnly(read_only) @@ -1840,10 +1997,15 @@ class FlexibleDateTimeInput(QWidget): ) cleaned_datetime = current_text if pure else "" self.line_edit.setText(cleaned_datetime) - self.line_edit.setProperty("styleClass", "stempel") + 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, @@ -1990,9 +2152,10 @@ class DynamicListWidget(QWidget): self.sub_forms: list[SubForm] = [] # button to add more sub forms self.add_btn = QPushButton("+ Hinzufügen") - self.add_btn.setStyleSheet( - "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" - ) + 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) @@ -2047,14 +2210,14 @@ class DynamicListWidget(QWidget): base_label=self.base_label, ) - def reset_form(self) -> None: + 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_form_data(self) -> list[str]: + 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)) @@ -2086,6 +2249,14 @@ class DynamicListWidget(QWidget): 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): """ @@ -2205,11 +2376,11 @@ class DynamicDropdownWidgetNumeric(QWidget): sub_forms=self.sub_forms, ) - def reset_form(self) -> None: + def reset(self) -> None: # resets dynamic content when dropdown is set back to default value self.dropdown_widget.setCurrentIndex(0) - def validate_form_data(self) -> list[str]: + 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)) @@ -2229,8 +2400,9 @@ class DynamicDropdownWidgetNumeric(QWidget): def set_form_data( self, - sub_form_data: dict[str, Any], + data: dict[str, Any], ) -> None: + # data is data of sub forms # delete all rows while self.sub_forms: self._remove_row() @@ -2240,23 +2412,23 @@ class DynamicDropdownWidgetNumeric(QWidget): num_subforms: int = -1 for key in self.full_keys: widget = self.widget_registry[key]["widget"] - value = sub_form_data[key] + value = data[key] set_widget_value(widget, value) if value is None: num_subforms = 0 else: num_subforms = value - del sub_form_data[key] + del data[key] logger_get_data.debug(">>>>>>>>> Call set_form_data for DynamicDropdown") - logger_get_data.debug("Data before unmerge:%s", pformat(sub_form_data)) + 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: sub_form_data[key] for key in relevant_keys} + 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 @@ -2265,8 +2437,18 @@ class DynamicDropdownWidgetNumeric(QWidget): logger_get_data.debug(">>>> DynamicDropdownWidget. Sub data:\n%s", pformat(sub_data)) assert len(sub_data) == len(self.sub_forms) - for sub_form_data, sub_form in zip(sub_data, self.sub_forms): - set_form_data(sub_form.registry, sub_form_data) + 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): @@ -2397,11 +2579,11 @@ class DynamicDropdownWidgetOption(QWidget): return whole_registry - def reset_form(self) -> None: + def reset(self) -> None: # resets dynamic content when dropdown is set back to default value self.dropdown_widget.setCurrentIndex(0) - def validate_form_data(self) -> list[str]: + 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)) @@ -2456,6 +2638,16 @@ class DynamicDropdownWidgetOption(QWidget): 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""" @@ -2615,6 +2807,7 @@ class Page_NewInitRec(QWidget): req_state = Page_InitRecCompany_State( session=self.STATE.session, un_id=None, + locked=False, ) logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) self.company_requested.emit(req_state) @@ -2623,6 +2816,7 @@ class Page_NewInitRec(QWidget): req_state = Page_InitRecPerson_State( session=self.STATE.session, pers_id=None, + locked=False, ) logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) self.person_requested.emit(req_state) @@ -2632,6 +2826,7 @@ class Page_NewInitRec(QWidget): 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) @@ -2686,7 +2881,7 @@ CONFIG_GRUNDERFASSUNG_PERSONEN: Final[AutoFormConfig] = AutoFormConfig( form_fields=INITREC_PERSON, ) -CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = { +CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = { "grunderfassung_unternehmen_suche": Grunderfassung_SuchWidget, } @@ -2723,12 +2918,6 @@ class Page_InitRecCompany(QWidget): header_layout = QVBoxLayout(header_container) header_layout.setContentsMargins(0, 0, 0, 10) - comp_profile_btn = QPushButton("Unternehmensprofil") - comp_profile_btn.clicked.connect(self._request_company_profile) - comp_profile_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - comp_profile_btn.setMinimumWidth(250) - # comp_profile_btn.setMaximumWidth(200) - 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) @@ -2743,12 +2932,19 @@ class Page_InitRecCompany(QWidget): title = QLabel("Grunderfassung Unternehmen") title.setStyleSheet("font-size: 20px; font-weight: bold;") + self.comp_profile_btn = QPushButton("Unternehmensprofil") + self.comp_profile_btn.clicked.connect(self._request_company_profile) + self.comp_profile_btn.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + self.comp_profile_btn.setFixedHeight(50) + header_layout.setSpacing(5) - header_layout.addWidget(comp_profile_btn) - header_layout.addSpacing(15) header_layout.addWidget(back_btn_step) header_layout.addWidget(back_btn_main) header_layout.addWidget(title) + header_layout.addSpacing(15) + header_layout.addWidget(self.comp_profile_btn) vert_layout.addWidget(header_container) # --- MAIN CONTENT --- @@ -2774,10 +2970,12 @@ class Page_InitRecCompany(QWidget): auto_form_state = AutoForm_State( session=self.STATE.session, cfg=self.AUTO_FORM_CFG, + locked=False, ) self.auto_form = AutoForm(auto_form_state) container_layout.addWidget(self.auto_form) self.auto_form.update_triggered.connect(lambda: self.update_main_page.emit()) + self.auto_form.update_triggered.connect(self._enable_company_profile) container_layout.addSpacing(15) @@ -2790,6 +2988,10 @@ class Page_InitRecCompany(QWidget): QLineEdit, search_widget.company_widgets["ma_unternehmensname"] ) + def _enable_company_profile(self) -> None: + self._sync_GUI_to_state() + self._sync_state_to_GUI("comp_profile") + def _request_company_profile(self) -> None: assert self.STATE.un_id is not None, ( "requested company profile for a company which is not known (no ID)" @@ -2804,14 +3006,25 @@ class Page_InitRecCompany(QWidget): def reset(self) -> None: self.auto_form.reset_form() - def _sync_state_to_GUI(self) -> None: - auto_form_state = AutoForm_State( - session=self.STATE.session, - cfg=self.AUTO_FORM_CFG, - rec_id=self.STATE.un_id, - form_data=None, - ) - self.auto_form.load_state(auto_form_state) + def _sync_state_to_GUI( + self, + section: Literal["auto_form", "comp_profile"] | None, + ) -> None: + if section is None or section == "auto_form": + auto_form_state = AutoForm_State( + session=self.STATE.session, + cfg=self.AUTO_FORM_CFG, + rec_id=self.STATE.un_id, + form_data=None, + locked=self.STATE.locked, + ) + self.auto_form.load_state(auto_form_state) + + if section is None or section == "comp_profile": + if self.STATE.un_id is None: + self.comp_profile_btn.setEnabled(False) + else: + self.comp_profile_btn.setEnabled(True) def _sync_GUI_to_state(self) -> None: auto_form_state = self.auto_form.get_state() @@ -2823,13 +3036,21 @@ class Page_InitRecCompany(QWidget): ) -> None: set_page_state(self.STATE, new_state) - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def get_state(self) -> Page_InitRecCompany_State: self._sync_GUI_to_state() return self.STATE + def lock(self) -> None: + self.auto_form.lock_form() + self.STATE.locked = True + + def unlock(self) -> None: + self.auto_form.unlock_form() + self.STATE.locked = False + class Page_InitRecPerson(QWidget): back_main_requested = Signal() # back to main page @@ -2901,7 +3122,11 @@ class Page_InitRecPerson(QWidget): # --- AUTO FORM LAYOUT --- container_layout.addSpacing(20) self.AUTO_FORM_CFG = CONFIG_GRUNDERFASSUNG_PERSONEN - auto_form_state = AutoForm_State(session=self.STATE.session, cfg=self.AUTO_FORM_CFG) + auto_form_state = AutoForm_State( + session=self.STATE.session, + cfg=self.AUTO_FORM_CFG, + locked=False, + ) self.auto_form = AutoForm(auto_form_state) container_layout.addWidget(self.auto_form) self.auto_form.update_triggered.connect(lambda: self.update_triggered.emit()) @@ -2952,6 +3177,7 @@ class Page_InitRecPerson(QWidget): cfg=self.AUTO_FORM_CFG, rec_id=self.STATE.pers_id, form_data=None, + locked=self.STATE.locked, ) self.auto_form.load_state(auto_form_state) @@ -2971,6 +3197,14 @@ class Page_InitRecPerson(QWidget): return self.STATE + def lock(self) -> None: + self.auto_form.lock_form() + self.STATE.locked = True + + def unlock(self) -> None: + self.auto_form.unlock_form() + self.STATE.locked = False + class Page_CompanyProfile(QWidget): back_main_requested = Signal() # back to main page @@ -3246,6 +3480,7 @@ class Page_CompanyProfile(QWidget): 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) @@ -3262,6 +3497,7 @@ class Page_CompanyProfile(QWidget): vorgang_id=None, un_id=self.STATE.rec_id, beratungs_typ=cons_type, + locked=False, ) self.consulting_requested.emit(new_state) @@ -3277,6 +3513,7 @@ class Page_CompanyProfile(QWidget): vorgang_id=data.cons_id, un_id=self.STATE.rec_id, beratungs_typ=data.cons_type, + locked=True, ) self.consulting_requested.emit(new_state) @@ -3574,6 +3811,7 @@ class Page_Consulting(QWidget): session=session, vorgang_id=None, beratungs_typ=ConsultingType.PAUSCHAL, + locked=False, ) # Hauptlayout der Seite @@ -3658,6 +3896,12 @@ class Page_Consulting(QWidget): ) 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) @@ -3678,13 +3922,18 @@ class Page_Consulting(QWidget): container_layout.addWidget(self.info_banners_container) # --- BUTTONS --- - self.edit_buttons = EditButtons(add_reset_btn=False) + 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) @@ -3707,7 +3956,10 @@ class Page_Consulting(QWidget): type_layout.addWidget(self.type_individual_btn) # container_layout.addSpacing(50) - table_state = Page_Consulting_Table_State(session=self.STATE.session) + 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) @@ -3743,7 +3995,10 @@ class Page_Consulting(QWidget): # font-weight: bold; # } # """) - self._sync_state_to_GUI() + 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...") @@ -3782,7 +4037,7 @@ class Page_Consulting(QWidget): logger_page_consulting.debug("[Consulting Page] Selected cons_type: %s", cons_type) self.STATE.beratungs_typ = cons_type - self._sync_state_to_GUI() + self._sync_state_to_GUI("cons_type") def _debug_get_state(self) -> None: logger_page_consulting.debug("[Consulting Page] Call get state on table...") @@ -3839,57 +4094,72 @@ class Page_Consulting(QWidget): self.info_banners.append(banner) self.info_banners_layout.addWidget(banner) - def _sync_state_to_GUI(self) -> None: - match self.STATE.beratungs_typ: - case ConsultingType.PAUSCHAL: - self.type_pauschal_btn.setChecked(True) - case ConsultingType.INDIVIDUAL: - self.type_individual_btn.setChecked(True) + def _sync_state_to_GUI( + self, + section: Literal["cons_type", "banners", "table", "locking"] | None, + ) -> None: + 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: - 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) + # 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 - self._clear_info_banners() - if self.STATE.vorgang_id is None: - self._add_linking_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: + if section is None or section == "banners": + self._clear_info_banners() + if self.STATE.vorgang_id is None: self._add_linking_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_linking_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, + 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_linking_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_linking_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 - table_state = Page_Consulting_Table_State( - session=self.STATE.session, - row_states=self.STATE.cons_sessions, - ) - self.table_sessions.load_state(table_state) + 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) + else: + self.table_sessions.unlock() + self.edit_buttons.set_modify_state(True) def _sync_GUI_to_state(self) -> None: # call to child modules' syncing method not needed because their syncing method is @@ -3912,7 +4182,7 @@ class Page_Consulting(QWidget): logger_page_consulting.debug("[Consulting] Loading with request:\n%s", new_state) set_page_state(self.STATE, new_state) - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def load_database( self, @@ -3960,6 +4230,7 @@ class Page_Consulting(QWidget): self.STATE.pers_id = data.pers_id self.STATE.titel = data.titel self.STATE.ist_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] @@ -3987,10 +4258,11 @@ class Page_Consulting(QWidget): 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) - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def get_state(self) -> Page_Consulting_State: self._sync_GUI_to_state() @@ -4121,6 +4393,14 @@ class Page_Consulting(QWidget): # 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_ConsultingSession(QWidget): """child of 'Page_Consulting_DynamicTable'""" @@ -4265,7 +4545,7 @@ class Page_Consulting_ConsultingSession(QWidget): layout.setStretch(i, stretch) self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def _delete_request(self) -> None: confirm = get_user_confirmation( @@ -4277,18 +4557,32 @@ class Page_Consulting_ConsultingSession(QWidget): self.STATE.ist_geloescht = True self.delete_request.emit(self) - def _sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI( + self, + section: Literal["values", "locking"] | None, + ) -> None: logger_page_consulting.debug( "[Page Consulting -- Consulting Session]: State = %s", self.STATE ) - 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 == "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() @@ -4320,7 +4614,15 @@ class Page_Consulting_ConsultingSession(QWidget): new_state: Page_Consulting_ConsultingSession_State, ) -> None: set_page_state(self.STATE, new_state) - self._sync_state_to_GUI() + 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): @@ -4481,11 +4783,11 @@ class Page_Consulting_Table(QWidget): 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) - # sys.exit(0) index = self.container_layout.count() - 1 self.container_layout.insertWidget(index, row) @@ -4507,15 +4809,29 @@ class Page_Consulting_Table(QWidget): while self.STATE.child_modules: self.remove_row(self.STATE.child_modules[0]) - def _sync_state_to_GUI(self) -> None: + def _sync_state_to_GUI( + self, + section: Literal["rows", "locking"] | None, + ) -> None: self.setUpdatesEnabled(False) - 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 == "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) @@ -4559,8 +4875,7 @@ class Page_Consulting_Table(QWidget): "[Page -- Consulting] Table: Call loading from state, %s", new_state ) set_page_state(self.STATE, new_state) - - self._sync_state_to_GUI() + self._sync_state_to_GUI(None) def validate(self) -> list[str]: errors: list[str] = [] @@ -4572,6 +4887,14 @@ class Page_Consulting_Table(QWidget): 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() @@ -4672,7 +4995,9 @@ class MainWindow(QMainWindow): self.new_initrec_select.consulting_requested.connect(self.show_page_consulting) self.stack.addWidget(self.new_initrec_select) # SITE: 'Grunderfassung Unternehmen' - initrec_company_state = Page_InitRecCompany_State(session=self.STATE.session) + initrec_company_state = Page_InitRecCompany_State( + session=self.STATE.session, 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.show_new_entry_select) @@ -4680,7 +5005,9 @@ class MainWindow(QMainWindow): self.initrec_company.company_profile_requested.connect(self.show_page_company_profile) self.stack.addWidget(self.initrec_company) # SITE: 'Grunderfassung Person' - initrec_person_state = Page_InitRecPerson_State(session=self.STATE.session) + initrec_person_state = Page_InitRecPerson_State( + session=self.STATE.session, 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.show_new_entry_select) @@ -4826,13 +5153,17 @@ class MainWindow(QMainWindow): target: QWidget if data.type is types.InitRecType.COMPANY: new_state = Page_InitRecCompany_State( - session=self.STATE.session, un_id=data.rec_id + session=self.STATE.session, + un_id=data.rec_id, + locked=True, ) self.initrec_company.load_state(new_state) target = self.initrec_company elif data.type is types.InitRecType.PERSON: new_state = Page_InitRecPerson_State( - session=self.STATE.session, pers_id=data.rec_id + session=self.STATE.session, + pers_id=data.rec_id, + locked=True, ) self.initrec_person.load_state(new_state) target = self.initrec_person @@ -4926,13 +5257,6 @@ def global_exception_handler(exc_type, exc_value, exc_traceback): "Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert.", error_msg, ) - # msg_box.setIcon(QMessageBox.Icon.Critical) - # msg_box.setWindowTitle("Kritischer Fehler") - # msg_box.setText( - # "Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert." - # ) - # # details for user or screenshots - # msg_box.setDetailedText(error_msg) msg_box.exec() sys.exit(1) @@ -5012,15 +5336,93 @@ def GUI_pydantic_validation_error_handling( 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..." @@ -5046,6 +5448,16 @@ class EditButtons(QWidget): self.delete_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) layout_btn.addWidget(self.delete_btn) + def set_modify_state( + self, + enable_modification: bool, + ) -> None: + print(f">>>>>>>>> SET modify GROUP: {enable_modification=}") + if self.modify_button is not None: + self.modify_button.set_modify_state(enable_modification) + self.save_btn.setEnabled(enable_modification) + self.delete_btn.setEnabled(enable_modification) + def qt_message_handler(mode, context, message): """forwards internal Qt C++ warnings to Python loggers""" @@ -5067,6 +5479,9 @@ if TYPE_CHECKING: _m3: type[Module] = Page_Consulting_ConsultingSession _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: