Add Dispatch_V0.1.1
This commit is contained in:
335
Dispatch_V0.1.1/ui/pages/acts_page.py
Normal file
335
Dispatch_V0.1.1/ui/pages/acts_page.py
Normal file
@@ -0,0 +1,335 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user