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

210 lines
7.9 KiB
Python
Raw 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/cards/task_card_view.py
"""Ticket task card view."""
from __future__ import annotations
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtWidgets import QSizePolicy
from gui.components import Label
from gui.containers import SContainer, VContainer
from domain import TicketTaskSnapshot
from domain.ticket_constants import (
STATE_COMPLETED,
STATE_CONFIRMATION,
STATE_IN_PROGRESS,
STATE_REFUSED,
STATE_TODO,
)
from ui.task_view_formatters import (
build_specialist_card_text,
build_specialist_photo_path,
build_task_fault_title,
)
from .task_card_pixmap_factory import (
build_placeholder_avatar_pixmap,
compute_square_size,
load_avatar_pixmap,
)
class TaskCardView(SContainer):
"""Compact Ticket card without action logic."""
card_clicked = Signal(object)
card_height_changed = Signal()
_ROOT_STYLE_BY_STATE = {
STATE_TODO: "TICKET_TASK_CARD_ROOT_TODO",
STATE_IN_PROGRESS: "TICKET_TASK_CARD_ROOT_IN_PROGRESS",
STATE_CONFIRMATION: "TICKET_TASK_CARD_ROOT_CONFIRMATION",
STATE_COMPLETED: "TICKET_TASK_CARD_ROOT_COMPLETED",
STATE_REFUSED: "TICKET_TASK_CARD_ROOT_REFUSED",
}
_DARK_TEXT_STATES = {STATE_TODO, STATE_CONFIRMATION, STATE_REFUSED}
def __init__(
self,
task: TicketTaskSnapshot,
parent=None,
):
super().__init__(
width_percent=100,
margin=0,
content_fit=True,
parent=parent,
)
self._card_id = task.task_id
self._fault_title_label: Label | None = None
self._avatar_label: Label | None = None
self._current_task: TicketTaskSnapshot | None = None
self._height_sync_in_progress = False
self._pixmap_refresh_pending = False
self._setup_ui()
self.update_task(task)
@property
def card_id(self) -> object:
return self._card_id
def _setup_ui(self) -> None:
self.setObjectName("ticket_task_card")
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.set_size_policy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# Content-shell: горизонтальное разделение текстовой зоны и аватара.
content = SContainer(
width_percent=100,
height_percent=100,
orientation="h",
margin=8,
spacing=0,
style="TICKET_TASK_CARD_CONTENT",
parent=self,
)
# Text-column: левая вертикальная зона с единственным текстовым полем заголовка.
text_column = VContainer(
width_percent=70,
spacing=0,
parent=content,
)
# Единственное текстовое поле карточки: краткий заголовок неисправности.
self._fault_title_label = Label(
"",
alignment="left",
height_percent=100,
parent=text_column,
)
# Avatar-column: правая зона для фото или плейсхолдера специалиста.
avatar_column = VContainer(
width_percent=26,
spacing=0,
parent=content,
)
self._avatar_label = Label(
"",
alignment="center",
width_percent=100,
height_percent=100,
style="TICKET_TASK_CARD_AVATAR_IMAGE",
parent=avatar_column,
)
def update_task(self, task: TicketTaskSnapshot) -> None:
self._card_id = task.task_id
self._current_task = task
if self._fault_title_label is not None:
self._fault_title_label.set_text(build_task_fault_title(task))
self._fault_title_label.set_tooltip(task.location or "")
self._apply_state_styles(task)
self._update_avatar(task)
self._sync_card_height()
self._schedule_pixmap_refresh()
def _apply_state_styles(self, task: TicketTaskSnapshot) -> None:
style_suffix = "DARK" if task.state_code in self._DARK_TEXT_STATES else "LIGHT"
self.style(self._ROOT_STYLE_BY_STATE.get(task.state_code, "TICKET_TASK_CARD_ROOT_TODO"))
if self._fault_title_label is not None:
self._fault_title_label.style(f"TICKET_TASK_CARD_TITLE_{style_suffix}")
def _update_avatar(self, task: TicketTaskSnapshot) -> None:
if self._avatar_label is None:
return
avatar_path = build_specialist_photo_path(task.assigned_specialist, task.specialist_photo)
self._avatar_label.set_tooltip(task.assigned_specialist or "Специалист не назначен")
pixmap = load_avatar_pixmap(avatar_path, self._avatar_label.width(), self._avatar_label.height(), padding=4)
if pixmap is not None:
self._avatar_label.set_text("")
self._avatar_label.set_pixmap(pixmap)
return
self._avatar_label.set_text("")
placeholder_size = self._label_square_size(self._avatar_label)
if placeholder_size > 0:
self._avatar_label.set_pixmap(build_placeholder_avatar_pixmap(placeholder_size))
@staticmethod
def _label_square_size(label: Label, padding: int = 0) -> int:
return compute_square_size(label.width(), label.height(), padding)
def resizeEvent(self, event) -> None:
super().resizeEvent(event)
self._sync_card_height()
if self._current_task is not None:
self._update_avatar(self._current_task)
self._schedule_pixmap_refresh()
def _schedule_pixmap_refresh(self) -> None:
"""Запланировать отложенный рендер pixmap-контента.
Процентные размеры дочерних Label вычисляются через
QTimer.singleShot(0) в PercentSizedWidget, поэтому при первом
показе карточки (или при динамической вставке из COM-сигнала)
label.width()/height() ещё равны 0 в момент resizeEvent.
Отложенный вызов гарантирует, что к моменту рендера layout
дочерних виджетов уже завершён.
"""
if self._pixmap_refresh_pending:
return
self._pixmap_refresh_pending = True
QTimer.singleShot(0, self._deferred_pixmap_refresh)
def _deferred_pixmap_refresh(self) -> None:
self._pixmap_refresh_pending = False
if self._current_task is not None:
self._update_avatar(self._current_task)
def _sync_card_height(self) -> None:
if self._height_sync_in_progress or self.width() <= 0:
return
target_height = max(1, round(self.width() / 2.745))
if self.height() == target_height:
return
self._height_sync_in_progress = True
self.setFixedHeight(target_height)
self._height_sync_in_progress = False
self.card_height_changed.emit()
def _on_parent_rebuild_finished(self) -> None:
# Фаза 2 каскада percent-sized: родительская колонка завершила
# перестроение, ширина карточки уже стабильная. Только теперь
# пересчитываем собственную высоту по соотношению width/2.745
# и обновляем pixmap-контент.
super()._on_parent_rebuild_finished()
self._sync_card_height()
if self._current_task is not None:
self._schedule_pixmap_refresh()
def mousePressEvent(self, event) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self.card_clicked.emit(self._card_id)
super().mousePressEvent(event)