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