336 lines
12 KiB
Python
336 lines
12 KiB
Python
# -*- 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()
|