# -*- coding: utf-8 -*- # hub/ticket/ui/pages/acts_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 # ------------------------------------------------------------------------- # Карточка акта (по образцу _ReportCardView) # ------------------------------------------------------------------------- class _ActCardView(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 "" 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 ActsPage(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, _ActCardView] = {} 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 = 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 = 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 = 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 = 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("acceptance") 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 = _ActCardView(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()