219 lines
8.4 KiB
Python
219 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
# hub/ticket/ui/dialogs/base_document_dialog.py
|
||
|
||
"""Базовый UI-диалог документов Ticket без файловой и доменной логики."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from gui.components import Button, Dialog, Label, TextInput
|
||
from gui.containers import HContainer, SContainer, VContainer
|
||
|
||
from domain import TicketTaskSnapshot, parse_location_parts
|
||
from ui.task_view_formatters import build_specialist_card_info
|
||
|
||
|
||
class BaseDocumentDialog(Dialog):
|
||
"""Базовый modal-диалог для ввода данных документа Ticket."""
|
||
|
||
def __init__(
|
||
self,
|
||
task: TicketTaskSnapshot,
|
||
title: str,
|
||
submit_text: str,
|
||
parent=None,
|
||
):
|
||
self._task = task
|
||
self._cancel_button: Button | None = None
|
||
self._submit_button: Button | None = None
|
||
self._form_text_edits: list[TextInput] = []
|
||
super().__init__(
|
||
title=title,
|
||
width=540,
|
||
height=740,
|
||
modal=True,
|
||
parent=parent,
|
||
)
|
||
self._setup_ui(submit_text)
|
||
self._connect_signals()
|
||
self._refresh_submit_state()
|
||
|
||
def _setup_ui(self, submit_text: str) -> None:
|
||
# Root-контейнер документа: собирает все основные зоны окна сверху вниз.
|
||
root = VContainer(margin=[24, 20, 24, 20], spacing=0)
|
||
self.add_widget(root)
|
||
root.add_widget(self._build_summary_shell())
|
||
root.add_widget(self._build_divider())
|
||
root.add_widget(self._build_form_shell())
|
||
root.add_widget(self._build_actions_section(submit_text))
|
||
|
||
def _build_summary_shell(self) -> SContainer:
|
||
# Summary-shell: верхняя область окна с базовой карточкой контекста документа.
|
||
summary_shell = SContainer(
|
||
height_percent=24,
|
||
orientation="v",
|
||
content_fit=False,
|
||
)
|
||
# Summary-content: вертикальный стек строк внутри summary-shell.
|
||
summary_content = VContainer(spacing=10, content_fit=True, parent=summary_shell)
|
||
for row in self._build_summary_rows():
|
||
summary_content.add_widget(row)
|
||
return summary_shell
|
||
|
||
def _build_summary_rows(self) -> list[HContainer]:
|
||
institution, room, device = parse_location_parts(self._task.location)
|
||
rows: list[HContainer] = []
|
||
for title, value in (
|
||
("Учреждение", institution or "Локация не указана"),
|
||
("Оборудование", device or "Аппарат не указан"),
|
||
("Кабинет", room or "Кабинет не указан"),
|
||
("Специалист", self._build_specialist_summary()),
|
||
):
|
||
rows.append(self._build_summary_row(title, value))
|
||
return rows
|
||
|
||
def _build_summary_row(self, title: str, value: str) -> HContainer:
|
||
# Summary-row: горизонтальная строка для пары "заголовок поля + значение".
|
||
row = HContainer(spacing=10, content_fit=True)
|
||
title_label = Label(
|
||
f"{title}:",
|
||
alignment="left",
|
||
style="TICKET_DETAILS_SUMMARY_TITLE",
|
||
)
|
||
value_label = Label(
|
||
value,
|
||
alignment="left",
|
||
style="TICKET_DETAILS_SUMMARY_VALUE",
|
||
)
|
||
value_label.set_tooltip(value)
|
||
row.add_widget(title_label)
|
||
row.add_widget_with_stretch(value_label, 1)
|
||
return row
|
||
|
||
def _build_divider(self) -> Label:
|
||
return Label(
|
||
"",
|
||
style="TICKET_DETAILS_DIVIDER",
|
||
height_percent=1,
|
||
)
|
||
|
||
def _build_form_shell(self) -> SContainer:
|
||
# Form-shell: центральная рабочая зона, куда наследник добавляет поля документа.
|
||
form_shell = SContainer(
|
||
height_percent=58,
|
||
orientation="v",
|
||
spacing=10,
|
||
content_fit=False,
|
||
)
|
||
self._build_form(form_shell)
|
||
return form_shell
|
||
|
||
def _build_actions_section(self, submit_text: str) -> HContainer:
|
||
# Actions-row: нижняя линия действий с фиксированной процентной сеткой.
|
||
actions = HContainer(
|
||
height_percent=11,
|
||
spacing=0,
|
||
content_fit=False,
|
||
)
|
||
# Левый spacer-контейнер: формирует стартовый отступ перед кнопкой отмены.
|
||
SContainer(
|
||
width_percent=10,
|
||
height_percent=100,
|
||
parent=actions,
|
||
)
|
||
self._cancel_button = Button(
|
||
"Отмена",
|
||
width_percent=26,
|
||
height_percent=100,
|
||
margin=0,
|
||
style="TICKET_DOCUMENT_CANCEL_BUTTON",
|
||
content_fit=False,
|
||
)
|
||
actions.add_widget(self._cancel_button)
|
||
# Центральный spacer-контейнер: удерживает зазор между двумя action-кнопками.
|
||
SContainer(
|
||
width_percent=4,
|
||
height_percent=100,
|
||
parent=actions,
|
||
)
|
||
self._submit_button = Button(
|
||
submit_text,
|
||
width_percent=50,
|
||
height_percent=100,
|
||
margin=0,
|
||
style="TICKET_DOCUMENT_SUBMIT_BUTTON",
|
||
content_fit=False,
|
||
)
|
||
actions.add_widget(self._submit_button)
|
||
# Правый spacer-контейнер: завершает строку действий симметричным отступом.
|
||
SContainer(
|
||
width_percent=10,
|
||
height_percent=100,
|
||
parent=actions,
|
||
)
|
||
self._submit_button.set_enabled(False)
|
||
return actions
|
||
|
||
def _connect_signals(self) -> None:
|
||
if self._cancel_button is not None:
|
||
self._cancel_button.clicked.connect(self.reject)
|
||
if self._submit_button is not None:
|
||
self._submit_button.clicked.connect(self.accept)
|
||
for text_edit in self._form_text_edits:
|
||
text_edit.text_changed.connect(self._refresh_submit_state)
|
||
self._connect_form_signals()
|
||
|
||
def build_payload(self) -> dict[str, str]:
|
||
"""Вернуть данные формы. Реализуется в наследниках."""
|
||
raise NotImplementedError
|
||
|
||
def _build_form(self, container: SContainer) -> None:
|
||
"""Построить специфическую часть формы."""
|
||
raise NotImplementedError
|
||
|
||
def _is_ready(self) -> bool:
|
||
"""Проверить, что форма готова к отправке."""
|
||
raise NotImplementedError
|
||
|
||
def _connect_form_signals(self) -> None:
|
||
"""Подключить сигналы элементов формы, если кроме текстовых полей есть другие элементы."""
|
||
return None
|
||
|
||
def _populate_text_block(
|
||
self,
|
||
field_shell: SContainer,
|
||
title: str,
|
||
placeholder: str,
|
||
) -> TextInput:
|
||
# Field-shell: ожидает внутри себя заголовок поля и текстовую область конкретной секции.
|
||
field_shell.add_widget(
|
||
Label(
|
||
title,
|
||
alignment="left",
|
||
style="TICKET_DETAILS_SECTION_TITLE",
|
||
)
|
||
)
|
||
text_edit = TextInput(
|
||
placeholder=placeholder,
|
||
style="TICKET_DOCUMENT_TEXTAREA",
|
||
multiline=True,
|
||
content_fit=False,
|
||
)
|
||
self._form_text_edits.append(text_edit)
|
||
field_shell.add_widget_with_stretch(text_edit, 1)
|
||
return text_edit
|
||
|
||
def _refresh_submit_state(self) -> None:
|
||
if self._submit_button is not None:
|
||
self._submit_button.set_enabled(self._is_ready())
|
||
|
||
def _build_specialist_summary(self) -> str:
|
||
specialist_name = self._task.assigned_specialist.strip()
|
||
if not specialist_name:
|
||
return "Не назначен"
|
||
specialist_info = build_specialist_card_info(specialist_name)
|
||
short_name = specialist_info["short_name"].strip() or specialist_name
|
||
position = specialist_info["position"].strip()
|
||
if not position:
|
||
return short_name
|
||
return f"{short_name}, {position}"
|