Add Dispatch_V0.1.1
This commit is contained in:
237
Dispatch_V0.1.1/ui/pages/archive_page.py
Normal file
237
Dispatch_V0.1.1/ui/pages/archive_page.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/archive_page.py
|
||||
|
||||
"""Самостоятельная страница архива Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
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 ArchiveRecordSnapshot, TicketDocumentSnapshot
|
||||
from ui.cards import TaskCardView
|
||||
from .archive_view_helpers import (
|
||||
build_preview_lines,
|
||||
cycle_token_from_record,
|
||||
record_to_task_snapshot,
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Страница архива
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class ArchivePage(SContainer):
|
||||
"""Страница архива с карточками в стиле доски задач и панелью просмотра."""
|
||||
|
||||
def __init__(self, application: TaskApplicationService, parent=None):
|
||||
super().__init__(width_percent=100, height_percent=100, parent=parent)
|
||||
self._application = application
|
||||
self._records: dict[str, ArchiveRecordSnapshot] = {}
|
||||
self._selected_record_key: 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, TaskCardView] = {}
|
||||
self._card_key_by_task_id: dict[int, str] = {}
|
||||
self._setup_ui()
|
||||
self._connect_signals()
|
||||
self._reload_records()
|
||||
|
||||
# -- 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)
|
||||
self._preview._input.setVerticalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff,
|
||||
)
|
||||
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_records)
|
||||
self._application.task_removed.connect(self._reload_records)
|
||||
self._application.state_loaded.connect(self._reload_records)
|
||||
|
||||
# -- Data refresh ------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _record_key(record: ArchiveRecordSnapshot) -> str:
|
||||
token = cycle_token_from_record(record)
|
||||
return f"{record.task_id}_{token}" if token else str(record.task_id)
|
||||
|
||||
def _reload_records(self, *_args) -> None:
|
||||
records = self._application.list_archive_records()
|
||||
self._records = {self._record_key(r): r for r in records}
|
||||
self._rebuild_cards(records)
|
||||
|
||||
if self._selected_record_key in self._records:
|
||||
self._select_record(self._selected_record_key, update_preview=True)
|
||||
return
|
||||
|
||||
self._selected_record_key = None
|
||||
self._clear_preview()
|
||||
|
||||
def _rebuild_cards(self, records: list[ArchiveRecordSnapshot]) -> 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()
|
||||
self._card_key_by_task_id.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 records:
|
||||
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, record in enumerate(records):
|
||||
key = self._record_key(record)
|
||||
synthetic_task = record_to_task_snapshot(record)
|
||||
card = TaskCardView(synthetic_task)
|
||||
card.card_clicked.connect(self._on_card_clicked)
|
||||
self._cards[key] = card
|
||||
self._card_key_by_task_id[record.task_id] = key
|
||||
self._card_host.insert_widget(index, card)
|
||||
|
||||
# -- Selection ---------------------------------------------------------
|
||||
|
||||
def _select_record(self, key: str | None, update_preview: bool) -> None:
|
||||
normalized = key if key in self._records else None
|
||||
self._selected_record_key = normalized
|
||||
if not update_preview:
|
||||
return
|
||||
record = self._records.get(normalized) if normalized is not None else None
|
||||
if record is None:
|
||||
self._clear_preview()
|
||||
return
|
||||
self._render_record(record)
|
||||
|
||||
def _render_record(self, record: ArchiveRecordSnapshot) -> None:
|
||||
if self._preview is None:
|
||||
return
|
||||
documents = self._load_cycle_documents(record)
|
||||
lines = build_preview_lines(record, documents)
|
||||
self._preview.set_text("\n".join(lines))
|
||||
|
||||
def _load_cycle_documents(
|
||||
self, record: ArchiveRecordSnapshot,
|
||||
) -> list[TicketDocumentSnapshot]:
|
||||
"""Загрузить только документы текущего цикла задачи."""
|
||||
cycle_token = cycle_token_from_record(record)
|
||||
all_docs = self._application.list_documents()
|
||||
return [
|
||||
d for d in all_docs
|
||||
if d.task_id == record.task_id and cycle_token in d.document_id
|
||||
]
|
||||
|
||||
def _clear_preview(self) -> None:
|
||||
if self._preview is not None:
|
||||
self._preview.set_text("")
|
||||
|
||||
def _on_card_clicked(self, card_id: object) -> None:
|
||||
if isinstance(card_id, int):
|
||||
key = self._card_key_by_task_id.get(card_id)
|
||||
if key is not None:
|
||||
self._select_record(key, update_preview=True)
|
||||
|
||||
def showEvent(self, event) -> None:
|
||||
super().showEvent(event)
|
||||
self._reload_records()
|
||||
Reference in New Issue
Block a user