Add Dispatch_V0.1.1
This commit is contained in:
16
Dispatch_V0.1.1/ui/pages/__init__.py
Normal file
16
Dispatch_V0.1.1/ui/pages/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/__init__.py
|
||||
|
||||
"""Страницы Ticket."""
|
||||
|
||||
from .acts_page import ActsPage
|
||||
from .archive_page import ArchivePage
|
||||
from .reports_page import ReportsPage
|
||||
from .report_viewer import ReportViewer
|
||||
|
||||
__all__ = [
|
||||
"ActsPage",
|
||||
"ArchivePage",
|
||||
"ReportsPage",
|
||||
"ReportViewer",
|
||||
]
|
||||
BIN
Dispatch_V0.1.1/ui/pages/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Dispatch_V0.1.1/ui/pages/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
Dispatch_V0.1.1/ui/pages/__pycache__/acts_page.cpython-313.pyc
Normal file
BIN
Dispatch_V0.1.1/ui/pages/__pycache__/acts_page.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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()
|
||||
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()
|
||||
129
Dispatch_V0.1.1/ui/pages/archive_view_helpers.py
Normal file
129
Dispatch_V0.1.1/ui/pages/archive_view_helpers.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/archive_view_helpers.py
|
||||
|
||||
"""Вспомогательные функции построения представления архива."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain import ArchiveRecordSnapshot, TicketDocumentSnapshot, TicketTaskSnapshot
|
||||
from domain.ticket_constants import STATE_REFUSED, TICKET_STATE_COLORS, TICKET_STATE_NAMES
|
||||
from ui.task_view_formatters import format_datetime, split_task_location
|
||||
|
||||
|
||||
def record_to_task_snapshot(record: ArchiveRecordSnapshot) -> TicketTaskSnapshot:
|
||||
"""Построить синтетический TicketTaskSnapshot для отрисовки карточки."""
|
||||
state_code = record.pre_archive_state_code
|
||||
return TicketTaskSnapshot(
|
||||
task_id=record.task_id,
|
||||
location=record.location,
|
||||
state_code=state_code,
|
||||
state_name=TICKET_STATE_NAMES.get(state_code, "Архив"),
|
||||
action_text=record.action_text,
|
||||
color_hex=TICKET_STATE_COLORS.get(state_code, record.color_hex),
|
||||
created_at=record.created_at,
|
||||
completed_at=record.completed_at,
|
||||
refused_from_state=record.refused_from_state,
|
||||
refusal_reason=record.refusal_reason,
|
||||
assigned_specialist=record.assigned_specialist,
|
||||
specialist_photo=record.specialist_photo,
|
||||
diagnostic_report_signed=record.diagnostic_report_signed,
|
||||
repair_report_signed=record.repair_report_signed,
|
||||
acceptance_report_signed=record.acceptance_report_signed,
|
||||
sequence_number=record.sequence_number,
|
||||
)
|
||||
|
||||
|
||||
def cycle_token_from_record(record: ArchiveRecordSnapshot) -> str:
|
||||
"""Получить cycle_token из created_at архивной записи."""
|
||||
if record.created_at is not None:
|
||||
return record.created_at.strftime("%Y%m%d_%H%M%S")
|
||||
return ""
|
||||
|
||||
|
||||
def build_preview_lines(
|
||||
record: ArchiveRecordSnapshot,
|
||||
documents: list[TicketDocumentSnapshot],
|
||||
) -> list[str]:
|
||||
"""Собрать текст предпросмотра архивной записи."""
|
||||
institution, device, room = split_task_location(record.location)
|
||||
is_refused = record.pre_archive_state_code == STATE_REFUSED
|
||||
|
||||
lines: list[str] = []
|
||||
seq = record.sequence_number or record.task_id
|
||||
lines.append(f"Задача #{seq}")
|
||||
lines.append(f"Статус: {record.pre_archive_state_name}")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"Учреждение: {institution}")
|
||||
lines.append(f"Оборудование: {device}")
|
||||
lines.append(f"Кабинет: {room}")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"Создана: {format_datetime(record.created_at)}")
|
||||
if record.completed_at is not None:
|
||||
label = "Отказано" if is_refused else "Завершена"
|
||||
lines.append(f"{label}: {format_datetime(record.completed_at)}")
|
||||
lines.append(f"Архивирована: {format_datetime(record.archived_at)}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("─── Ход работ ───")
|
||||
lines.append("")
|
||||
|
||||
specialist = record.assigned_specialist.strip()
|
||||
lines.append(f"Специалист: {specialist or 'Не назначен'}")
|
||||
lines.append(
|
||||
f"Диагностика: {'Подписан' if record.diagnostic_report_signed else 'Не подписан'}"
|
||||
)
|
||||
lines.append(
|
||||
f"Ремонт: {'Подписан' if record.repair_report_signed else 'Не подписан'}"
|
||||
)
|
||||
lines.append(
|
||||
f"Приёмка: {'Подписан' if record.acceptance_report_signed else 'Не подписан'}"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if is_refused and record.refusal_reason:
|
||||
lines.append("─── Причина отказа ───")
|
||||
lines.append("")
|
||||
lines.append(record.refusal_reason)
|
||||
lines.append("")
|
||||
|
||||
if documents:
|
||||
for doc in documents:
|
||||
_append_document_block(lines, doc)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
_BASE_PAYLOAD_KEYS = frozenset({
|
||||
"task_id", "institution", "room", "device", "location", "specialist",
|
||||
})
|
||||
|
||||
_PAYLOAD_KEY_LABELS: dict[str, str] = {
|
||||
"initial_cause": "Первичное заключение",
|
||||
"actual_cause": "Вторичное заключение",
|
||||
"work_done": "Выполненные работы",
|
||||
"used_parts": "Использованные запчасти",
|
||||
"recommendations": "Рекомендации",
|
||||
"work_description": "Описание работ",
|
||||
"executor_signature": "Исполнитель",
|
||||
"customer_signature": "Заказчик",
|
||||
}
|
||||
|
||||
|
||||
def _append_document_block(
|
||||
lines: list[str], doc: TicketDocumentSnapshot,
|
||||
) -> None:
|
||||
"""Добавить блок документа без дублирования данных заголовка записи."""
|
||||
doc_date = format_datetime(doc.created_at)
|
||||
lines.append(f"─── {doc.title} ({doc_date}) ───")
|
||||
lines.append("")
|
||||
if doc.payload:
|
||||
for key, value in doc.payload.items():
|
||||
if not value or key in _BASE_PAYLOAD_KEYS:
|
||||
continue
|
||||
label = _PAYLOAD_KEY_LABELS.get(key, key)
|
||||
lines.append(f"{label}: {value}")
|
||||
elif doc.summary:
|
||||
lines.append(doc.summary)
|
||||
lines.append("")
|
||||
153
Dispatch_V0.1.1/ui/pages/document_browser_page.py
Normal file
153
Dispatch_V0.1.1/ui/pages/document_browser_page.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/document_browser_page.py
|
||||
|
||||
"""Общая страница просмотра документов Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.components import Button, Label, TextInput
|
||||
from gui.containers import HContainer, SContainer, VContainer
|
||||
|
||||
from application import TaskApplicationService
|
||||
from domain import TicketDocumentSnapshot
|
||||
from ui.ticket_selection_list import TicketSelectionEntry, TicketSelectionList
|
||||
from .report_viewer import ReportViewer
|
||||
|
||||
|
||||
class DocumentBrowserPage(SContainer):
|
||||
"""Базовая страница списка документов и предпросмотра."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
application: TaskApplicationService,
|
||||
title: str,
|
||||
empty_message: str,
|
||||
document_type: str,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(width_percent=100, height_percent=100, parent=parent)
|
||||
self._application = application
|
||||
self._title = title
|
||||
self._empty_message = empty_message
|
||||
self._document_type = document_type
|
||||
self._documents: dict[str, TicketDocumentSnapshot] = {}
|
||||
self._list_widget: TicketSelectionList | None = None
|
||||
self._preview: TextInput | None = None
|
||||
self._open_button: Button | None = None
|
||||
self._setup_ui()
|
||||
self._connect_signals()
|
||||
self._reload_documents()
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Root-контейнер страницы документов: заголовок плюс двухпанельная область.
|
||||
main_container = VContainer(margin=12, spacing=10, parent=self)
|
||||
title_label = Label(self._title, alignment="left", style="TICKET_LIST_HEADER")
|
||||
# Content-row документов: раскладывает список документов и их предпросмотр.
|
||||
content_row = HContainer(spacing=10, parent=main_container)
|
||||
|
||||
# Левая панель списка документов
|
||||
list_panel = SContainer(style="TICKET_LIST_CONTAINER")
|
||||
# List-layout: внутренний контейнер заголовка и списка документов.
|
||||
list_layout = VContainer(margin=12, spacing=8, parent=list_panel)
|
||||
list_title_label = Label("Список документов",
|
||||
margin=[0, 0, 0, 8],
|
||||
alignment="left",
|
||||
style="TICKET_LIST_TITLE")
|
||||
|
||||
self._list_widget = TicketSelectionList()
|
||||
list_layout.add_widget(list_title_label)
|
||||
list_layout.add_widget(self._list_widget)
|
||||
|
||||
# Правая панель предпросмотра
|
||||
preview_panel = SContainer(style="TICKET_LIST_CONTAINER")
|
||||
# Preview-layout: внутренний контейнер заголовка, preview-поля и кнопки открытия.
|
||||
preview_layout = VContainer(margin=12, spacing=8, parent=preview_panel)
|
||||
preview_title_label = Label("Предпросмотр", alignment="left", style="TICKET_LIST_TITLE")
|
||||
self._preview = TextInput(style="TICKET_PREVIEW_AREA", multiline=True)
|
||||
self._preview.set_read_only(True)
|
||||
self._open_button = Button("Открыть документ", style="FILTER_BUTTON", content_fit=True)
|
||||
self._open_button.set_enabled(False)
|
||||
preview_layout.add_widget(preview_title_label)
|
||||
preview_layout.add_widget(self._preview)
|
||||
preview_layout.add_widget(self._open_button)
|
||||
|
||||
content_row.add_widget_with_stretch(list_panel, 4)
|
||||
content_row.add_widget_with_stretch(preview_panel, 7)
|
||||
main_container.add_widget(title_label)
|
||||
main_container.add_widget(content_row)
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
# Application-состояние
|
||||
self._application.task_updated.connect(self._reload_documents)
|
||||
self._application.state_loaded.connect(self._reload_documents)
|
||||
|
||||
# UI-события страницы
|
||||
if self._list_widget is not None:
|
||||
self._list_widget.selection_changed.connect(self._update_preview)
|
||||
self._list_widget.item_activated.connect(self._open_current_document)
|
||||
if self._open_button is not None:
|
||||
self._open_button.clicked.connect(self._open_current_document)
|
||||
|
||||
def _reload_documents(self, *_args) -> None:
|
||||
if self._list_widget is None or self._preview is None:
|
||||
return
|
||||
current_document_id = self._current_document_id()
|
||||
self._documents = {
|
||||
document.document_id: document
|
||||
for document in self._application.list_documents(self._document_type)
|
||||
}
|
||||
entries = [
|
||||
TicketSelectionEntry(
|
||||
entry_id=document.document_id,
|
||||
title=document.title,
|
||||
subtitle=document.created_at.strftime("%d.%m.%Y %H:%M"),
|
||||
)
|
||||
for document in self._documents.values()
|
||||
]
|
||||
self._list_widget.set_entries(entries)
|
||||
if not self._documents:
|
||||
self._preview.set_text(self._empty_message)
|
||||
if self._open_button is not None:
|
||||
self._open_button.set_enabled(False)
|
||||
return
|
||||
self._restore_selection(current_document_id)
|
||||
self._update_preview()
|
||||
|
||||
def _restore_selection(self, document_id: str | None) -> None:
|
||||
if self._list_widget is None:
|
||||
return
|
||||
self._list_widget.set_current_entry(document_id)
|
||||
|
||||
def _update_preview(self, *_args) -> None:
|
||||
if self._preview is None:
|
||||
return
|
||||
document = self._current_document()
|
||||
if document is None:
|
||||
self._preview.set_text(self._empty_message)
|
||||
if self._open_button is not None:
|
||||
self._open_button.set_enabled(False)
|
||||
return
|
||||
self._preview.set_text(document.content or document.summary or document.title)
|
||||
if self._open_button is not None:
|
||||
self._open_button.set_enabled(True)
|
||||
|
||||
def _current_document(self) -> TicketDocumentSnapshot | None:
|
||||
document_id = self._current_document_id()
|
||||
if document_id is None:
|
||||
return None
|
||||
return self._documents.get(document_id)
|
||||
|
||||
def _current_document_id(self) -> str | None:
|
||||
if self._list_widget is None:
|
||||
return None
|
||||
entry_id = self._list_widget.current_entry_id()
|
||||
return str(entry_id) if entry_id is not None else None
|
||||
|
||||
def _open_current_document(self, *_args) -> None:
|
||||
document = self._current_document()
|
||||
if document is not None:
|
||||
ReportViewer(document, parent=self).exec()
|
||||
|
||||
def showEvent(self, event) -> None:
|
||||
super().showEvent(event)
|
||||
self._reload_documents()
|
||||
69
Dispatch_V0.1.1/ui/pages/report_viewer.py
Normal file
69
Dispatch_V0.1.1/ui/pages/report_viewer.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/report_viewer.py
|
||||
|
||||
"""Просмотрщик документа Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.components import Button, Dialog, Label, TextInput
|
||||
from gui.containers import VContainer
|
||||
|
||||
from domain import TicketDocumentSnapshot
|
||||
|
||||
|
||||
class ReportViewer(Dialog):
|
||||
"""Простой viewer сохранённого документа Ticket."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document: TicketDocumentSnapshot,
|
||||
parent=None,
|
||||
):
|
||||
self._document = document
|
||||
self._close_button: Button | None = None
|
||||
super().__init__(
|
||||
title=document.title,
|
||||
width=720,
|
||||
height=760,
|
||||
modal=True,
|
||||
parent=parent,
|
||||
)
|
||||
self._setup_ui()
|
||||
self._connect_signals()
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Root-контейнер viewer-окна: заголовок, метаданные, текст документа и кнопка закрытия.
|
||||
main_container = VContainer(margin=20,
|
||||
spacing=12)
|
||||
self.add_widget(main_container)
|
||||
|
||||
title_label = Label(
|
||||
self._document.title,
|
||||
alignment="left",
|
||||
style="TICKET_LIST_HEADER",
|
||||
)
|
||||
metadata_label = Label(
|
||||
(
|
||||
f"Задача #{self._document.payload.get('task_id', self._document.task_id)}\n"
|
||||
f"{self._document.created_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
f"{self._document.location or 'Локация не указана'}"
|
||||
),
|
||||
alignment="left",
|
||||
style="TICKET_LIST_SUBTITLE",
|
||||
)
|
||||
viewer = TextInput(
|
||||
text=self._document.content or self._document.summary,
|
||||
style="TICKET_PREVIEW_AREA",
|
||||
multiline=True,
|
||||
)
|
||||
viewer.set_read_only(True)
|
||||
self._close_button = Button("Закрыть", style="FILTER_BUTTON", content_fit=True)
|
||||
|
||||
main_container.add_widget(title_label)
|
||||
main_container.add_widget(metadata_label)
|
||||
main_container.add_widget(viewer)
|
||||
main_container.add_widget(self._close_button)
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
if self._close_button is not None:
|
||||
self._close_button.clicked.connect(self.accept)
|
||||
340
Dispatch_V0.1.1/ui/pages/reports_page.py
Normal file
340
Dispatch_V0.1.1/ui/pages/reports_page.py
Normal file
@@ -0,0 +1,340 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/pages/reports_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
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Карточка отчёта (по образцу TaskCardView)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class _ReportCardView(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 ""
|
||||
# Убрать название аппарата в скобках (дублирует 3-ю строку)
|
||||
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 ReportsPage(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, _ReportCardView] = {}
|
||||
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 (как в _TicketBoardColumn, но без счётчика)
|
||||
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
|
||||
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
|
||||
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 — preview
|
||||
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("report")
|
||||
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 = _ReportCardView(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