Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View 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}"