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

341 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# hub/ticket/ui/pages/reports_page.py
"""Самостоятельная страница отчётов Ticket."""
from __future__ import annotations
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QSizePolicy
from gui.components import Label, TextInput
from gui.containers import HContainer, SContainer, ScrollContainer, VContainer
from application import TaskApplicationService
from domain import TicketDocumentSnapshot
# -------------------------------------------------------------------------
# Карточка отчёта (по образцу TaskCardView)
# -------------------------------------------------------------------------
class _ReportCardView(SContainer):
"""Карточка отчёта: заголовок, дата+учреждение, аппарат+кабинет."""
card_clicked = Signal(str)
def __init__(self, document: TicketDocumentSnapshot, parent=None):
super().__init__(
width_percent=100,
margin=0,
content_fit=True,
parent=parent,
)
self._document_id = document.document_id
self._title_label: Label | None = None
self._subtitle_label: Label | None = None
self._meta_label: Label | None = None
self._selected = False
self._height_sync_in_progress = False
self.setObjectName("ticket_report_card")
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.set_size_policy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._setup_ui()
self._fill(document)
@property
def document_id(self) -> str:
return self._document_id
def set_selected(self, selected: bool) -> None:
self._selected = selected
self._apply_root_style()
def _setup_ui(self) -> None:
content = SContainer(
width_percent=100,
height_percent=100,
margin=8,
spacing=0,
style="TICKET_REPORT_CARD_CONTENT",
parent=self,
)
text_column = VContainer(
width_percent=100,
spacing=2,
parent=content,
)
self._title_label = Label("", alignment="left", parent=text_column)
self._subtitle_label = Label("", alignment="left", parent=text_column)
self._meta_label = Label("", alignment="left", parent=text_column)
def _fill(self, document: TicketDocumentSnapshot) -> None:
title = document.title or "Отчёт"
created_at = document.created_at.strftime("%d.%m.%Y %H:%M")
facility = document.payload.get("facility") or document.location or ""
# Убрать название аппарата в скобках (дублирует 3-ю строку)
if "(" in facility:
facility = facility[:facility.index("(")].strip()
device = document.payload.get("device") or ""
cabinet = document.payload.get("cabinet") or ""
subtitle_parts = [p for p in (created_at, facility) if p]
meta_parts = [p for p in (device, cabinet) if p]
if self._title_label is not None:
self._title_label.set_text(title)
if self._subtitle_label is not None:
self._subtitle_label.set_text(" , ".join(subtitle_parts))
if self._meta_label is not None:
self._meta_label.set_text(" - ".join(meta_parts))
self._apply_root_style()
self._apply_text_styles()
def _apply_root_style(self) -> None:
if self._selected:
self.style("TICKET_REPORT_CARD_ROOT_SELECTED")
else:
self.style("TICKET_REPORT_CARD_ROOT")
def _apply_text_styles(self) -> None:
if self._title_label is not None:
self._title_label.style("TICKET_REPORT_CARD_TITLE")
if self._subtitle_label is not None:
self._subtitle_label.style("TICKET_REPORT_CARD_SUBTITLE")
if self._meta_label is not None:
self._meta_label.style("TICKET_REPORT_CARD_META")
def mousePressEvent(self, event) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self.card_clicked.emit(self._document_id)
super().mousePressEvent(event)
def resizeEvent(self, event) -> None:
super().resizeEvent(event)
self._sync_card_height()
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
# -------------------------------------------------------------------------
# Страница отчётов
# -------------------------------------------------------------------------
class ReportsPage(SContainer):
"""Страница отчётов, полностью локализованная внутри собственного класса."""
def __init__(self, application: TaskApplicationService, parent=None):
super().__init__(width_percent=100, height_percent=100, parent=parent)
self._application = application
self._documents: dict[str, TicketDocumentSnapshot] = {}
self._selected_document_id: str | None = None
self._card_host: VContainer | None = None
self._preview: TextInput | None = None
self._empty_list_label: Label | None = None
self._cards: dict[str, _ReportCardView] = {}
self._setup_ui()
self._connect_signals()
self._apply_initial_state()
self._reload_documents()
# -- UI ----------------------------------------------------------------
def _setup_ui(self) -> None:
board_row = HContainer(
margin=[0, 0, 0, 0],
height_percent=100,
spacing=16,
style="TICKET_SURFACE_HOST",
parent=self,
)
board_row.add_widget(self._build_list_column())
board_row.add_widget(self._build_preview_column())
def _build_list_column(self) -> SContainer:
column = SContainer(spacing=12, parent=None)
# Header (как в _TicketBoardColumn, но без счётчика)
header = HContainer(
height_percent=5.37,
margin=0,
spacing=16,
content_fit=False,
style="TICKET_BOARD_COLUMN_HEADER",
)
header.add_widget(
Label("Список документов", margin=[12, 0, 0, 0], style="TICKET_BOARD_COLUMN_TITLE"),
)
header.add_stretch()
# Body
body = SContainer(
margin=0,
style="TICKET_REPORT_COLUMN_BODY",
)
scroll = ScrollContainer(
margin=0,
spacing=0,
orientation="v",
vertical_scroll_bar_policy="always_off",
horizontal_scroll_bar_policy="always_off",
style="SCROLL_CONTAINER",
parent=body,
)
scroll.scroll_area.verticalScrollBar().setSingleStep(48)
self._card_host = VContainer(
spacing=12,
content_fit=False,
parent=scroll,
)
self._card_host.set_size_policy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Fixed,
)
column.add_widget(header)
column.add_widget(body)
return column
def _build_preview_column(self) -> SContainer:
column = SContainer(spacing=12, width_percent=79.83, parent=None)
# Header
header = HContainer(
height_percent=5.37,
margin=0,
spacing=16,
content_fit=False,
style="TICKET_BOARD_COLUMN_HEADER",
)
header.add_widget(
Label("Просмотр", margin=[12, 0, 0, 0], style="TICKET_BOARD_COLUMN_TITLE"),
)
header.add_stretch()
# Body — preview
body = SContainer(
margin=0,
style="TICKET_REPORT_PREVIEW_BODY",
)
preview_inner = VContainer(margin=0, spacing=0, parent=body)
self._preview = TextInput(style="TICKET_REPORT_PREVIEW_AREA", multiline=True)
self._preview.set_read_only(True)
preview_inner.add_widget_with_stretch(self._preview, 1)
column.add_widget(header)
column.add_widget(body)
return column
# -- Signals -----------------------------------------------------------
def _connect_signals(self) -> None:
self._application.task_updated.connect(self._reload_documents)
self._application.state_loaded.connect(self._reload_documents)
def _apply_initial_state(self) -> None:
self._clear_preview()
# -- Data refresh ------------------------------------------------------
def _reload_documents(self, *_args) -> None:
ordered_documents = self._application.list_documents("report")
self._documents = {
document.document_id: document
for document in ordered_documents
}
self._rebuild_cards(ordered_documents)
if self._selected_document_id in self._documents:
self._select_document(self._selected_document_id, update_preview=True)
return
self._selected_document_id = None
self._update_card_selection()
self._clear_preview()
def _rebuild_cards(self, documents: list[TicketDocumentSnapshot]) -> None:
if self._card_host is None:
return
for card in list(self._cards.values()):
self._card_host.remove_widget(card)
card.setParent(None)
self._cards.clear()
if self._empty_list_label is not None:
self._card_host.remove_widget(self._empty_list_label)
self._empty_list_label.setParent(None)
self._empty_list_label = None
if not documents:
self._empty_list_label = Label(
"Подписанные отчёты пока не созданы.",
alignment="left",
style="TICKET_REPORT_EMPTY_LABEL",
)
self._card_host.insert_widget(0, self._empty_list_label)
return
for index, document in enumerate(documents):
card = _ReportCardView(document)
card.card_clicked.connect(self._on_card_clicked)
self._cards[document.document_id] = card
self._card_host.insert_widget(index, card)
self._update_card_selection()
# -- Selection ---------------------------------------------------------
def _select_document(self, document_id: str | None, update_preview: bool) -> None:
normalized = document_id if document_id in self._documents else None
self._selected_document_id = normalized
self._update_card_selection()
if not update_preview:
return
document = self._current_document()
if document is None:
self._clear_preview()
return
self._render_document(document)
def _update_card_selection(self) -> None:
for doc_id, card in self._cards.items():
card.set_selected(doc_id == self._selected_document_id)
def _current_document(self) -> TicketDocumentSnapshot | None:
if self._selected_document_id is None:
return None
return self._documents.get(self._selected_document_id)
def _render_document(self, document: TicketDocumentSnapshot) -> None:
if self._preview is None:
return
self._preview.set_text(document.content or document.summary or document.title)
def _clear_preview(self) -> None:
if self._preview is not None:
self._preview.set_text("")
def _on_card_clicked(self, document_id: str) -> None:
self._select_document(document_id, update_preview=True)
def showEvent(self, event) -> None:
super().showEvent(event)
self._reload_documents()