refactoring and basic loading of existing consultations on the company profile

This commit is contained in:
2026-07-13 17:50:59 +02:00
parent b41d55db7d
commit eb66b435a0
4 changed files with 572 additions and 286 deletions
+66 -2
View File
@@ -10,9 +10,20 @@ from dopt_basics.result_pattern import wrap_result
from wce_crm import db from wce_crm import db
from wce_crm.constants import TIMEZONE_CEST from wce_crm.constants import TIMEZONE_CEST
from wce_crm.data_models import Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang from wce_crm.data_models import (
Beratungsgespraech_Einzelgespraech,
Beratungsgespraech_Vorgang,
)
from wce_crm.logging import logger_back as logger from wce_crm.logging import logger_back as logger
from wce_crm.types import CompanyInfo, ContactPersonInfo, InitRecType, MainPageEntry from wce_crm.types import (
CompanyInfo,
CompanyProfileConsultationEntry,
CompanyProfileConsultations,
ConsultingType,
ContactPersonInfo,
InitRecType,
MainPageEntry,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId
@@ -393,6 +404,59 @@ def page_consulting_from_db(
return consultation_data return consultation_data
# // consulting page interaction
def companyprofile_page_get_consultations(
un_id: RecId,
) -> CompanyProfileConsultations:
logger.debug("[Call backend] _companyprofile_page_get_consultations")
stmt = sql.select(
db.beratung_vorgang.c.vorgang_id,
db.beratung_vorgang.c.aktualisiert,
db.beratung_vorgang.c.titel,
db.beratung_vorgang.c.beratungs_typ,
).where(db.beratung_vorgang.c.un_id == un_id)
with db.ENGINE.connect() as conn:
res = conn.execute(stmt)
cons_entries_pauschal: list[CompanyProfileConsultationEntry] = []
cons_entries_individual: list[CompanyProfileConsultationEntry] = []
for entry in res.mappings():
cons_id = entry["vorgang_id"]
assert cons_id, "no VorgangID defined"
datetime_updated = cast(datetime.datetime, entry["aktualisiert"])
datetime_updated = datetime_updated.astimezone(TIMEZONE_CEST)
base_title = entry["titel"]
_cons_type = entry["beratungs_typ"]
cons_type = ConsultingType(_cons_type)
con_entry = CompanyProfileConsultationEntry(
cons_id=cons_id,
title=base_title,
date_updated=datetime_updated,
cons_type=cons_type,
)
if cons_type is ConsultingType.PAUSCHAL:
cons_entries_pauschal.append(con_entry)
elif cons_type is ConsultingType.INDIVIDUAL:
cons_entries_individual.append(con_entry)
else:
raise TypeError(f"Unknown consulting type: {_cons_type}")
cons_entries_pauschal.sort(key=lambda x: x.date_updated, reverse=True)
cons_entries_individual.sort(key=lambda x: x.date_updated, reverse=True)
return CompanyProfileConsultations(
pauschal=cons_entries_pauschal,
individual=cons_entries_individual,
)
# // main page interaction # // main page interaction
def _main_page_get_company_list() -> list[MainPageEntry]: def _main_page_get_company_list() -> list[MainPageEntry]:
logger.debug("[Call backend] get_company_list") logger.debug("[Call backend] get_company_list")
+209 -9
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import copy
import dataclasses as dc
import datetime import datetime
import enum
import json import json
from typing import Annotated, Any, Final, Literal from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar
from pydantic import ( from pydantic import (
AwareDatetime, AwareDatetime,
@@ -16,8 +18,16 @@ from pydantic import (
) )
from pydantic_core import ErrorDetails from pydantic_core import ErrorDetails
ValidAge = Annotated[int, Field(ge=0, le=99)] from wce_crm.form_defs import FormField
from wce_crm.types import ConsId, ConsultingType, RecId, UserId
if TYPE_CHECKING:
from wce_crm.gui import Page_Consulting_ConsultingSession # noqa: F401
ValidAge = Annotated[int, Field(ge=0, le=99)]
COLUMN_SEP: Final[str] = "__"
M = TypeVar("M", bound="Module")
S = TypeVar("S", bound="PageState")
# // validation translation # // validation translation
GERMAN_ERROR_MESSAGES: Final[dict[str, str]] = { GERMAN_ERROR_MESSAGES: Final[dict[str, str]] = {
@@ -64,9 +74,204 @@ def _parse_json(value: Any) -> str:
raise TypeError raise TypeError
COLUMN_SEP: Final[str] = "__" # // GUI states and types
def set_page_state(
target: S,
source: S,
deep: bool = False,
) -> None:
"""
transfer all field values generically from 'source' to 'target'
"""
if not (dc.is_dataclass(target) and dc.is_dataclass(source)):
raise TypeError("Both source and atrget must be dataclasses")
if type(target) is not type(source):
raise TypeError("Both source and target must have the same type")
for field in dc.fields(source):
if field.name == "child_modules":
continue
new_value = getattr(source, field.name)
if deep:
new_value = copy.deepcopy(new_value)
setattr(target, field.name, new_value)
class WrapperModule(Protocol):
def _sync_state_to_GUI(self) -> 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:
"""call `get_state` method on all child modules and assign respective
properties to state of parent module
"""
...
def load_state(self, new_state: Any) -> None: ...
def get_state(self) -> Any:
"""should always be a simple call to `_sync_GUI_to_state` and then returning
the module's state
Returns
-------
Any
state of the respective module
"""
...
class Module(WrapperModule, Protocol):
def validate(self) -> list[str]:
"""should be a list of field names which could not be validated successfully,
propagates through: parent components must call this on all child modules and extend
their own error list with the return value
Returns
-------
list[str]
list of error fields (form field label)
"""
...
def save_data(self) -> None: ...
class AutoFormInsert(Protocol):
def __call__(
self,
data: dict[str, Any],
) -> RecId: ...
class AutoFormUpdate(Protocol):
def __call__(
self,
id_: int,
data: dict[str, Any],
) -> None: ...
class AutoFormGet(Protocol):
def __call__(
self,
id_: int,
) -> dict[str, Any]: ...
class AutoFormDelete(Protocol):
def __call__(
self,
id_: int,
) -> None: ...
@dc.dataclass(slots=True)
class Session:
user_id: UserId
user_name: str
@dc.dataclass(slots=True)
class PageState(Generic[M]):
child_modules: list[M] = dc.field(default_factory=list)
@dc.dataclass(slots=True, kw_only=True)
class Page_MainPage_State(PageState[Module]):
session: Session
row_count: int = 0
@dc.dataclass(slots=True, kw_only=True)
class Page_NewInitRec_State(PageState[Module]):
session: Session
@dc.dataclass(slots=True, kw_only=True)
class Page_InitRecCompany_State(PageState[Module]):
session: Session
un_id: RecId | None = None
@dc.dataclass(slots=True, kw_only=True)
class AutoFormConfig:
model: type[FlatBaseModel]
data_insert: AutoFormInsert
data_update: AutoFormUpdate
data_get: AutoFormGet
data_delete: AutoFormDelete
form_fields: Sequence[FormField]
ignored_keys: Iterable[str] = tuple()
add_buttons: bool = True
@dc.dataclass(slots=True, kw_only=True)
class AutoForm_State(PageState[Module]):
session: Session
cfg: AutoFormConfig
rec_id: RecId | None = None
form_data: dict[str, Any] | None = None
@dc.dataclass(slots=True, kw_only=True)
class Page_InitRecPerson_State(PageState[Module]):
session: Session
pers_id: RecId | None = None
@dc.dataclass(slots=True, kw_only=True)
class Page_Consulting_State(PageState[Module]):
session: Session
vorgang_id: ConsId | None
un_id: RecId | None = None
pers_id: RecId | None = None
titel: str = "TEST-TITEL" # add title later to GUI or build it in the background
beratungs_typ: ConsultingType
cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field(
default_factory=list
)
ist_geloescht: bool = False
@dc.dataclass(slots=True, kw_only=True)
class Page_Consulting_ConsultingSession_State(PageState[Module]):
session: Session
nutzer_id: UserId # must be set with the session (always known)
nutzer_name: str # must be set with the session (always known)
beratung_id: int | None # known and not known entries
zeitstempel: datetime.datetime | None = None
# dc.field(default_factory=lambda: datetime.datetime.now(tz=datetime.UTC))
ansprechpartner: str | None = None
kommunikationsweg: str | None = None
thema_crm_matrix: str | None = (
None # TODO should be linked to something, target unclear (need specification by customer)
)
anmerkungen: str | None = None
rueckmeldung: str | None = None
ist_geloescht: bool = False
@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)
@dc.dataclass(slots=True, kw_only=True)
class Page_CompanyProfile_State(PageState[Module]):
session: Session
rec_id: RecId | None = None
# // Pydantic models
class FlatBaseModel(BaseModel): class FlatBaseModel(BaseModel):
""" """
Optimised Pydantic base class, which parses JSON strings and column Optimised Pydantic base class, which parses JSON strings and column
@@ -347,11 +552,6 @@ class Grunderfassung_Sprachen(BaseModel):
# ** Consulting # ** Consulting
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
class Beratungsgespraech_Vorgang(BaseModel): class Beratungsgespraech_Vorgang(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True) model_config = ConfigDict(str_strip_whitespace=True)
+263 -261
View File
@@ -7,13 +7,12 @@ import re
import sys import sys
import traceback import traceback
from collections import defaultdict from collections import defaultdict
from collections.abc import Container, Iterable, Sequence from collections.abc import Callable, Container, Iterable, Sequence
from pprint import pformat from pprint import pformat
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Final, Final,
Generic,
Literal, Literal,
Protocol, Protocol,
TypeAlias, TypeAlias,
@@ -80,12 +79,22 @@ from wce_crm.backend import backend
from wce_crm.constants import TIMEZONE_CEST from wce_crm.constants import TIMEZONE_CEST
from wce_crm.data_models import ( from wce_crm.data_models import (
COLUMN_SEP, COLUMN_SEP,
AutoForm_State,
AutoFormConfig,
Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Einzelgespraech,
Beratungsgespraech_Vorgang, Beratungsgespraech_Vorgang,
ConsultingType,
FlatBaseModel,
Grunderfassung_Personen, Grunderfassung_Personen,
Grunderfassung_Unternehmen, Grunderfassung_Unternehmen,
Page_CompanyProfile_State,
Page_Consulting_ConsultingSession_State,
Page_Consulting_State,
Page_Consulting_Table_State,
Page_InitRecCompany_State,
Page_InitRecPerson_State,
Page_MainPage_State,
Page_NewInitRec_State,
Session,
set_page_state,
translate_pydantic_errors, translate_pydantic_errors,
) )
from wce_crm.form_defs import ( from wce_crm.form_defs import (
@@ -101,16 +110,17 @@ from wce_crm.logging import (
logger_gui, logger_gui,
logger_page_consulting, logger_page_consulting,
) )
from wce_crm.types import CompanyProfileConsultationEntry, ConsultingType
if TYPE_CHECKING: if TYPE_CHECKING:
from wce_crm.types import ConsId, ExtMaId, RecId, UserId from wce_crm.data_models import AutoForm_State, Module, WrapperModule
from wce_crm.types import ConsId, ExtMaId
K = TypeVar("K") K = TypeVar("K")
V = TypeVar("V") V = TypeVar("V")
T = TypeVar("T") T = TypeVar("T")
W = TypeVar("W", bound="QWidget") W = TypeVar("W", bound="QWidget")
M = TypeVar("M", bound="Module")
S = TypeVar("S", bound="PageState")
DEBUG: bool = True DEBUG: bool = True
DEBUG_SEARCH_WIDGET: bool = False DEBUG_SEARCH_WIDGET: bool = False
@@ -137,6 +147,60 @@ QSS = """
*[styleClass="stempel"]:focus { *[styleClass="stempel"]:focus {
border: 1px dashed #cbd5e1; border: 1px dashed #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="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="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 ---" DROPDOWN_DEFAULT: Final[str] = "--- Bitte wählen ---"
DYNAMIC_LIST_KEY_PATTERN: Final[re.Pattern] = re.compile(r"-\[(\d+)\]") DYNAMIC_LIST_KEY_PATTERN: Final[re.Pattern] = re.compile(r"-\[(\d+)\]")
@@ -199,78 +263,6 @@ class CustomForm(Protocol):
def validate_form_data(self) -> list[str]: ... def validate_form_data(self) -> list[str]: ...
class WrapperModule(Protocol):
def _sync_state_to_GUI(self) -> 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:
"""call `get_state` method on all child modules and assign respective
properties to state of parent module
"""
...
def load_state(self, new_state: Any) -> None: ...
def get_state(self) -> Any:
"""should always be a simple call to `_sync_GUI_to_state` and then returning
the module's state
Returns
-------
Any
state of the respective module
"""
...
class Module(WrapperModule, Protocol):
def validate(self) -> list[str]:
"""should be a list of field names which could not be validated successfully,
propagates through: parent components must call this on all child modules and extend
their own error list with the return value
Returns
-------
list[str]
list of error fields (form field label)
"""
...
def save_data(self) -> None: ...
class AutoFormInsert(Protocol):
def __call__(
self,
data: dict[str, Any],
) -> backend.RecId: ...
class AutoFormUpdate(Protocol):
def __call__(
self,
id_: int,
data: dict[str, Any],
) -> None: ...
class AutoFormGet(Protocol):
def __call__(
self,
id_: int,
) -> dict[str, Any]: ...
class AutoFormDelete(Protocol):
def __call__(
self,
id_: int,
) -> None: ...
class CustomWidget(QWidget): class CustomWidget(QWidget):
def __init__( def __init__(
self, self,
@@ -292,104 +284,6 @@ class CustomWidget(QWidget):
def validate_form_data(self) -> list[str]: ... def validate_form_data(self) -> list[str]: ...
@dc.dataclass(slots=True)
class Session:
user_id: UserId
user_name: str
@dc.dataclass(slots=True)
class PageState(Generic[M]):
child_modules: list[M] = dc.field(default_factory=list)
@dc.dataclass(slots=True, kw_only=True)
class Page_MainPage_State(PageState[Module]):
session: Session
row_count: int = 0
@dc.dataclass(slots=True, kw_only=True)
class Page_NewInitRec_State(PageState[Module]):
session: Session
@dc.dataclass(slots=True, kw_only=True)
class Page_InitRecCompany_State(PageState[Module]):
session: Session
un_id: RecId | None = None
@dc.dataclass(slots=True, kw_only=True)
class AutoFormConfig:
model: type[FlatBaseModel]
data_insert: AutoFormInsert
data_update: AutoFormUpdate
data_get: AutoFormGet
data_delete: AutoFormDelete
form_fields: Sequence[FormField]
ignored_keys: Iterable[str] = tuple()
add_buttons: bool = True
@dc.dataclass(slots=True, kw_only=True)
class AutoForm_State(PageState[Module]):
session: Session
cfg: AutoFormConfig
rec_id: RecId | None = None
form_data: dict[str, Any] | None = None
@dc.dataclass(slots=True, kw_only=True)
class Page_InitRecPerson_State(PageState[Module]):
session: Session
pers_id: RecId | None = None
@dc.dataclass(slots=True, kw_only=True)
class Page_Consulting_State(PageState[Module]):
session: Session
vorgang_id: ConsId | None
un_id: RecId | None = None
pers_id: RecId | None = None
titel: str = "TEST-TITEL" # add title later to GUI or build it in the background
beratungs_typ: ConsultingType
cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field(
default_factory=list
)
ist_geloescht: bool = False
@dc.dataclass(slots=True, kw_only=True)
class Page_Consulting_ConsultingSession_State(PageState[Module]):
session: Session
nutzer_id: UserId # must be set with the session (always known)
nutzer_name: str # must be set with the session (always known)
beratung_id: int | None # known and not known entries
zeitstempel: datetime.datetime | None = None
# dc.field(default_factory=lambda: datetime.datetime.now(tz=datetime.UTC))
ansprechpartner: str | None = None
kommunikationsweg: str | None = None
thema_crm_matrix: str | None = (
None # TODO should be linked to something, target unclear (need specification by customer)
)
anmerkungen: str | None = None
rueckmeldung: str | None = None
ist_geloescht: bool = False
@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)
@dc.dataclass(slots=True, kw_only=True)
class Page_CompanyProfile_State(PageState[Module]):
session: Session
rec_id: RecId | None = None
def _add_widget_to_layout( def _add_widget_to_layout(
form_field: FormField[W], form_field: FormField[W],
widget: QWidget, widget: QWidget,
@@ -2581,25 +2475,43 @@ class NoScrollFilter(QObject):
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
class MainPage_ClickableCell(QFrame): class HeaderCell(QLabel):
"""cell in the table on the startup screen""" """basic header for table like objects"""
clicked = Signal(types.MainPageEntry) 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__( def __init__(
self, self,
text: str, text: str,
data_record: types.MainPageEntry,
): ):
super().__init__() super().__init__()
self.data_record = data_record
self.setStyleSheet(""" self.setStyleSheet("""
MainPage_ClickableCell { ClickableCell {
background-color: white; background-color: white;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;
} }
MainPage_ClickableCell:hover { ClickableCell:hover {
background-color: #eff6ff; background-color: #eff6ff;
border: 1px solid #60a5fa; border: 1px solid #60a5fa;
} }
@@ -2610,29 +2522,41 @@ class MainPage_ClickableCell(QFrame):
label.setAlignment(Qt.AlignmentFlag.AlignCenter) label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(label) 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): def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton: if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit(self.data_record) self.clicked.emit(self.data_record)
class MainPage_HeaderCell(QLabel): class MainPage_ClickableCell(ClickableCell):
"""cell in the table on the startup screen"""
clicked = Signal(types.MainPageEntry)
def __init__( def __init__(
self, self,
text: str, text: str,
font_size: int = 12, data_record: types.MainPageEntry,
): ):
super().__init__(text) super().__init__(text=text)
self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.data_record = data_record
self.setStyleSheet(f"""
MainPage_HeaderCell {{ def mousePressEvent(self, event):
background-color: #e2e8f0; if event.button() == Qt.MouseButton.LeftButton:
color: #475569; self.clicked.emit(self.data_record)
font-weight: bold;
padding: 10px;
border-radius: 6px;
font-size: {font_size}px;
}}
""")
class Page_NewInitRec(QWidget): class Page_NewInitRec(QWidget):
@@ -2867,6 +2791,9 @@ class Page_InitRecCompany(QWidget):
) )
def _request_company_profile(self) -> None: 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)"
)
req_state = Page_CompanyProfile_State( req_state = Page_CompanyProfile_State(
session=self.STATE.session, session=self.STATE.session,
rec_id=self.STATE.un_id, rec_id=self.STATE.un_id,
@@ -3148,6 +3075,25 @@ class Page_CompanyProfile(QWidget):
change_title_btn.clicked.connect(self.set_company_name) change_title_btn.clicked.connect(self.set_company_name)
container_layout.addLayout(layout_change_title) 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 = QFrame()
separator2.setFrameShape(QFrame.Shape.HLine) separator2.setFrameShape(QFrame.Shape.HLine)
separator2.setFrameShadow(QFrame.Shadow.Sunken) separator2.setFrameShadow(QFrame.Shadow.Sunken)
@@ -3155,7 +3101,7 @@ class Page_CompanyProfile(QWidget):
container_layout.addSpacing(30) container_layout.addSpacing(30)
self.title_company_name = MainPage_HeaderCell("Unternehmen: n.v.", font_size=16) self.title_company_name = HeaderCell("Unternehmen: n.v.", font_size=16)
container_layout.addWidget(self.title_company_name) container_layout.addWidget(self.title_company_name)
container_layout.addSpacing(20) container_layout.addSpacing(20)
@@ -3171,7 +3117,7 @@ class Page_CompanyProfile(QWidget):
left_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum) left_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
left_layout = QVBoxLayout(left_frame) left_layout = QVBoxLayout(left_frame)
main_hor_layout.addWidget(left_frame, stretch=1, alignment=Qt.AlignmentFlag.AlignTop) main_hor_layout.addWidget(left_frame, stretch=1, alignment=Qt.AlignmentFlag.AlignTop)
master_data_label = MainPage_HeaderCell("Stammdaten & Vermerke") master_data_label = HeaderCell("Stammdaten & Vermerke")
master_data_label.setSizePolicy( master_data_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
@@ -3193,21 +3139,23 @@ class Page_CompanyProfile(QWidget):
top_right_frame.setSizePolicy( top_right_frame.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
) )
top_right_layout = QVBoxLayout(top_right_frame) self.layout_cons_pauschal = QVBoxLayout(top_right_frame)
common_consultancy_label = MainPage_HeaderCell("Pauschalberatung") self.cons_pauschal: list[CompanyProfile_Consultation_ClickableCell] = []
common_consultancy_label = HeaderCell("Pauschalberatung")
common_consultancy_label.setSizePolicy( common_consultancy_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
top_right_layout.addWidget(common_consultancy_label) self.layout_cons_pauschal.addWidget(common_consultancy_label)
self.add_btn_pauschal = QPushButton("+ Hinzufügen") self.add_btn_pauschal = QPushButton("+ Hinzufügen")
self.add_btn_pauschal.clicked.connect( self.add_btn_pauschal.clicked.connect(
lambda: self._request_consulting(ConsultingType.PAUSCHAL) lambda: self._request_new_consulting(ConsultingType.PAUSCHAL)
) )
self.add_btn_pauschal.setSizePolicy( self.add_btn_pauschal.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
self.add_btn_pauschal.setMinimumHeight(30) self.add_btn_pauschal.setMinimumHeight(30)
top_right_layout.addWidget(self.add_btn_pauschal) self.add_btn_pauschal.setProperty("styleClass", "flatAddButton")
self.layout_cons_pauschal.addWidget(self.add_btn_pauschal)
# bottom right area # bottom right area
bottom_right_frame = QFrame() bottom_right_frame = QFrame()
@@ -3215,21 +3163,23 @@ class Page_CompanyProfile(QWidget):
bottom_right_frame.setSizePolicy( bottom_right_frame.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
) )
bottom_right_layout = QVBoxLayout(bottom_right_frame) self.layout_cons_indiv = QVBoxLayout(bottom_right_frame)
individual_consultancy_label = MainPage_HeaderCell("Individualberatung") self.cons_indiv: list[CompanyProfile_Consultation_ClickableCell] = []
individual_consultancy_label = HeaderCell("Individualberatung")
individual_consultancy_label.setSizePolicy( individual_consultancy_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
bottom_right_layout.addWidget(individual_consultancy_label) self.layout_cons_indiv.addWidget(individual_consultancy_label)
self.add_btn_individual = QPushButton("+ Hinzufügen") self.add_btn_individual = QPushButton("+ Hinzufügen")
self.add_btn_individual.clicked.connect( self.add_btn_individual.clicked.connect(
lambda: self._request_consulting(ConsultingType.INDIVIDUAL) lambda: self._request_new_consulting(ConsultingType.INDIVIDUAL)
) )
self.add_btn_individual.setSizePolicy( self.add_btn_individual.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
) )
self.add_btn_individual.setMinimumHeight(30) self.add_btn_individual.setMinimumHeight(30)
bottom_right_layout.addWidget(self.add_btn_individual) 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(top_right_frame)
right_column_layout.addWidget(bottom_right_frame) right_column_layout.addWidget(bottom_right_frame)
@@ -3270,15 +3220,37 @@ class Page_CompanyProfile(QWidget):
# } # }
# """) # """)
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: def _request_initrec_company(self) -> None:
req_state = Page_InitRecCompany_State( req_state = Page_InitRecCompany_State(
session=self.STATE.session, session=self.STATE.session,
un_id=self.STATE.rec_id, un_id=self.STATE.rec_id,
) )
logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) logger_gui.debug("[Page Company Profile] State to call: %s", req_state)
self.back_requested.emit(req_state) self.back_requested.emit(req_state)
def _request_consulting( def _request_new_consulting(
self, self,
cons_type: ConsultingType, cons_type: ConsultingType,
) -> None: ) -> None:
@@ -3293,6 +3265,62 @@ class Page_CompanyProfile(QWidget):
) )
self.consulting_requested.emit(new_state) self.consulting_requested.emit(new_state)
def _request_existing_consulting(
self,
data: CompanyProfileConsultationEntry,
) -> None:
assert self.STATE.rec_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.rec_id,
beratungs_typ=data.cons_type,
)
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:
assert self.STATE.rec_id is not None, (
"cannot update consultations if RecID is not set"
)
consultations = backend.companyprofile_page_get_consultations(un_id=self.STATE.rec_id)
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 _disable_add_btns(self) -> None: def _disable_add_btns(self) -> None:
self.add_btn_pauschal.setEnabled(False) self.add_btn_pauschal.setEnabled(False)
self.add_btn_individual.setEnabled(False) self.add_btn_individual.setEnabled(False)
@@ -3797,13 +3825,18 @@ class Page_Consulting(QWidget):
self, self,
text: str, text: str,
button_text: str, button_text: str,
button_func: Callable[[], None] | None = None,
) -> None: ) -> None:
if button_text and button_func is None:
raise ValueError("Can not add button without calling function")
banner = InfoBanner( banner = InfoBanner(
text=text, text=text,
button_text=button_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.append(banner)
banner.link_clicked.connect(self._open_linking_dialogue)
self.info_banners_layout.addWidget(banner) self.info_banners_layout.addWidget(banner)
def _sync_state_to_GUI(self) -> None: def _sync_state_to_GUI(self) -> None:
@@ -3824,11 +3857,17 @@ class Page_Consulting(QWidget):
# info banners needed # info banners needed
self._clear_info_banners() 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 self.STATE.un_id is None and self.STATE.pers_id is None:
# link missing
self._add_linking_banner( self._add_linking_banner(
text="⚠️ Dieser Eintrag ist noch nicht mit einer Entität verknüpft.", text="⚠️ Dieser Eintrag ist noch nicht mit einer Entität verknüpft.",
button_text="Jetzt verknüpfen", button_text="Jetzt verknüpfen",
button_func=self._open_linking_dialogue,
) )
elif ( elif (
self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL
@@ -3840,6 +3879,7 @@ class Page_Consulting(QWidget):
"einer Person fehlt." "einer Person fehlt."
), ),
button_text="Jetzt verknüpfen", button_text="Jetzt verknüpfen",
button_func=self._open_linking_dialogue,
) )
# child modules # child modules
@@ -4124,6 +4164,7 @@ class Page_Consulting_ConsultingSession(QWidget):
layout, layout,
widget_registry=self.widget_registry, widget_registry=self.widget_registry,
) )
self.timestamp.set_pydatetime(datetime.datetime.now(TIMEZONE_CEST))
contact_def = FormField[QLineEdit]( contact_def = FormField[QLineEdit](
"Kontaktperson", "Kontaktperson",
@@ -4302,24 +4343,10 @@ class Page_Consulting_Table(QWidget):
# --- BUTTON (ADD) --- # --- BUTTON (ADD) ---
self.btn_add = QPushButton("+ Neuen Eintrag hinzufügen") self.btn_add = QPushButton("+ Neuen Eintrag hinzufügen")
self.btn_add.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_add.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_add.setObjectName("BlueAddButton") # ID vergeben self.btn_add.setProperty("styleClass", "addButton")
main_layout.addWidget(self.btn_add) main_layout.addWidget(self.btn_add)
self.btn_add.clicked.connect(lambda: self.add_row(None, False)) self.btn_add.clicked.connect(lambda: self.add_row(None, False))
self.btn_add.setStyleSheet("""
#BlueAddButton {
background-color: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 8px 15px;
font-weight: bold;
}
#BlueAddButton:hover {
background-color: #0098ff;
}
""")
# --- HEADER --- # --- HEADER ---
header_widget = QWidget() header_widget = QWidget()
header_layout = QHBoxLayout(header_widget) header_layout = QHBoxLayout(header_widget)
@@ -4547,7 +4574,7 @@ class Page_Consulting_Table(QWidget):
class InfoBanner(QFrame): class InfoBanner(QFrame):
link_clicked = Signal() btn_clicked = Signal()
def __init__( def __init__(
self, self,
@@ -4582,43 +4609,18 @@ class InfoBanner(QFrame):
layout.setContentsMargins(15, 10, 15, 10) layout.setContentsMargins(15, 10, 15, 10)
self.label = QLabel(text) self.label = QLabel(text)
self.button = QPushButton(button_text)
layout.addWidget(self.label) layout.addWidget(self.label)
layout.addStretch() layout.addStretch()
layout.addWidget(self.button)
self.button.clicked.connect(self.link_clicked.emit) if button_text:
self.button = QPushButton(button_text)
layout.addWidget(self.button)
self.button.clicked.connect(self.btn_clicked.emit)
#################################################################### ####################################################################
def set_page_state(
target: S,
source: S,
deep: bool = False,
) -> None:
"""
transfer all field values generically from 'source' to 'target'
"""
if not (dc.is_dataclass(target) and dc.is_dataclass(source)):
raise TypeError("Both source and atrget must be dataclasses")
if type(target) is not type(source):
raise TypeError("Both source and target must have the same type")
for field in dc.fields(source):
if field.name == "child_modules":
continue
new_value = getattr(source, field.name)
if deep:
new_value = copy.deepcopy(new_value)
setattr(target, field.name, new_value)
def clear_layout( def clear_layout(
layout: QLayout | None, layout: QLayout | None,
) -> None: ) -> None:
@@ -4782,7 +4784,7 @@ class MainWindow(QMainWindow):
"Datum", "Datum",
] ]
for col_idx, title in enumerate(headers): for col_idx, title in enumerate(headers):
self.header_grid.addWidget(MainPage_HeaderCell(title), self.current_row, col_idx) self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx)
self.update_grid() self.update_grid()
+20
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, TypeAlias, TypedDict
if TYPE_CHECKING: if TYPE_CHECKING:
import polars as pl import polars as pl
UserId: TypeAlias = int UserId: TypeAlias = int
RecId: TypeAlias = int RecId: TypeAlias = int
ConsId: TypeAlias = int ConsId: TypeAlias = int
@@ -22,6 +23,11 @@ class InitRecType(enum.IntEnum):
PERSON = enum.auto() PERSON = enum.auto()
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
# // database interaction data structures # // database interaction data structures
# ** externals # ** externals
class CompanyInfo(TypedDict): class CompanyInfo(TypedDict):
@@ -83,3 +89,17 @@ class MainPageEntry:
display_name: str display_name: str
Metadaten_aktualisierung: datetime.datetime Metadaten_aktualisierung: datetime.datetime
type: InitRecType type: InitRecType
@dc.dataclass(slots=True)
class CompanyProfileConsultationEntry:
title: str
cons_id: ConsId
date_updated: datetime.datetime
cons_type: ConsultingType
@dc.dataclass(slots=True)
class CompanyProfileConsultations:
pauschal: list[CompanyProfileConsultationEntry]
individual: list[CompanyProfileConsultationEntry]