generated from dopt-python/py311
Dev #7
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user