diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 7b10d53..1f2a46d 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -1,15 +1,18 @@ Ext_Kontaktliste.Master ma_id int pk + Ext_Kontaktliste.Ansprechpartner an_id int pk ma_id int -> Ext_Kontaktliste.Master(ma_id) + Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'. un_id int pk -> Ext_Kontaktliste.Master(ma_id) erstellt "timestamp with time zone" geloescht "timestamp with time zone" nullable + Personen_Unternehmen_Zuordnung id int pk {autoIncrement} pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung @@ -20,6 +23,7 @@ Personen_Unternehmen_Zuordnung aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL) + Grunderfassung_Personen pers_id int pk {autoIncrement} erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` @@ -72,16 +76,19 @@ Grunderfassung_Personen WeitereInfos__WI_deutsch_sprache text nullable WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable WeitereInfos__WI_meldung_institution text nullable -# Metadaten_erstellung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable -# Metadaten_aktualisierung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} -# Zuordnung_Personen_Unternehmen | Eine Person kann zu mehreren Unternehmen gehören, ein Unternehmen kann mehrere Personen betreuen. -# id int pk {autoIncrement} -# un_id int -> Grunderfassung_Unternehmen(un_id) -# pers_id int -> Grunderfassung_Personen(pers_id) -# gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` -# gueltig_bis "timestamp with time zone" nullable +Grunderfassung_Dokumente + id int pk {autoIncrement} + pers_id int -> Grunderfassung_Personen(pers_id) + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} + geloescht "timestamp with time zone" nullable + speicher_pfad text + dateiname_original text + dateigroesse int nullable + mime_type text + anmerkung text Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein (pauschal) oder beide (individual) gebunden sein @@ -93,7 +100,7 @@ Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} geloescht "timestamp with time zone" nullable -# temporaer bool | Gibt an, ob der Vorgang losgelöst von der Grunderfassung gestartet wurde (Daten müssen nachträglich zugeordnet werden); theoretisch auch erfüllt, wenn `un_id` und `pers_id` beide NULL sind + Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden beratung_id int pk {autoIncrement} diff --git a/prototypes/initrec_file_selection.py b/prototypes/initrec_file_selection.py new file mode 100644 index 0000000..acbb1b6 --- /dev/null +++ b/prototypes/initrec_file_selection.py @@ -0,0 +1,412 @@ +from __future__ import annotations + +import datetime +import os +import sys +from collections.abc import Iterable +from pathlib import Path +from typing import TYPE_CHECKING + +from PySide6.QtCore import ( + QFileInfo, + QMimeDatabase, + Qt, + QUrl, + Signal, +) +from PySide6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent +from PySide6.QtWidgets import ( + QApplication, + QComboBox, + QFileDialog, + QFileIconProvider, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QMainWindow, + QPushButton, + QVBoxLayout, + QWidget, +) + +from wce_crm.constants import TIMEZONE_CEST + +if TYPE_CHECKING: + from wce_crm.data_models import Module + + +def get_mime_type( + file_path: Path, +) -> str: + """gets the MIME type of a file via PySide6 / QMimeDatabase""" + db = QMimeDatabase() + mime_type = db.mimeTypeForFile(str(file_path)) + + # .name() -> for example "application/pdf" or "image/png" + return mime_type.name() + + +def _format_file_size(size_in_bytes): + """converts bytes in readable string, e.g. KB or MB""" + if size_in_bytes < 1024: + return f"{size_in_bytes} B" + elif size_in_bytes < 1024 * 1024: + return f"{size_in_bytes / 1024:.1f} KB" + else: + return f"{size_in_bytes / (1024 * 1024):.1f} MB" + + +class _DropZoneWidget(QFrame): + files_dropped = Signal(list) # sends list of pathlib.Path objects + + STYLE_NORMAL = """ + _DropZoneWidget { + border: 2px dashed #B0B0B0; + border-radius: 8px; + background-color: #F9F9F9; + } + _DropZoneWidget:hover { + border-color: #0078D4; + background-color: #F0F6FF; + } + """ + + STYLE_DRAG_ACTIVE = """ + _DropZoneWidget { + border: 2px dashed #0078D4; + border-radius: 8px; + background-color: #E1EFFA; + } + """ + + def __init__( + self, + parent: QWidget | None = None, + ): + super().__init__(parent) + self.setAcceptDrops(True) + self.setCursor(Qt.CursorShape.PointingHandCursor) + + self.setStyleSheet(self.STYLE_NORMAL) + self.setFixedHeight(60) + + layout = QVBoxLayout(self) + self.label = QLabel( + "📁 Dateien hierher ziehen oder klicken zum Auswählen" + ) + self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.label) + + def dragEnterEvent( + self, + event: QDragEnterEvent, + ): + if event.mimeData().hasUrls(): + # check if MDB/explorer gives at least one real file (no folder) + has_files = False + for url in event.mimeData().urls(): + pfad = url.toLocalFile() + if pfad and os.path.isfile(pfad): + has_files = True + break + + if has_files: + event.acceptProposedAction() + self.setStyleSheet(self.STYLE_DRAG_ACTIVE) + + def dragLeaveEvent(self, event): + self.setStyleSheet(self.STYLE_NORMAL) + + def dropEvent(self, event: QDropEvent): + self.dragLeaveEvent(None) + + valid_files: list[Path] = [] + for url in event.mimeData().urls(): + if url.isLocalFile(): + path = Path(url.toLocalFile()).resolve() + # only add if it is a file + if path.is_file(): + valid_files.append(path) + + if valid_files: + self.files_dropped.emit(valid_files) + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen") + if files: + files_pathlib = [Path(file).resolve() for file in files] + self.files_dropped.emit(files_pathlib) + + +class _FileItemWidget(QFrame): + request_remove = Signal(QWidget) # signal to parent to remove entry + + def __init__( + self, + file_path: Path, + parent: QWidget | None = None, + ): + super().__init__(parent) + self.file_path = file_path + self._created_at_UTC = datetime.datetime.now(tz=datetime.UTC) + self.created_at_string = self._created_at_UTC.astimezone(TIMEZONE_CEST).strftime( + "%d.%m.%Y %H:%M" + ) + + self.setStyleSheet(""" + _FileItemWidget { + background-color: #FFFFFF; + border: 1px solid #E0E0E0; + border-radius: 6px; + padding: 6px; + } + _FileItemWidget:hover { + border-color: #B0B0B0; + } + """) + + layout = QHBoxLayout(self) + layout.setContentsMargins(8, 6, 8, 6) + + # file icon: use dummy to avoid icons with checkmarks (synchronised folder) + icon_label = QLabel() + provider = QFileIconProvider() + file_icon = provider.icon(QFileInfo(f"dummy{file_path.suffix}")) + icon_label.setPixmap(file_icon.pixmap(28, 28)) + layout.addWidget(icon_label) + + # file info: name, size, date, mime type + info_vbox = QVBoxLayout() + info_vbox.setSpacing(2) + + self.original_name = file_path.name + self.mime_type = get_mime_type(file_path) + self._raw_file_size: int | None = None + self.file_size_display: str + try: + self._raw_file_size = file_path.stat().st_size + self.file_size_display = _format_file_size(self._raw_file_size) + except OSError: + self.file_size_display = "Unbekannt" + + name_label = QLabel(f"{self.original_name}") + meta_label = QLabel( + ( + f"{self.file_size_display} • Hinzugefügt: " + f"{self.created_at_string}" + ) + ) + meta_label.setStyleSheet("font-size: 11px;") + + # additional note field for more comprehensible file descriptions + self.note_edit = QLineEdit() + self.note_edit.setPlaceholderText("Eigene Anmerkung / Bez. eingeben (optional)...") + self.note_edit.setStyleSheet(""" + QLineEdit { + font-size: 11px; + padding: 2px 5px; + min-height: 22px; /* avoid vertical cropping of text */ + border: 1px solid #CCCCCC; + border-radius: 3px; + } + QLineEdit:focus { + border-color: #0078D4; /* blue frame on click */ + } + """) + + info_vbox.addWidget(name_label) + info_vbox.addWidget(meta_label) + info_vbox.addWidget(self.note_edit) + layout.addLayout(info_vbox, stretch=2) + + # category dropdown: not needed by WCE, but still included to re-enable later + # TODO maybe add later + self.category_combo = QComboBox() + self.category_combo.addItems( + [ + "Allgemein / Unsortiert", + "Identitätsnachweis", + "Vertrag", + "Gewerbeanmeldung", + "Sonstiges", + ] + ) + self.category_combo.setFixedWidth(160) + layout.addWidget(self.category_combo) + self.category_combo.setVisible(False) + + # action buttons: view and delete + self.btn_preview = QPushButton("👁️") + self.btn_preview.setToolTip("Datei im Standardprogramm öffnen") + self.btn_preview.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn_preview.setFixedWidth(36) + self.btn_preview.clicked.connect(self._open_preview) + + self.btn_delete = QPushButton("🗑️") + self.btn_delete.setToolTip("Datei entfernen") + self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor) + self.btn_delete.setFixedWidth(36) + self.btn_delete.clicked.connect(lambda: self.request_remove.emit(self)) + + layout.addWidget(self.btn_preview) + layout.addWidget(self.btn_delete) + + def _open_preview(self): + QDesktopServices.openUrl(QUrl.fromLocalFile(self.file_path)) + + # TODO change to protocol/interface with state + def get_data(self): + return { + "file_path": self.file_path, + "created_at": self._created_at_UTC, + "category": None, + "note": self.note_edit.text().strip(), + } + + +class DocumentSectionWidget(QWidget): + """main widget for file selection""" + + count_changed = Signal(int) # sends current number of files contained by the widget + + def __init__( + self, + parent: QWidget | None = None, + ): + super().__init__(parent) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + + # Dropzone at the top + self.drop_zone = _DropZoneWidget() + self.drop_zone.files_dropped.connect(self._add_files) + main_layout.addWidget(self.drop_zone) + + # list containing the file items + self.file_list_widget = QListWidget() + self.file_list_widget.setStyleSheet(""" + QListWidget { + border: none; + background: transparent; + padding: 0px; + margin: 0px; + } + QListWidget::item { + padding: 0px; + margin: 0px 0px 6px 0px; /* 6px Abstand zwischen den einzelnen Datei-Zeilen */ + } + """) + + main_layout.addWidget(self.file_list_widget) + + def _add_files( + self, + filepaths: Iterable[Path], + ): + for path in filepaths: + # create custom row widget + item_widget = _FileItemWidget(path) + item_widget.request_remove.connect(self._remove_file_item) + + # embed in QListWidget + list_item = QListWidgetItem(self.file_list_widget) + # min size to avoid cropping of text + size_hint = item_widget.sizeHint() + size_hint.setHeight(size_hint.height() + 4) + list_item.setSizeHint(size_hint) + + self.file_list_widget.addItem(list_item) + self.file_list_widget.setItemWidget(list_item, item_widget) + + self.count_changed.emit(self.file_list_widget.count()) + + def _remove_file_item( + self, + widget: QWidget, + ): + # search and delete row + for i in range(self.file_list_widget.count()): + item = self.file_list_widget.item(i) + if self.file_list_widget.itemWidget(item) == widget: + self.file_list_widget.takeItem(i) + break + self.count_changed.emit(self.file_list_widget.count()) + + # TODO change to protocol/interface with state + def get_all_documents_data(self): + """Methode zum Auslesen aller angehängten Dateien inkl. Metadaten für das Speichern.""" + documents = [] + for i in range(self.file_list_widget.count()): + item = self.file_list_widget.item(i) + widget = self.file_list_widget.itemWidget(item) + if isinstance(widget, _FileItemWidget): + documents.append(widget.get_data()) + return documents + + +class _MainWindow(QMainWindow): + """main window for internal test only, not the this module's component""" + + def __init__(self): + super().__init__() + self.setWindowTitle("Prototyp: Dokumente & Anhänge") + self.resize(650, 500) + + central_widget = QWidget() + layout = QVBoxLayout(central_widget) + + # simulate custom widget in form + self.header_label = QLabel("📎 Dokumente & Anhänge (0)") + self.header_label.setStyleSheet("font-size: 14px; color: #333333;") + layout.addWidget(self.header_label) + + # custom widget + self.doc_section = DocumentSectionWidget() + self.doc_section.count_changed.connect(self.update_header) + layout.addWidget(self.doc_section) + + # test button to trigger data gathering (simulation for saving procedure and + # backend interaction) + self.btn_save = QPushButton("Erfassung speichern (Daten auslesen)") + self.btn_save.setStyleSheet( + "background-color: #0078D4; color: white; padding: 8px; font-weight: bold;" + ) + self.btn_save.clicked.connect(self.print_backend_data) + layout.addWidget(self.btn_save) + + self.setCentralWidget(central_widget) + + def update_header(self, count): + """update counter in corresponding label, must be mapped to section heading in form""" + self.header_label.setText(f"📎 Dokumente & Anhänge ({count})") + + def print_backend_data(self): + data = self.doc_section.get_all_documents_data() + print("\n--- DATEN FÜR SPEICHERVORGANG BEREIT ---") + for i, doc in enumerate(data, 1): + print(f"Datei {i}:") + print(f" Pfad: {doc['file_path']}") + print(f" Datum: {doc['created_at']}") + print(f" Kategorie: {doc['category']}") + print(f" Anmerkung: {doc['note']}") + + +def main() -> None: + app = QApplication(sys.argv) + window = _MainWindow() + window.show() + sys.exit(app.exec()) + + +if TYPE_CHECKING: + # enable static type checking for the protocol + _m1: type[Module] = DocumentSectionWidget + _m2: type[Module] = _FileItemWidget + +if __name__ == "__main__": + main() diff --git a/prototypes/tests.py b/prototypes/tests.py index 46d3b72..c7f38d5 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -275,7 +275,7 @@ print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") # %% db_path = constants.Config.DB_PATH_MAIN -crm_path = constants.Config.DB_PATH_CRM +crm_path = constants.Config.DB_PATH_KONTAKTLISTE engine = sql.create_engine(f"sqlite:///{str(db_path)}") engine_crm = sql.create_engine(f"sqlite:///{str(crm_path)}") # %% diff --git a/src/wce_crm/constants.py b/src/wce_crm/constants.py index 8c9f695..f4f389d 100644 --- a/src/wce_crm/constants.py +++ b/src/wce_crm/constants.py @@ -19,7 +19,7 @@ assert BASE_PATH class Config: DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None)) - DB_PATH_CRM: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db") + DB_PATH_KONTAKTLISTE: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db") DB_PATH_MAIN: Path = BASE_PATH / os.getenv( "DOPT_DB_MAIN", "data/db/wce_grunderfassung.db" ) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 4a3f578..38b1989 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -5,6 +5,7 @@ import dataclasses as dc import datetime import json from collections.abc import Iterable, Sequence +from pathlib import Path from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar from dopt_basics.datastructures import DualDict @@ -265,6 +266,34 @@ class Page_InitRec_Form_State(PageState[Module]): geloescht: bool = False +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_DocumentSection_State: + session: Session + pers_id: int | None + docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list) + locked: bool + geloescht: bool = False + + @property + def doc_count(self) -> int: + return len(self.docs) + + +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_DocumentSection_Doc_State: + id: int | None + pers_id: int | None + datum_erstellt_utc: datetime.datetime = dc.field( + default_factory=lambda: datetime.datetime.now(datetime.UTC) + ) + + speicher_pfad: Path + dateiname_original: str + dateigroesse: int | None + locked: bool + geloescht: bool = False + + @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_State(PageState[Module]): session: Session diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 2b4b231..acc99b6 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -45,29 +45,40 @@ class UTCDateTime(TypeDecorator): if value is not None: # Ensure it's converted to UTC before saving if value.tzinfo is None: - value = value.replace(tzinfo=datetime.timezone.utc) + value = value.replace(tzinfo=datetime.UTC) else: - value = value.astimezone(datetime.timezone.utc) + value = value.astimezone(datetime.UTC) return value def process_result_value(self, value, dialect): """Runs when fetching from the database.""" if value is not None and value.tzinfo is None: # Explicitly tell Python this data *is* UTC - value = value.replace(tzinfo=datetime.timezone.utc) + value = value.replace(tzinfo=datetime.UTC) return value MD_CRM = sa.MetaData() MD_MAIN = sa.MetaData() -ENGINE = sa.create_engine(f"sqlite:///{constants.Config.DB_PATH_MAIN}") +URL_MAIN = sa.engine.URL.create( + "sqlite+pysqlite", + database=str(constants.Config.DB_PATH_MAIN), +) +ENGINE = sa.create_engine( + URL_MAIN, + connect_args={"uri": True, "timeout": 0.5}, +) + +KONTAKTLISTE_URI = constants.Config.DB_PATH_KONTAKTLISTE.as_uri() + "?mode=ro" @event.listens_for(ENGINE, "connect") -def attach_second_db(dbapi_connection, connection_record): +def _db_conn_event(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() # 'kontaktliste' is the alias for the second DB - cursor.execute(f"ATTACH DATABASE '{constants.Config.DB_PATH_CRM}' AS kontaktliste") + cursor.execute("ATTACH DATABASE ? AS kontaktliste", (KONTAKTLISTE_URI,)) + # enable foreign keys + cursor.execute("PRAGMA foreign_keys=ON") cursor.close() @@ -146,7 +157,7 @@ def get_ext_crm_master( return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema) -DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_CRM) +DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_KONTAKTLISTE) ext_crm_nutzer: Table = Table( "Nutzer", @@ -253,7 +264,7 @@ def get_ext_crm_contact_person( # df_contact_person = get_ext_crm_contact_person(None) -DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_CRM) +DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_KONTAKTLISTE) t_ext_crm_master_attach: Table = Table( "Master", diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index c0da687..99bd975 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -132,7 +132,7 @@ class CountryList: def get_country_list_german() -> CountryList: - locale = babel.Locale("de", "DE") + locale = babel.Locale("en", "GB") countries: list[tuple[str, str]] = [] iso_to_country: dict[str, str] = {} @@ -938,7 +938,7 @@ INITREC_PERSON = [ trigger_value="ja", children=[ FormField( - "Relevanz", + "Relevant perspektivERZ", FormFieldType.DROPDOWN, required=True, options=[("ja", None), ("nein", None)], diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 2cd2a18..b1a1c6d 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -3256,7 +3256,7 @@ class Page_InitRec(QWidget): 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.reset_btn.clicked.connect(self._trigger_reset_btn) self.delete_btn.clicked.connect(self._delete_data) header_layout.addSpacing(5) header_layout.addWidget(self.edit_buttons) @@ -3477,6 +3477,21 @@ class Page_InitRec(QWidget): ) return all(valid_id_states) + def _trigger_reset_btn(self) -> None: + assert not self.STATE.locked, "reset initialised, even though form is locked" + confirm = get_user_confirmation( + self, + "Zurücksetzen bestätigen", + ( + "Soll der Eintrag wirklich zurückgesetzt werden? Alle nicht " + "gespeicherten Änderungen gehen verloren." + ), + ) + if not confirm: + return + + self._sync_state_to_GUI(None) + def _delete_data(self) -> None: assert self._deletion_valid(), "deletion initialised despite IDs are not properly set" confirm = get_user_confirmation( @@ -3525,7 +3540,6 @@ class Page_InitRec(QWidget): return # TODO add loading logic with backend call - # !! just a dummy initrec = backend.page_initrec_from_db(ids) # TODO re-enable later @@ -4589,6 +4603,7 @@ class Page_Consulting(QWidget): container_layout.addWidget(self.info_banners_container) # --- BUTTONS --- + # TODO check if no reset button is correct self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=False) modify_btn = self.edit_buttons.modify_button assert modify_btn @@ -6165,7 +6180,7 @@ class EditButtons(QWidget): self.save_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) layout_btn.addWidget(self.save_btn) # reset - self.reset_button: QPushButton | None = None + self.reset_btn: QPushButton | None = None if add_reset_btn: self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)") self.reset_btn.setShortcut("Ctrl+Z") @@ -6185,12 +6200,15 @@ class EditButtons(QWidget): self, enable_modification: bool, set_delete_btn: bool = True, + set_reset_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) + if self.reset_btn is not None and set_reset_btn: + self.reset_btn.setEnabled(enable_modification) class MainWindow(QMainWindow): @@ -6220,8 +6238,6 @@ class MainWindow(QMainWindow): 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.new_consulting_requested.connect(self.show_page_consulting) self.new_entry_select.new_initrec_requested.connect(self.show_page_initrec) self.stack.addWidget(self.new_entry_select) @@ -6504,7 +6520,6 @@ def qt_message_handler(mode, context, 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 diff --git a/src/wce_crm/gui_components/__init__.py b/src/wce_crm/gui_components/__init__.py new file mode 100644 index 0000000..e69de29