Add Dispatch_V0.1.1
This commit is contained in:
218
Dispatch_V0.1.1/ui/dialogs/base_document_dialog.py
Normal file
218
Dispatch_V0.1.1/ui/dialogs/base_document_dialog.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# -*- 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}"
|
||||
Reference in New Issue
Block a user