# -*- 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}"