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