224 lines
8.4 KiB
Python
224 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
# hub/ticket/ui/dialogs/specialist_dialog.py
|
||
|
||
"""UI-диалог выбора специалиста Ticket."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Sequence
|
||
|
||
from PySide6.QtCore import Qt, Signal
|
||
from PySide6.QtGui import QMouseEvent
|
||
|
||
from gui.components import Button, Dialog, Label
|
||
from gui.containers import HContainer, ScrollContainer, SContainer, VContainer
|
||
|
||
from ui.cards.task_card_pixmap_factory import (
|
||
build_placeholder_avatar_pixmap,
|
||
load_avatar_pixmap,
|
||
)
|
||
from ui.task_view_formatters import (
|
||
build_specialist_card_info,
|
||
build_specialist_photo_path,
|
||
)
|
||
|
||
|
||
class _SpecialistRow(SContainer):
|
||
"""Строка выбора специалиста с фото, именем и должностью."""
|
||
|
||
clicked = Signal(str)
|
||
activated = Signal(str)
|
||
|
||
def __init__(self, specialist_name: str, parent=None):
|
||
super().__init__(
|
||
margin=0,
|
||
spacing=0,
|
||
content_fit=True,
|
||
parent=parent,
|
||
style="TICKET_SPECIALIST_ITEM",
|
||
active_style="TICKET_SPECIALIST_ITEM_SELECTED",
|
||
is_active=False,
|
||
)
|
||
self._specialist_name = specialist_name
|
||
self._info = build_specialist_card_info(specialist_name)
|
||
self._name_label: Label | None = None
|
||
self._role_label: Label | None = None
|
||
self._avatar_label: Label | None = None
|
||
self.setObjectName("ticket_specialist_row")
|
||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||
self.set_min_height(88)
|
||
self._setup_ui()
|
||
|
||
@property
|
||
def specialist_name(self) -> str:
|
||
return self._specialist_name
|
||
|
||
def set_selected(self, selected: bool) -> None:
|
||
self.style(is_active=selected)
|
||
|
||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self.clicked.emit(self._specialist_name)
|
||
super().mousePressEvent(event)
|
||
|
||
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self.clicked.emit(self._specialist_name)
|
||
self.activated.emit(self._specialist_name)
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
def _setup_ui(self) -> None:
|
||
# Body-row карточки специалиста: фото слева и текстовый блок справа.
|
||
body = HContainer(margin=[12, 10, 12, 10], spacing=16, content_fit=True, parent=self)
|
||
body.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||
|
||
self._avatar_label = Label("", style="TICKET_TASK_CARD_AVATAR_IMAGE")
|
||
self._avatar_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||
self._avatar_label.set_fixed_size(72, 72)
|
||
self._avatar_label.set_pixmap(self._build_avatar_pixmap())
|
||
|
||
self._name_label = Label(
|
||
self._specialist_name,
|
||
alignment="left",
|
||
style="TICKET_DETAILS_SECTION_TITLE",
|
||
)
|
||
self._name_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||
|
||
self._role_label = Label(
|
||
self._info.get("position", "").strip() or "Специалист",
|
||
alignment="left",
|
||
style="TICKET_SPECIALIST_ROLE",
|
||
)
|
||
self._role_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||
|
||
# Text-block специалиста: вертикальный контейнер имени и должности.
|
||
text_block = VContainer(spacing=2, content_fit=True)
|
||
text_block.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||
text_block.add_widget(self._name_label)
|
||
text_block.add_widget(self._role_label)
|
||
|
||
body.add_widget(self._avatar_label)
|
||
body.add_widget_with_stretch(text_block, 1)
|
||
|
||
def _build_avatar_pixmap(self):
|
||
photo_path = build_specialist_photo_path(
|
||
self._specialist_name,
|
||
self._info.get("photo", ""),
|
||
)
|
||
avatar = load_avatar_pixmap(photo_path, 72, 72, padding=2)
|
||
if avatar is None:
|
||
return build_placeholder_avatar_pixmap(72)
|
||
return avatar
|
||
|
||
|
||
class SpecialistDialog(Dialog):
|
||
"""Диалог выбора специалиста без application-логики."""
|
||
|
||
def __init__(
|
||
self,
|
||
specialists: Sequence[str],
|
||
parent=None,
|
||
):
|
||
self._specialists = [str(item).strip() for item in specialists if str(item).strip()]
|
||
self._selected_specialist = ""
|
||
self._rows: dict[str, _SpecialistRow] = {}
|
||
self._cancel_button: Button | None = None
|
||
self._submit_button: Button | None = None
|
||
super().__init__(
|
||
title="Выбор специалиста",
|
||
width=540,
|
||
height=740,
|
||
modal=True,
|
||
parent=parent,
|
||
)
|
||
self._setup_ui()
|
||
self._connect_signals()
|
||
self._refresh_submit_state()
|
||
|
||
@property
|
||
def selected_specialist(self) -> str:
|
||
return self._selected_specialist
|
||
|
||
def _setup_ui(self) -> None:
|
||
# Root-контейнер диалога: подсказка, список специалистов и action-строка.
|
||
main_container = VContainer(margin=[22, 20, 22, 20], spacing=16)
|
||
self.add_widget(main_container)
|
||
main_container.add_widget(
|
||
Label(
|
||
"Выберите специалиста для назначения на\nзадачу:",
|
||
alignment="left",
|
||
style="TICKET_SPECIALIST_HINT",
|
||
)
|
||
)
|
||
main_container.add_widget_with_stretch(self._build_list(), 1)
|
||
main_container.add_widget(self._build_actions())
|
||
|
||
def _build_list(self) -> ScrollContainer:
|
||
# Scroll-list специалистов: прокручиваемая область с вариантами назначения.
|
||
scroll = ScrollContainer(
|
||
spacing=12,
|
||
orientation="v",
|
||
content_margins=[0, 0, 0, 0],
|
||
vertical_scroll_bar_policy="as_needed",
|
||
horizontal_scroll_bar_policy="always_off",
|
||
style="SCROLL_CONTAINER",
|
||
)
|
||
for specialist_name in self._specialists:
|
||
row = _SpecialistRow(specialist_name)
|
||
self._rows[specialist_name] = row
|
||
scroll.add_widget(row)
|
||
return scroll
|
||
|
||
def _build_actions(self) -> HContainer:
|
||
# Actions-row диалога: выравнивает кнопки отмены и подтверждения выбора.
|
||
actions = HContainer(spacing=18, content_fit=True)
|
||
actions.add_stretch()
|
||
self._cancel_button = Button(
|
||
"Отмена",
|
||
style="TICKET_DOCUMENT_CANCEL_BUTTON",
|
||
content_fit=True,
|
||
)
|
||
self._cancel_button.set_min_width(160)
|
||
self._cancel_button.set_min_height(56)
|
||
self._submit_button = Button(
|
||
"Выбрать",
|
||
style="TICKET_DOCUMENT_SUBMIT_BUTTON",
|
||
content_fit=True,
|
||
)
|
||
self._submit_button.set_min_width(180)
|
||
self._submit_button.set_min_height(56)
|
||
actions.add_widget(self._cancel_button)
|
||
actions.add_widget(self._submit_button)
|
||
return actions
|
||
|
||
def _connect_signals(self) -> None:
|
||
for row in self._rows.values():
|
||
row.clicked.connect(self._on_row_clicked)
|
||
row.activated.connect(self._on_row_activated)
|
||
if self._submit_button is not None:
|
||
self._submit_button.clicked.connect(self._handle_accept)
|
||
if self._cancel_button is not None:
|
||
self._cancel_button.clicked.connect(self.reject)
|
||
|
||
def _refresh_submit_state(self) -> None:
|
||
if self._submit_button is not None:
|
||
self._submit_button.set_enabled(bool(self._selected_specialist))
|
||
|
||
def _set_selected_specialist(self, specialist_name: str) -> None:
|
||
self._selected_specialist = specialist_name if specialist_name in self._rows else ""
|
||
for row_name, row in self._rows.items():
|
||
row.set_selected(row_name == self._selected_specialist)
|
||
self._refresh_submit_state()
|
||
|
||
def _handle_accept(self) -> None:
|
||
if not self._selected_specialist:
|
||
return
|
||
self.accept()
|
||
|
||
def _on_row_clicked(self, specialist_name: str) -> None:
|
||
self._set_selected_specialist(specialist_name)
|
||
|
||
def _on_row_activated(self, specialist_name: str) -> None:
|
||
self._set_selected_specialist(specialist_name)
|
||
self._handle_accept()
|