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
+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()