Files
Dispatch/Dispatch_V0.1.1/ui/details/task_details_dialog.py
2026-04-29 08:18:54 +04:00

280 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()