# -*- 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()