Add Dispatch_V0.1.1
This commit is contained in:
321
Dispatch_V0.1.1/application/task_application_service.py
Normal file
321
Dispatch_V0.1.1/application/task_application_service.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/task_application_service.py
|
||||
|
||||
"""Единый application-фасад Ticket поверх state, domain и hardware gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from error_logger import log_exception
|
||||
|
||||
from domain import (
|
||||
ArchiveRecordSnapshot,
|
||||
TicketConnectionStatus,
|
||||
TicketDocumentSnapshot,
|
||||
TicketHardwareStatus,
|
||||
TicketStateService,
|
||||
TicketTaskSnapshot,
|
||||
)
|
||||
from domain.task import TicketTask
|
||||
from domain.ticket_constants import STATE_COMPLETED, STATE_REFUSED
|
||||
from services import ServiceManager, TicketHardwareGateway
|
||||
from state import TicketRuntimeState
|
||||
from .archive_service import ArchiveService
|
||||
from .document_flow_service import DocumentFlowService
|
||||
from .report_signing_service import ReportSigningService
|
||||
from .specialist_assignment_service import SpecialistAssignmentService
|
||||
|
||||
|
||||
class TaskApplicationService(QObject):
|
||||
"""Канонический application facade Ticket."""
|
||||
|
||||
task_updated = Signal(object)
|
||||
task_removed = Signal(int)
|
||||
connection_changed = Signal(str, str)
|
||||
active_view_changed = Signal(str)
|
||||
state_loaded = Signal()
|
||||
com_connection_changed = Signal(bool, str)
|
||||
button_initialization_changed = Signal(bool, int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: TicketRuntimeState | None = None,
|
||||
hardware_gateway: TicketHardwareGateway | None = None,
|
||||
state_service: TicketStateService | None = None,
|
||||
specialist_service: SpecialistAssignmentService | None = None,
|
||||
report_service: ReportSigningService | None = None,
|
||||
archive_service: ArchiveService | None = None,
|
||||
document_service: DocumentFlowService | None = None,
|
||||
parent: QObject | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._state = state or TicketRuntimeState()
|
||||
self._hardware_gateway = hardware_gateway or ServiceManager(parent=self)
|
||||
self._state_service = state_service or TicketStateService()
|
||||
self._specialist_service = specialist_service or SpecialistAssignmentService(self._state)
|
||||
self._report_service = report_service or ReportSigningService(self._state)
|
||||
self._document_service = document_service or DocumentFlowService(self._state)
|
||||
self._archive_service = archive_service or ArchiveService(
|
||||
self._state,
|
||||
self._state_service,
|
||||
document_service=self._document_service,
|
||||
)
|
||||
self._started = False
|
||||
self._connect_state_signals()
|
||||
|
||||
def start(self) -> None:
|
||||
"""Загрузить state, синхронизировать gateway и запустить сервисы."""
|
||||
if self._started:
|
||||
return
|
||||
self._started = True
|
||||
self._state.load()
|
||||
self._hardware_gateway.reset_button_states()
|
||||
for task in self._state.list_tasks():
|
||||
self._hardware_gateway.set_button_state(task.task_id, task.state_code)
|
||||
self._hardware_gateway.set_observer(self)
|
||||
self._hardware_gateway.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Остановить gateway и снять observer."""
|
||||
if not self._started:
|
||||
return
|
||||
self._started = False
|
||||
self._hardware_gateway.set_observer(None)
|
||||
self._hardware_gateway.stop()
|
||||
|
||||
def list_tasks(self) -> list[TicketTaskSnapshot]:
|
||||
return list(self._state.list_tasks())
|
||||
|
||||
def list_active_tasks(self) -> list[TicketTaskSnapshot]:
|
||||
return list(self._state.list_active_tasks())
|
||||
|
||||
def list_archived_tasks(self) -> list[TicketTaskSnapshot]:
|
||||
return list(self._state.list_archived_tasks())
|
||||
|
||||
def list_archive_records(self) -> list[ArchiveRecordSnapshot]:
|
||||
return self._archive_service.list_archive_records()
|
||||
|
||||
def get_task(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
return self._state.get_task(task_id)
|
||||
|
||||
def handle_task_action(self, raw_action: Mapping[str, Any]) -> TicketTaskSnapshot | None:
|
||||
"""Обработать аппаратное действие через доменную state-machine."""
|
||||
button_id = self._normalize_int(raw_action.get("button_id"))
|
||||
hardware_state = self._normalize_int(raw_action.get("hardware_state"))
|
||||
if button_id is None or hardware_state is None:
|
||||
return None
|
||||
|
||||
current_snapshot = self._state.get_task(button_id)
|
||||
current_task = (
|
||||
TicketTask.from_snapshot(current_snapshot)
|
||||
if current_snapshot is not None
|
||||
else None
|
||||
)
|
||||
result = self._state_service.handle_hardware_signal(
|
||||
button_id,
|
||||
hardware_state,
|
||||
current_task,
|
||||
)
|
||||
if result.task is not None:
|
||||
self._assign_sequence_on_terminal_state(result.task)
|
||||
updated_snapshot = result.task.to_snapshot()
|
||||
self._state.upsert_task(updated_snapshot)
|
||||
self._hardware_gateway.set_button_state(
|
||||
updated_snapshot.task_id,
|
||||
updated_snapshot.state_code,
|
||||
)
|
||||
self._auto_save_archive_record(updated_snapshot)
|
||||
return updated_snapshot
|
||||
if current_snapshot is not None:
|
||||
self._hardware_gateway.set_button_state(
|
||||
current_snapshot.task_id,
|
||||
current_snapshot.state_code,
|
||||
)
|
||||
return current_snapshot
|
||||
|
||||
def set_active_view(self, view_name: str) -> None:
|
||||
self._state.active_view = view_name
|
||||
|
||||
def submit_new_task(self, snapshot: TicketTaskSnapshot) -> TicketTaskSnapshot | None:
|
||||
"""Зарегистрировать заявку, созданную через форму Dispatch.
|
||||
|
||||
Назначение: добавляет в runtime-state готовый снимок задачи в
|
||||
состоянии «Новая заявка» (`STATE_TODO`). Вызывает `upsert_task`,
|
||||
который эмитит `task_updated` — доска подхватывает карточку без
|
||||
дополнительной шины событий. Аппаратный шлюз в Dispatch
|
||||
отключён (`NullHardwareGateway`), поэтому синхронизация
|
||||
состояния кнопок не выполняется.
|
||||
"""
|
||||
if snapshot is None:
|
||||
return None
|
||||
self._state.upsert_task(snapshot)
|
||||
return self._state.get_task(snapshot.task_id)
|
||||
|
||||
def allocate_new_task_id(self) -> int:
|
||||
"""Выдать свободный идентификатор для заявки, созданной диспетчером."""
|
||||
existing_ids = [task.task_id for task in self._state.list_tasks()]
|
||||
base = max(existing_ids, default=0)
|
||||
# Резервируем диапазон 1..99 за аппаратными button_id, чтобы при
|
||||
# будущей реальной интеграции с COM-каналом идентификаторы заявок
|
||||
# из формы не пересекались с физическими кнопками.
|
||||
return max(base, 99) + 1
|
||||
|
||||
def assign_specialist(
|
||||
self,
|
||||
task_id: int,
|
||||
specialist_name: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
snapshot = self._specialist_service.assign_specialist(task_id, specialist_name)
|
||||
return self._sync_gateway_state(snapshot)
|
||||
|
||||
def list_specialists(self) -> list[str]:
|
||||
return self._specialist_service.list_specialists()
|
||||
|
||||
def sign_report(
|
||||
self,
|
||||
task_id: int,
|
||||
report_type: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
snapshot = self._report_service.sign_report(task_id, report_type)
|
||||
return self._sync_gateway_state(snapshot)
|
||||
|
||||
def sign_acceptance_report(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
snapshot = self._report_service.sign_acceptance_report(task_id)
|
||||
return self._sync_gateway_state(snapshot)
|
||||
|
||||
def archive_task(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
snapshot = self._archive_service.archive_task(task_id)
|
||||
return self._sync_gateway_state(snapshot)
|
||||
|
||||
def refuse_task(
|
||||
self,
|
||||
task_id: int,
|
||||
refusal_reason: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
normalized_reason = str(refusal_reason or "").strip()
|
||||
if not normalized_reason:
|
||||
return None
|
||||
current_snapshot = self._state.get_task(task_id)
|
||||
current_task = (
|
||||
TicketTask.from_snapshot(current_snapshot)
|
||||
if current_snapshot is not None
|
||||
else None
|
||||
)
|
||||
result = self._state_service.mark_task_as_refused(
|
||||
task_id,
|
||||
current_task,
|
||||
normalized_reason,
|
||||
)
|
||||
if result.task is None:
|
||||
return None
|
||||
self._assign_sequence_on_terminal_state(result.task)
|
||||
snapshot = result.task.to_snapshot()
|
||||
self._state.upsert_task(snapshot)
|
||||
self._auto_save_archive_record(snapshot)
|
||||
return self._sync_gateway_state(snapshot)
|
||||
|
||||
def create_diagnostic_report(
|
||||
self, task_id: int, initial_cause: str, actual_cause: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
return self._document_service.create_diagnostic_report(
|
||||
task_id, initial_cause, actual_cause,
|
||||
)
|
||||
|
||||
def create_repair_report(
|
||||
self, task_id: int, work_done: str, used_parts: str, recommendations: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
return self._document_service.create_repair_report(
|
||||
task_id, work_done, used_parts, recommendations,
|
||||
)
|
||||
|
||||
def create_acceptance_report(
|
||||
self, task_id: int, work_description: str,
|
||||
executor_signature: str, customer_signature: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
return self._document_service.create_acceptance_report(
|
||||
task_id, work_description, executor_signature, customer_signature,
|
||||
)
|
||||
|
||||
def list_documents(
|
||||
self,
|
||||
document_type: str | None = None,
|
||||
) -> list[TicketDocumentSnapshot]:
|
||||
return self._document_service.list_documents(document_type)
|
||||
|
||||
def can_advance_to_confirmation(self, task_id: int) -> bool:
|
||||
return self._report_service.can_advance_to_confirmation(task_id)
|
||||
|
||||
def can_advance_to_completed(self, task_id: int) -> bool:
|
||||
return self._report_service.can_advance_to_completed(task_id)
|
||||
|
||||
def get_active_view(self) -> str:
|
||||
return self._state.active_view
|
||||
|
||||
def get_gateway_status(self) -> TicketHardwareStatus:
|
||||
return self._hardware_gateway.get_status()
|
||||
|
||||
def on_task_action(self, raw_action: Mapping[str, Any]) -> None:
|
||||
"""Observer callback от hardware gateway."""
|
||||
try:
|
||||
self.handle_task_action(raw_action)
|
||||
except Exception as exc:
|
||||
log_exception(__name__, "TaskApplicationService.on_task_action", exc)
|
||||
|
||||
def on_gateway_status(self, status: TicketHardwareStatus) -> None:
|
||||
"""Observer callback обновления статуса hardware gateway."""
|
||||
if status.connection_status == TicketConnectionStatus.ERROR:
|
||||
self._state.set_error(status.message)
|
||||
else:
|
||||
self._state.connection_status = status.connection_status
|
||||
self._state.set_com_connection(
|
||||
status.connection_status == TicketConnectionStatus.CONNECTED,
|
||||
status.message,
|
||||
)
|
||||
self._state.set_button_initialization(
|
||||
status.buttons_initialized,
|
||||
status.button_count,
|
||||
)
|
||||
|
||||
def on_gateway_error(self, message: str) -> None:
|
||||
"""Observer callback ошибки hardware gateway."""
|
||||
self._state.set_error(message)
|
||||
|
||||
def _connect_state_signals(self) -> None:
|
||||
self._state.task_updated.connect(self.task_updated.emit)
|
||||
self._state.task_removed.connect(self.task_removed.emit)
|
||||
self._state.connection_changed.connect(self.connection_changed.emit)
|
||||
self._state.active_view_changed.connect(self.active_view_changed.emit)
|
||||
self._state.state_loaded.connect(self.state_loaded.emit)
|
||||
self._state.com_connection_changed.connect(self.com_connection_changed.emit)
|
||||
self._state.button_initialization_changed.connect(
|
||||
self.button_initialization_changed.emit
|
||||
)
|
||||
|
||||
def _sync_gateway_state(
|
||||
self,
|
||||
snapshot: TicketTaskSnapshot | None,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
if snapshot is None:
|
||||
return None
|
||||
self._hardware_gateway.set_button_state(snapshot.task_id, snapshot.state_code)
|
||||
return snapshot
|
||||
|
||||
def _assign_sequence_on_terminal_state(self, task: TicketTask) -> None:
|
||||
"""Присвоить сквозной номер при переходе в Выполненные/Отказ."""
|
||||
if task.state_code in {STATE_COMPLETED, STATE_REFUSED}:
|
||||
task.sequence_number = self._state.next_sequence_number()
|
||||
|
||||
def _auto_save_archive_record(self, snapshot: TicketTaskSnapshot) -> None:
|
||||
if snapshot.state_code in {STATE_COMPLETED, STATE_REFUSED}:
|
||||
self._archive_service.ensure_archive_record(snapshot)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_int(value: Any) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
Reference in New Issue
Block a user