Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View 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()