Dev #7

Merged
foefl merged 74 commits from dev into main 2026-07-31 14:19:01 +00:00
4 changed files with 572 additions and 286 deletions
Showing only changes of commit eb66b435a0 - Show all commits
+66 -2
View File
@@ -10,9 +10,20 @@ from dopt_basics.result_pattern import wrap_result
from wce_crm import db
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.types import CompanyInfo, ContactPersonInfo, InitRecType, MainPageEntry
from wce_crm.types import (
CompanyInfo,
CompanyProfileConsultationEntry,
CompanyProfileConsultations,
ConsultingType,
ContactPersonInfo,
InitRecType,
MainPageEntry,
)
if TYPE_CHECKING:
from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId
@@ -393,6 +404,59 @@ def page_consulting_from_db(
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
def _main_page_get_company_list() -> list[MainPageEntry]:
logger.debug("[Call backend] get_company_list")
+209 -9
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
import copy
import dataclasses as dc
import datetime
import enum
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 (
AwareDatetime,
@@ -16,8 +18,16 @@ from pydantic import (
)
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
GERMAN_ERROR_MESSAGES: Final[dict[str, str]] = {
@@ -64,9 +74,204 @@ def _parse_json(value: Any) -> str:
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):
"""
Optimised Pydantic base class, which parses JSON strings and column
@@ -347,11 +552,6 @@ class Grunderfassung_Sprachen(BaseModel):
# ** Consulting
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
class Beratungsgespraech_Vorgang(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
+277 -275
View File
@@ -7,13 +7,12 @@ import re
import sys
import traceback
from collections import defaultdict
from collections.abc import Container, Iterable, Sequence
from collections.abc import Callable, Container, Iterable, Sequence
from pprint import pformat
from typing import (
TYPE_CHECKING,
Any,
Final,
Generic,
Literal,
Protocol,
TypeAlias,
@@ -80,12 +79,22 @@ from wce_crm.backend import backend
from wce_crm.constants import TIMEZONE_CEST
from wce_crm.data_models import (
COLUMN_SEP,
AutoForm_State,
AutoFormConfig,
Beratungsgespraech_Einzelgespraech,
Beratungsgespraech_Vorgang,
ConsultingType,
FlatBaseModel,
Grunderfassung_Personen,
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,
)
from wce_crm.form_defs import (
@@ -101,16 +110,17 @@ from wce_crm.logging import (
logger_gui,
logger_page_consulting,
)
from wce_crm.types import CompanyProfileConsultationEntry, ConsultingType
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")
V = TypeVar("V")
T = TypeVar("T")
W = TypeVar("W", bound="QWidget")
M = TypeVar("M", bound="Module")
S = TypeVar("S", bound="PageState")
DEBUG: bool = True
DEBUG_SEARCH_WIDGET: bool = False
@@ -137,6 +147,60 @@ QSS = """
*[styleClass="stempel"]:focus {
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 ---"
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]: ...
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):
def __init__(
self,
@@ -292,104 +284,6 @@ class CustomWidget(QWidget):
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(
form_field: FormField[W],
widget: QWidget,
@@ -2581,25 +2475,43 @@ class NoScrollFilter(QObject):
return super().eventFilter(obj, event)
class MainPage_ClickableCell(QFrame):
"""cell in the table on the startup screen"""
clicked = Signal(types.MainPageEntry)
class HeaderCell(QLabel):
"""basic header for table like objects"""
def __init__(
self,
text: str,
font_size: int = 12,
):
super().__init__(text)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet(f"""
HeaderCell {{
background-color: #e2e8f0;
color: #475569;
font-weight: bold;
padding: 10px;
border-radius: 6px;
font-size: {font_size}px;
}}
""")
class ClickableCell(QFrame):
"""basic cell which can be clicked"""
def __init__(
self,
text: str,
data_record: types.MainPageEntry,
):
super().__init__()
self.data_record = data_record
self.setStyleSheet("""
MainPage_ClickableCell {
ClickableCell {
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
MainPage_ClickableCell:hover {
ClickableCell:hover {
background-color: #eff6ff;
border: 1px solid #60a5fa;
}
@@ -2610,29 +2522,41 @@ class MainPage_ClickableCell(QFrame):
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(label)
class CompanyProfile_Consultation_ClickableCell(ClickableCell):
"""cell on the company profile for consultations"""
clicked = Signal(CompanyProfileConsultationEntry)
def __init__(
self,
text: str,
data_record: CompanyProfileConsultationEntry,
):
super().__init__(text=text)
self.data_record = data_record
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit(self.data_record)
class MainPage_HeaderCell(QLabel):
class MainPage_ClickableCell(ClickableCell):
"""cell in the table on the startup screen"""
clicked = Signal(types.MainPageEntry)
def __init__(
self,
text: str,
font_size: int = 12,
data_record: types.MainPageEntry,
):
super().__init__(text)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet(f"""
MainPage_HeaderCell {{
background-color: #e2e8f0;
color: #475569;
font-weight: bold;
padding: 10px;
border-radius: 6px;
font-size: {font_size}px;
}}
""")
super().__init__(text=text)
self.data_record = data_record
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit(self.data_record)
class Page_NewInitRec(QWidget):
@@ -2867,6 +2791,9 @@ class Page_InitRecCompany(QWidget):
)
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(
session=self.STATE.session,
rec_id=self.STATE.un_id,
@@ -3148,6 +3075,25 @@ class Page_CompanyProfile(QWidget):
change_title_btn.clicked.connect(self.set_company_name)
container_layout.addLayout(layout_change_title)
btn_dummy_pauschal = QPushButton("Dummy Pauschalberatung hinzufügen")
btn_dummy_pauschal.clicked.connect(
lambda: self._debug_consulting_dummy(ConsultingType.PAUSCHAL)
)
container_layout.addWidget(btn_dummy_pauschal)
btn_dummy_inidividual = QPushButton("Dummy Individualberatung hinzufügen")
btn_dummy_inidividual.clicked.connect(
lambda: self._debug_consulting_dummy(ConsultingType.INDIVIDUAL)
)
container_layout.addWidget(btn_dummy_inidividual)
btn_clear_cons = QPushButton("Beratungen leeren")
btn_clear_cons.clicked.connect(self._clear_consultations)
container_layout.addWidget(btn_clear_cons)
btn_update_cons = QPushButton("Beratungen aktualisieren")
btn_update_cons.clicked.connect(self._update_consultations)
container_layout.addWidget(btn_update_cons)
separator2 = QFrame()
separator2.setFrameShape(QFrame.Shape.HLine)
separator2.setFrameShadow(QFrame.Shadow.Sunken)
@@ -3155,7 +3101,7 @@ class Page_CompanyProfile(QWidget):
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.addSpacing(20)
@@ -3171,7 +3117,7 @@ class Page_CompanyProfile(QWidget):
left_frame.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
left_layout = QVBoxLayout(left_frame)
main_hor_layout.addWidget(left_frame, stretch=1, alignment=Qt.AlignmentFlag.AlignTop)
master_data_label = MainPage_HeaderCell("Stammdaten & Vermerke")
master_data_label = HeaderCell("Stammdaten & Vermerke")
master_data_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
@@ -3193,21 +3139,23 @@ class Page_CompanyProfile(QWidget):
top_right_frame.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
)
top_right_layout = QVBoxLayout(top_right_frame)
common_consultancy_label = MainPage_HeaderCell("Pauschalberatung")
self.layout_cons_pauschal = QVBoxLayout(top_right_frame)
self.cons_pauschal: list[CompanyProfile_Consultation_ClickableCell] = []
common_consultancy_label = HeaderCell("Pauschalberatung")
common_consultancy_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
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.clicked.connect(
lambda: self._request_consulting(ConsultingType.PAUSCHAL)
lambda: self._request_new_consulting(ConsultingType.PAUSCHAL)
)
self.add_btn_pauschal.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
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_frame = QFrame()
@@ -3215,21 +3163,23 @@ class Page_CompanyProfile(QWidget):
bottom_right_frame.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
)
bottom_right_layout = QVBoxLayout(bottom_right_frame)
individual_consultancy_label = MainPage_HeaderCell("Individualberatung")
self.layout_cons_indiv = QVBoxLayout(bottom_right_frame)
self.cons_indiv: list[CompanyProfile_Consultation_ClickableCell] = []
individual_consultancy_label = HeaderCell("Individualberatung")
individual_consultancy_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
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.clicked.connect(
lambda: self._request_consulting(ConsultingType.INDIVIDUAL)
lambda: self._request_new_consulting(ConsultingType.INDIVIDUAL)
)
self.add_btn_individual.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
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(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:
req_state = Page_InitRecCompany_State(
session=self.STATE.session,
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)
def _request_consulting(
def _request_new_consulting(
self,
cons_type: ConsultingType,
) -> None:
@@ -3293,6 +3265,62 @@ class Page_CompanyProfile(QWidget):
)
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:
self.add_btn_pauschal.setEnabled(False)
self.add_btn_individual.setEnabled(False)
@@ -3797,13 +3825,18 @@ class Page_Consulting(QWidget):
self,
text: str,
button_text: str,
button_func: Callable[[], None] | None = None,
) -> None:
if button_text and button_func is None:
raise ValueError("Can not add button without calling function")
banner = InfoBanner(
text=text,
button_text=button_text,
)
if button_func is not None:
banner.btn_clicked.connect(button_func)
self.info_banners.append(banner)
banner.link_clicked.connect(self._open_linking_dialogue)
self.info_banners_layout.addWidget(banner)
def _sync_state_to_GUI(self) -> None:
@@ -3824,23 +3857,30 @@ class Page_Consulting(QWidget):
# info banners needed
self._clear_info_banners()
if self.STATE.un_id is None and self.STATE.pers_id is None:
# link missing
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",
)
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",
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
@@ -4124,6 +4164,7 @@ class Page_Consulting_ConsultingSession(QWidget):
layout,
widget_registry=self.widget_registry,
)
self.timestamp.set_pydatetime(datetime.datetime.now(TIMEZONE_CEST))
contact_def = FormField[QLineEdit](
"Kontaktperson",
@@ -4302,24 +4343,10 @@ class Page_Consulting_Table(QWidget):
# --- BUTTON (ADD) ---
self.btn_add = QPushButton("+ Neuen Eintrag hinzufügen")
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)
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_widget = QWidget()
header_layout = QHBoxLayout(header_widget)
@@ -4547,7 +4574,7 @@ class Page_Consulting_Table(QWidget):
class InfoBanner(QFrame):
link_clicked = Signal()
btn_clicked = Signal()
def __init__(
self,
@@ -4582,43 +4609,18 @@ class InfoBanner(QFrame):
layout.setContentsMargins(15, 10, 15, 10)
self.label = QLabel(text)
self.button = QPushButton(button_text)
layout.addWidget(self.label)
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(
layout: QLayout | None,
) -> None:
@@ -4782,7 +4784,7 @@ class MainWindow(QMainWindow):
"Datum",
]
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()
+20
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, TypeAlias, TypedDict
if TYPE_CHECKING:
import polars as pl
UserId: TypeAlias = int
RecId: TypeAlias = int
ConsId: TypeAlias = int
@@ -22,6 +23,11 @@ class InitRecType(enum.IntEnum):
PERSON = enum.auto()
class ConsultingType(enum.StrEnum):
PAUSCHAL = enum.auto()
INDIVIDUAL = enum.auto()
# // database interaction data structures
# ** externals
class CompanyInfo(TypedDict):
@@ -83,3 +89,17 @@ class MainPageEntry:
display_name: str
Metadaten_aktualisierung: datetime.datetime
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]