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,279 @@
# -*- coding: utf-8 -*-
# hub/ticket/ui/details/task_details_dialog.py
"""Dialog-экран подробностей задачи Ticket в ecco-геометрии."""
from __future__ import annotations
from gui.components import Button, Dialog, Label
from gui.containers import HContainer, VContainer
from application.ticket_application_api import TicketApplicationApi
from domain import TicketTaskSnapshot
from ui.cards.task_card_pixmap_factory import (
build_placeholder_avatar_pixmap,
load_avatar_pixmap,
)
from .task_details_actions import TaskDetailsActions
from .task_details_view_data import (
build_employee_view_data,
build_task_stage_rows,
build_task_summary_rows,
can_archive_task,
can_refuse_task,
)
from .task_stage_action_row import TaskStageActionRow
class TaskDetailsDialog(Dialog):
"""Диалог подробностей задачи и доступных действий по этапам."""
_SUMMARY_TITLES = (
"Учреждение",
"Оборудование",
"Кабинет",
"Назначение",
)
_STAGE_ORDER = (
"specialist",
"diagnostic",
"repair",
"acceptance",
)
def __init__(
self,
application: TicketApplicationApi,
task_id: int,
parent=None,
):
self._application = application
self._task_id = task_id
self._task: TicketTaskSnapshot | None = None
self._actions = TaskDetailsActions(application)
self._summary_value_labels: dict[str, Label] = {}
self._stage_rows: dict[str, TaskStageActionRow] = {}
self._employee_avatar_label: Label | None = None
self._employee_name_label: Label | None = None
self._employee_role_label: Label | None = None
self._refuse_button: Button | None = None
super().__init__(
title="Подробности",
width=360,
height=600,
modal=True,
parent=parent,
)
self._setup_ui()
self._connect_signals()
self._reload_task()
def _setup_ui(self) -> None:
# Root-контейнер окна: раскладывает summary, этапы, сотрудника и footer по вертикали.
root = VContainer(margin=[24, 20, 24, 20], spacing=20)
self.add_widget(root)
root.add_widget(self._build_summary_section())
root.add_widget(Label("", style="TICKET_DETAILS_DIVIDER"))
root.add_widget(self._build_stages_section())
root.add_widget(self._build_employee_section())
root.add_stretch()
root.add_widget(self._build_footer_section())
def _build_summary_section(self) -> VContainer:
# Summary-section: верхняя сводка по задаче с основными идентификационными полями.
summary = VContainer(spacing=12, content_fit=True)
for title in self._SUMMARY_TITLES:
summary.add_widget(self._build_summary_row(title))
return summary
def _build_summary_row(self, title: 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(
"",
alignment="left",
style="TICKET_DETAILS_SUMMARY_VALUE",
)
self._summary_value_labels[title] = value_label
row.add_widget(title_label)
row.add_widget_with_stretch(value_label, 1)
return row
def _build_stages_section(self) -> VContainer:
# Stages-section: блок со списком шагов выполнения и доступных действий по ним.
section = VContainer(spacing=12, content_fit=True)
section.add_widget(
Label(
"Этапы выполнения заявки",
alignment="left",
style="TICKET_DETAILS_SECTION_TITLE",
)
)
# Stage-list: стек интерактивных строк этапов внутри секции stages.
stage_list = VContainer(spacing=12, content_fit=True)
for stage_key in self._STAGE_ORDER:
stage_row = TaskStageActionRow(stage_key)
stage_row.clicked.connect(self._on_stage_clicked)
self._stage_rows[stage_key] = stage_row
stage_list.add_widget(stage_row)
section.add_widget(stage_list)
return section
def _build_employee_section(self) -> VContainer:
# Employee-section: отдельный блок ответственного сотрудника и его служебной информации.
section = VContainer(spacing=12, content_fit=True)
section.add_widget(
Label(
"Ответственный сотрудник",
alignment="left",
style="TICKET_DETAILS_SECTION_TITLE",
)
)
# Employee-row: горизонтальная строка карточки сотрудника с аватаром и текстом.
employee_row = HContainer(spacing=16, content_fit=True)
self._employee_avatar_label = Label("", style="TICKET_TASK_CARD_AVATAR_IMAGE")
self._employee_avatar_label.set_fixed_size(64, 64)
self._employee_name_label = Label(
"",
alignment="left",
style="TICKET_DETAILS_EMPLOYEE_NAME",
)
self._employee_role_label = Label(
"",
alignment="left",
style="TICKET_DETAILS_EMPLOYEE_ROLE",
)
# Info-column: вертикальный столбец имени и должности рядом с аватаром.
info_column = VContainer(spacing=2, content_fit=True)
info_column.add_widget(self._employee_name_label)
info_column.add_widget(self._employee_role_label)
employee_row.add_widget(self._employee_avatar_label)
employee_row.add_widget_with_stretch(info_column, 1)
section.add_widget(employee_row)
return section
def _build_footer_section(self) -> HContainer:
# Footer-row: нижняя линия действий с центрированными кнопками.
footer = HContainer(spacing=12, content_fit=True)
self._refuse_button = Button(
"Отказать в обслуживании",
style="TICKET_DETAILS_REFUSE_BUTTON",
content_fit=True,
)
self._archive_button = Button(
"В архив",
style="TICKET_DETAILS_REFUSE_BUTTON",
content_fit=True,
)
footer.add_stretch()
footer.add_widget(self._refuse_button)
footer.add_widget(self._archive_button)
footer.add_stretch()
return footer
def _connect_signals(self) -> None:
self._application.task_updated.connect(self._on_task_updated)
self._application.task_removed.connect(self._on_task_removed)
if self._refuse_button is not None:
self._refuse_button.clicked.connect(self._on_refuse_clicked)
if self._archive_button is not None:
self._archive_button.clicked.connect(self._on_archive_clicked)
def _reload_task(self) -> None:
task = self._application.get_task(self._task_id)
if task is None:
self.reject()
return
self._task = task
self._update_view(task)
def _update_view(self, task: TicketTaskSnapshot) -> None:
for title, value in build_task_summary_rows(task):
label = self._summary_value_labels.get(title)
if label is not None:
label.set_text(value)
label.set_tooltip(value)
for stage_view in build_task_stage_rows(task):
stage_row = self._stage_rows.get(stage_view.key)
if stage_row is not None:
stage_row.configure(
text=stage_view.text,
icon_path=stage_view.icon_path,
emphasized=stage_view.emphasized,
clickable=stage_view.clickable,
)
self._update_employee_section(task)
if self._refuse_button is not None:
self._refuse_button.set_visible(can_refuse_task(task))
if self._archive_button is not None:
self._archive_button.set_visible(can_archive_task(task))
def _update_employee_section(self, task: TicketTaskSnapshot) -> None:
employee_data = build_employee_view_data(task)
if self._employee_name_label is not None:
self._employee_name_label.set_text(employee_data["name"])
if self._employee_role_label is not None:
role_text = employee_data["position"].strip()
self._employee_role_label.set_text(role_text)
self._employee_role_label.set_visible(bool(role_text))
if self._employee_avatar_label is None:
return
avatar_pixmap = load_avatar_pixmap(
employee_data["photo_path"],
64,
64,
padding=2,
)
if avatar_pixmap is None:
avatar_pixmap = build_placeholder_avatar_pixmap(64)
self._employee_avatar_label.set_pixmap(avatar_pixmap)
def _on_task_updated(self, task: TicketTaskSnapshot) -> None:
if task.task_id != self._task_id:
return
self._task = task
self._update_view(task)
def _on_task_removed(self, task_id: int) -> None:
if task_id == self._task_id:
self.reject()
def _on_stage_clicked(self, stage_key: str) -> None:
if self._task is None:
return
if stage_key == "specialist":
self._actions.assign_specialist(self._task, self)
return
if stage_key == "diagnostic":
self._actions.sign_diagnostic(self._task, self)
return
if stage_key == "repair":
self._actions.sign_repair(self._task, self)
return
if stage_key == "acceptance":
self._actions.sign_acceptance(self._task, self)
def _on_refuse_clicked(self) -> None:
if self._task is None:
return
snapshot = self._actions.refuse_task(self._task, self)
if snapshot is not None:
self.accept()
def _on_archive_clicked(self) -> None:
if self._task is None:
return
snapshot = self._actions.archive_task(self._task, self)
if snapshot is not None:
self.accept()