Add Dispatch_V0.1.1
This commit is contained in:
9
Dispatch_V0.1.1/ui/details/__init__.py
Normal file
9
Dispatch_V0.1.1/ui/details/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/details/__init__.py
|
||||
|
||||
"""Экран деталей и действия Ticket."""
|
||||
|
||||
from .task_details_actions import TaskDetailsActions
|
||||
from .task_details_dialog import TaskDetailsDialog
|
||||
|
||||
__all__ = ["TaskDetailsActions", "TaskDetailsDialog"]
|
||||
BIN
Dispatch_V0.1.1/ui/details/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Dispatch_V0.1.1/ui/details/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
135
Dispatch_V0.1.1/ui/details/task_details_actions.py
Normal file
135
Dispatch_V0.1.1/ui/details/task_details_actions.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/details/task_details_actions.py
|
||||
|
||||
"""Отдельный модуль действий экрана деталей Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from application.ticket_application_api import TicketApplicationApi
|
||||
from domain import TicketDocumentSnapshot, TicketTaskSnapshot
|
||||
from ui.dialogs import (
|
||||
AcceptanceDialog,
|
||||
DiagnosticReportDialog,
|
||||
RepairReportDialog,
|
||||
SpecialistDialog,
|
||||
TaskRefusalDialog,
|
||||
)
|
||||
from ui.ticket_message_dialog import TicketMessageDialog
|
||||
|
||||
|
||||
class TaskDetailsActions:
|
||||
"""Обёртка над application-командами для details-экрана."""
|
||||
|
||||
def __init__(self, application: TicketApplicationApi):
|
||||
self._application = application
|
||||
|
||||
def assign_specialist(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
dialog = SpecialistDialog(
|
||||
specialists=self._application.list_specialists(),
|
||||
parent=parent,
|
||||
)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return None
|
||||
specialist_name = dialog.selected_specialist.strip()
|
||||
if not specialist_name:
|
||||
self._show_warning(parent, "Имя специалиста не указано.")
|
||||
return None
|
||||
snapshot = self._application.assign_specialist(task.task_id, specialist_name)
|
||||
if snapshot is None:
|
||||
self._show_warning(parent, "Не удалось назначить специалиста.")
|
||||
return snapshot
|
||||
|
||||
def sign_diagnostic(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketDocumentSnapshot | None:
|
||||
dialog = DiagnosticReportDialog(task, parent=parent)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return None
|
||||
try:
|
||||
return self._application.create_diagnostic_report(
|
||||
task.task_id,
|
||||
**dialog.build_payload(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
self._show_warning(parent, str(exc))
|
||||
return None
|
||||
|
||||
def sign_repair(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketDocumentSnapshot | None:
|
||||
dialog = RepairReportDialog(task, parent=parent)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return None
|
||||
try:
|
||||
return self._application.create_repair_report(
|
||||
task.task_id,
|
||||
**dialog.build_payload(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
self._show_warning(parent, str(exc))
|
||||
return None
|
||||
|
||||
def sign_acceptance(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketDocumentSnapshot | None:
|
||||
dialog = AcceptanceDialog(task, parent=parent)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return None
|
||||
try:
|
||||
return self._application.create_acceptance_report(
|
||||
task.task_id,
|
||||
**dialog.build_payload(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
self._show_warning(parent, str(exc))
|
||||
return None
|
||||
|
||||
def archive_task(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
answer = TicketMessageDialog.ask_confirmation(
|
||||
parent=parent,
|
||||
title="Архивация задачи",
|
||||
message=f"Переместить задачу #{task.sequence_number or task.task_id} в архив?",
|
||||
accept_text="В архив",
|
||||
reject_text="Отмена",
|
||||
)
|
||||
if not answer:
|
||||
return None
|
||||
snapshot = self._application.archive_task(task.task_id)
|
||||
if snapshot is None:
|
||||
self._show_warning(parent, "Не удалось переместить задачу в архив.")
|
||||
return snapshot
|
||||
|
||||
def refuse_task(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
dialog = TaskRefusalDialog(task, parent=parent)
|
||||
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||
return None
|
||||
refusal_reason = dialog.refusal_reason
|
||||
if not refusal_reason:
|
||||
self._show_warning(parent, "Причина отказа не указана.")
|
||||
return None
|
||||
snapshot = self._application.refuse_task(task.task_id, refusal_reason)
|
||||
if snapshot is None:
|
||||
self._show_warning(parent, "Не удалось перевести задачу в отказ.")
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def _show_warning(parent, text: str) -> None:
|
||||
TicketMessageDialog.show_warning(parent, "Ticket", text)
|
||||
279
Dispatch_V0.1.1/ui/details/task_details_dialog.py
Normal file
279
Dispatch_V0.1.1/ui/details/task_details_dialog.py
Normal 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()
|
||||
116
Dispatch_V0.1.1/ui/details/task_details_view_data.py
Normal file
116
Dispatch_V0.1.1/ui/details/task_details_view_data.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/details/task_details_view_data.py
|
||||
|
||||
"""Форматирование данных для нового dialog-экрана подробностей Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from domain import TicketTaskSnapshot, parse_location_parts
|
||||
from domain.ticket_constants import STATE_ARCHIVED, STATE_COMPLETED, STATE_REFUSED
|
||||
from ui.task_view_formatters import (
|
||||
build_specialist_card_info,
|
||||
build_specialist_photo_path,
|
||||
build_stage_icon_path,
|
||||
can_assign_specialist,
|
||||
can_sign_acceptance,
|
||||
can_sign_diagnostic,
|
||||
can_sign_repair,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TaskStageRowData:
|
||||
"""View-data одной строки этапа выполнения заявки."""
|
||||
|
||||
key: str
|
||||
text: str
|
||||
icon_path: str
|
||||
emphasized: bool
|
||||
clickable: bool
|
||||
|
||||
|
||||
def build_task_summary_rows(task: TicketTaskSnapshot) -> tuple[tuple[str, str], ...]:
|
||||
institution, room, device = parse_location_parts(task.location or "")
|
||||
return (
|
||||
("Учреждение", institution or "Локация не указана"),
|
||||
("Оборудование", device or "Аппарат не указан"),
|
||||
("Кабинет", room or "Кабинет не указан"),
|
||||
("Назначение", build_task_status_text(task)),
|
||||
)
|
||||
|
||||
|
||||
def build_task_status_text(task: TicketTaskSnapshot) -> str:
|
||||
specialist_info = build_specialist_card_info(task.assigned_specialist)
|
||||
short_name = specialist_info["short_name"].strip()
|
||||
if not short_name:
|
||||
return "Специалист не назначен"
|
||||
return short_name
|
||||
|
||||
|
||||
def build_employee_view_data(task: TicketTaskSnapshot) -> dict[str, str]:
|
||||
specialist_info = build_specialist_card_info(task.assigned_specialist)
|
||||
short_name = specialist_info["short_name"].strip()
|
||||
position = specialist_info["position"].strip()
|
||||
return {
|
||||
"name": short_name or "Специалист не назначен",
|
||||
"position": position if short_name else "Ожидает назначения",
|
||||
"photo_path": build_specialist_photo_path(
|
||||
task.assigned_specialist,
|
||||
task.specialist_photo,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_task_stage_rows(task: TicketTaskSnapshot) -> tuple[TaskStageRowData, ...]:
|
||||
return (
|
||||
TaskStageRowData(
|
||||
key="specialist",
|
||||
text="Специалист назначен" if task.assigned_specialist.strip() else "Назначить специалиста",
|
||||
icon_path=build_stage_icon_path("specialist", True),
|
||||
emphasized=bool(task.assigned_specialist.strip()),
|
||||
clickable=can_assign_specialist(task),
|
||||
),
|
||||
TaskStageRowData(
|
||||
key="diagnostic",
|
||||
text=(
|
||||
"Отчёт диагностики составлен"
|
||||
if task.diagnostic_report_signed
|
||||
else "Составить отчёт диагностики"
|
||||
),
|
||||
icon_path=build_stage_icon_path("diagnostic", True),
|
||||
emphasized=bool(task.diagnostic_report_signed),
|
||||
clickable=can_sign_diagnostic(task),
|
||||
),
|
||||
TaskStageRowData(
|
||||
key="repair",
|
||||
text=(
|
||||
"Отчёт по ремонту составлен"
|
||||
if task.repair_report_signed
|
||||
else "Составить отчёт по ремонту"
|
||||
),
|
||||
icon_path=build_stage_icon_path("repair", True),
|
||||
emphasized=bool(task.repair_report_signed),
|
||||
clickable=can_sign_repair(task),
|
||||
),
|
||||
TaskStageRowData(
|
||||
key="acceptance",
|
||||
text=(
|
||||
"Акт приёмки работ подписан"
|
||||
if task.acceptance_report_signed
|
||||
else "Составить акт приёмки работ"
|
||||
),
|
||||
icon_path=build_stage_icon_path("acceptance", True),
|
||||
emphasized=bool(task.acceptance_report_signed),
|
||||
clickable=can_sign_acceptance(task),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def can_refuse_task(task: TicketTaskSnapshot) -> bool:
|
||||
return task.state_code not in {STATE_COMPLETED, STATE_REFUSED, STATE_ARCHIVED}
|
||||
|
||||
|
||||
def can_archive_task(task: TicketTaskSnapshot) -> bool:
|
||||
return task.state_code in {STATE_COMPLETED, STATE_REFUSED}
|
||||
112
Dispatch_V0.1.1/ui/details/task_stage_action_row.py
Normal file
112
Dispatch_V0.1.1/ui/details/task_stage_action_row.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/details/task_stage_action_row.py
|
||||
|
||||
"""Clickable task stage row for Ticket details."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
|
||||
from gui.components import Label
|
||||
from gui.containers import HContainer
|
||||
from gui.theme_bus import theme_bus
|
||||
|
||||
from ui.cards.task_card_pixmap_factory import load_tinted_icon_pixmap
|
||||
|
||||
|
||||
class TaskStageActionRow(HContainer):
|
||||
"""Row with icon, text and optional click action."""
|
||||
|
||||
clicked = Signal(str)
|
||||
|
||||
def __init__(self, stage_key: str, parent=None):
|
||||
super().__init__(
|
||||
margin=[4, 2, 4, 2],
|
||||
spacing=12,
|
||||
content_fit=True,
|
||||
parent=parent,
|
||||
style="TICKET_DETAILS_STAGE_ROW",
|
||||
active_style="TICKET_DETAILS_STAGE_ROW_ACTIVE",
|
||||
is_active=False,
|
||||
)
|
||||
self._stage_key = stage_key
|
||||
self._icon_path = ""
|
||||
self._theme = "dark" if self.palette().window().color().lightness() < 128 else "light"
|
||||
self._is_clickable = False
|
||||
self._is_highlighted = False
|
||||
self._icon_label: Label | None = None
|
||||
self._text_label: Label | None = None
|
||||
self._setup_ui()
|
||||
theme_bus.theme_changed.connect(self.set_theme)
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Root-row этапа: держит иконку, текст этапа и свободное место для выравнивания строки.
|
||||
self._icon_label = Label("", style="TICKET_TASK_CARD_AVATAR_IMAGE")
|
||||
self._icon_label.set_fixed_size(22, 22)
|
||||
self._text_label = Label(
|
||||
"",
|
||||
alignment="left",
|
||||
style="TICKET_DETAILS_STAGE_TEXT_INACTIVE",
|
||||
active_style="TICKET_DETAILS_STAGE_TEXT_ACTIVE",
|
||||
is_active=False,
|
||||
)
|
||||
self.add_widget(self._icon_label)
|
||||
self.add_widget(self._text_label)
|
||||
self.add_stretch()
|
||||
|
||||
def configure(
|
||||
self,
|
||||
text: str,
|
||||
icon_path: str,
|
||||
emphasized: bool,
|
||||
clickable: bool,
|
||||
) -> None:
|
||||
self._icon_path = icon_path
|
||||
self._is_clickable = bool(clickable)
|
||||
self._is_highlighted = bool(emphasized) or self._is_clickable
|
||||
|
||||
if self._text_label is not None:
|
||||
self._text_label.set_text(text)
|
||||
self._text_label.style(is_active=self._is_highlighted)
|
||||
|
||||
self.style(is_active=self._is_clickable)
|
||||
self.setCursor(
|
||||
Qt.CursorShape.PointingHandCursor
|
||||
if self._is_clickable
|
||||
else Qt.CursorShape.ArrowCursor
|
||||
)
|
||||
self._refresh_icon()
|
||||
|
||||
@Slot(str)
|
||||
def set_theme(self, theme: str) -> None:
|
||||
normalized_theme = (theme or "").strip().lower()
|
||||
if normalized_theme in {"dark", "light"}:
|
||||
self._theme = normalized_theme
|
||||
super().set_theme(theme)
|
||||
self._refresh_icon()
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||
if self._is_clickable and event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit(self._stage_key)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def _refresh_icon(self) -> None:
|
||||
if self._icon_label is None or not self._icon_path:
|
||||
return
|
||||
icon_width = self._icon_label.width() or 22
|
||||
icon_height = self._icon_label.height() or 22
|
||||
pixmap = load_tinted_icon_pixmap(
|
||||
self._icon_path,
|
||||
icon_width,
|
||||
icon_height,
|
||||
self._resolve_icon_color(),
|
||||
padding=2,
|
||||
)
|
||||
if pixmap is not None:
|
||||
self._icon_label.set_pixmap(pixmap)
|
||||
|
||||
def _resolve_icon_color(self) -> str:
|
||||
if self._is_highlighted:
|
||||
return "#F5F7FA" if self._theme == "dark" else "#172B4D"
|
||||
return "#8C9BAB" if self._theme == "dark" else "#6B778C"
|
||||
Reference in New Issue
Block a user