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