Source code for cadrumo.entrypoints.tui.ledger.routes

"""Closed internal Ledger route catalogue and injected root factory."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Final, override

from textual.app import ComposeResult
from textual.widgets import DataTable, Static

from ....application.ledger.attachment_review import AttachmentReviewItem
from ....application.ledger.workspace import LedgerWorkspaceArea, LedgerWorkspaceProjectionV1
from ....application.operator_actions.models import ActionReference
from ..navigation import TuiScreenContextV1, TuiScreenFactoryV1
from .classification import LedgerClassificationScreen
from .controller import LedgerWorkspaceController, LedgerWorkspaceScreen, ledger_copy
from .entries import LedgerEntriesScreen
from .evidence import LedgerEvidenceScreen
from .import_flow import LedgerImportScreen
from .models import (
    LEDGER_DESTINATION_BY_AREA,
    LedgerClassificationSubmitterV1,
    LedgerDestinationIdV1,
    LedgerEvidenceDoorV1,
    LedgerExclusionSubmitterV1,
    LedgerImportDoorV1,
    LedgerInvoiceAddDoorV1,
    LedgerLinkSubmitterV1,
    LedgerRouteRefusalV1,
    LedgerRouteTargetV1,
    declared_ledger_destination_ids,
)
from .overview import LedgerOverviewScreen
from .reconciliation import LedgerReconciliationScreen
from .review import LedgerReviewScreen
from .workspace_injection import LedgerWorkspaceInjection, LedgerWorkspaceRefreshDoorV1
from .workspace_presentation import ledger_workspace_page

type LedgerInternalScreenFactoryV1 = Callable[[LedgerWorkspaceController], LedgerWorkspaceScreen]


[docs] class LedgerUnavailableScreen(LedgerWorkspaceScreen): """Typed placeholder for an area unavailable in application state.""" def __init__(self, controller: LedgerWorkspaceController, refusal: LedgerRouteRefusalV1) -> None: """Retain the typed refusal without resolving its protected reason code.""" super().__init__(controller, id="ledger-unavailable-screen") self.refusal = refusal self._route_refusal = refusal
[docs] @override def compose(self) -> ComposeResult: """Render the explicit unavailability and no action affordance.""" yield Static( ledger_copy("tui.ledger.unavailable.title"), classes="cadrumo-banner", ) with ledger_workspace_page() as navigation: yield navigation yield Static( ledger_copy(self._route_refusal.reason_key), id="ledger-refusal", classes="ledger-refusal", markup=False, )
[docs] def on_mount(self) -> None: """Keep the entire workspace vocabulary reachable beside the refusal.""" self.populate_navigation() self.query_one("#ledger-navigation", DataTable).focus()
[docs] def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: """Allow movement away from a placeholder while preserving refusals.""" self.handle_navigation_selection(event)
[docs] @dataclass(frozen=True, slots=True) class LedgerRouteV1: """One internal destination and its read body.""" destination: LedgerDestinationIdV1 area: LedgerWorkspaceArea factory: LedgerInternalScreenFactoryV1
#: Which read body each area opens. Only the screens are declared here; the #: destination each area resolves to comes from the canonical pairing, so this #: catalogue cannot disagree with the controller about where an area leads. _SCREEN_BY_AREA: Final[dict[LedgerWorkspaceArea, LedgerInternalScreenFactoryV1]] = { LedgerWorkspaceArea.OVERVIEW: LedgerOverviewScreen, LedgerWorkspaceArea.ENTRIES: LedgerEntriesScreen, LedgerWorkspaceArea.REVIEW: LedgerReviewScreen, LedgerWorkspaceArea.IMPORT: LedgerImportScreen, LedgerWorkspaceArea.CLASSIFICATION: LedgerClassificationScreen, LedgerWorkspaceArea.EVIDENCE: LedgerEvidenceScreen, LedgerWorkspaceArea.RECONCILIATION: LedgerReconciliationScreen, } LEDGER_ROUTES: Final[tuple[LedgerRouteV1, ...]] = tuple( LedgerRouteV1(destination, area, _SCREEN_BY_AREA[area]) for area, destination in LEDGER_DESTINATION_BY_AREA.items() ) _ROUTES_BY_ID: Final = {route.destination: route for route in LEDGER_ROUTES} def _require_total_routes() -> None: """Refuse at import unless every area has a body and a distinct destination. The destination half is settled by the pairing this builds from, so what is left to check here is the screen half: an area absent from ``_SCREEN_BY_AREA`` yields a route with no factory, which reaches the operator as a raise rather than a typed refusal. """ if frozenset(_ROUTES_BY_ID) != declared_ledger_destination_ids() or len(_ROUTES_BY_ID) != len(LEDGER_ROUTES): raise ValueError("Ledger routes must cover the internal destination catalogue exactly once") if tuple(route.area for route in LEDGER_ROUTES) != tuple(LedgerWorkspaceArea): raise ValueError("Ledger routes must preserve canonical workspace area order") if frozenset(_SCREEN_BY_AREA) != frozenset(LedgerWorkspaceArea): raise ValueError("Ledger routes must name a read body for every workspace area") _require_total_routes()
[docs] def resolve_ledger_screen( controller: LedgerWorkspaceController, target: LedgerRouteTargetV1, ) -> LedgerWorkspaceScreen: """Resolve one internal route without reading state or invoking an action.""" route = _ROUTES_BY_ID[target.destination] if route.area is not target.area: raise ValueError("Ledger target destination and area disagree") refusal = controller.refusal_for(route.area) if refusal is not None: return LedgerUnavailableScreen(controller, refusal) return route.factory(controller)
[docs] def ledger_screen_factory( projection: LedgerWorkspaceProjectionV1, *, review_action: ActionReference, classify_action: ActionReference | None = None, classification_submitter: LedgerClassificationSubmitterV1 | None = None, import_door: LedgerImportDoorV1 | None = None, evidence_action: ActionReference | None = None, evidence_items: tuple[AttachmentReviewItem, ...] | None = None, link_action: ActionReference | None = None, link_submitter: LedgerLinkSubmitterV1 | None = None, invoice_add_door: LedgerInvoiceAddDoorV1 | None = None, exclusion_submitter: LedgerExclusionSubmitterV1 | None = None, evidence_door: LedgerEvidenceDoorV1 | None = None, refresh: LedgerWorkspaceRefreshDoorV1 | None = None, ) -> TuiScreenFactoryV1: """Bind an injected immutable projection to the outer navigation factory contract.""" injection = LedgerWorkspaceInjection( review_action=review_action, classify_action=classify_action, classification_submitter=classification_submitter, import_door=import_door, evidence_action=evidence_action, evidence_items=evidence_items, link_action=link_action, link_submitter=link_submitter, invoice_add_door=invoice_add_door, exclusion_submitter=exclusion_submitter, evidence_door=evidence_door, refresh=refresh, ) # Which area performs each injected action. Classification needs an entry # chosen first, so a request to classify opens the entries to choose from. area_by_action = { str(action.action_id): area for action, area in ( (review_action, LedgerWorkspaceArea.REVIEW), (classify_action, LedgerWorkspaceArea.ENTRIES), (evidence_action, LedgerWorkspaceArea.EVIDENCE), (link_action, LedgerWorkspaceArea.RECONCILIATION), ) if action is not None } def create(context: TuiScreenContextV1) -> LedgerWorkspaceScreen: controller = LedgerWorkspaceController(context, projection, injection) area = area_by_action.get(context.action_candidate_id or "", LedgerWorkspaceArea.OVERVIEW) if controller.refusal_for(area) is not None: area = LedgerWorkspaceArea.OVERVIEW return resolve_ledger_screen(controller, controller.route_target(area)) return create
__all__ = [ "LEDGER_ROUTES", "LedgerInternalScreenFactoryV1", "LedgerRouteV1", "LedgerUnavailableScreen", "ledger_screen_factory", "resolve_ledger_screen", ]