|
17 | 17 | - Manage keyboard bindings (that's UI layer) |
18 | 18 | """ |
19 | 19 |
|
20 | | -from typing import Optional, List |
21 | | -from datetime import datetime, date as date_type |
| 20 | +from datetime import date as date_type |
| 21 | +from datetime import datetime |
| 22 | +from typing import List, Optional |
| 23 | + |
22 | 24 | import polars as pl |
23 | 25 |
|
24 | | -from .view_interface import IViewPresenter |
25 | | -from .state import AppState, ViewMode, SortMode, SortDirection, TransactionEdit, TimeFrame |
| 26 | +from .commit_orchestrator import CommitOrchestrator |
26 | 27 | from .data_manager import DataManager |
27 | 28 | from .formatters import ViewPresenter |
28 | | -from .commit_orchestrator import CommitOrchestrator |
29 | | -from .time_navigator import TimeNavigator |
30 | 29 | from .logging_config import get_logger |
| 30 | +from .state import AppState, SortDirection, SortMode, TimeFrame, TransactionEdit, ViewMode |
| 31 | +from .time_navigator import TimeNavigator |
| 32 | +from .view_interface import IViewPresenter |
31 | 33 |
|
32 | 34 | logger = get_logger(__name__) |
33 | 35 |
|
@@ -138,7 +140,9 @@ def refresh_view(self, force_rebuild: bool = True) -> None: |
138 | 140 | elif sort_col in ["merchant", "category", "group", "account"]: |
139 | 141 | sort_col = field_name |
140 | 142 |
|
141 | | - descending = ViewPresenter.should_sort_descending(sort_col, self.state.sort_direction) |
| 143 | + descending = ViewPresenter.should_sort_descending( |
| 144 | + sort_col, self.state.sort_direction |
| 145 | + ) |
142 | 146 | if not agg.is_empty(): |
143 | 147 | agg = agg.sort(sort_col, descending=descending) |
144 | 148 |
|
@@ -193,7 +197,7 @@ def refresh_view(self, force_rebuild: bool = True) -> None: |
193 | 197 | filtered_df = self.state.get_filtered_df() |
194 | 198 | if filtered_df is not None and not filtered_df.is_empty(): |
195 | 199 | # Exclude hidden from totals |
196 | | - non_hidden_df = filtered_df.filter(filtered_df["hideFromReports"] == False) |
| 200 | + non_hidden_df = filtered_df.filter(~filtered_df["hideFromReports"]) |
197 | 201 |
|
198 | 202 | income_df = non_hidden_df.filter(pl.col("group") == "Income") |
199 | 203 | total_income = float(income_df["amount"].sum()) if not income_df.is_empty() else 0.0 |
@@ -549,30 +553,32 @@ def clear_selection(self): |
549 | 553 | """Clear all selections.""" |
550 | 554 | self.state.clear_selection() |
551 | 555 |
|
552 | | - def drill_down(self, item_name: str, cursor_position: int): |
| 556 | + def drill_down(self, item_name: str, cursor_position: int, scroll_y: float = 0.0): |
553 | 557 | """ |
554 | 558 | Drill down into an item (merchant/category/group/account). |
555 | 559 |
|
556 | 560 | Args: |
557 | 561 | item_name: Name of item to drill into |
558 | 562 | cursor_position: Current cursor position to save for go_back |
| 563 | + scroll_y: Current scroll position to save for go_back |
559 | 564 | """ |
560 | | - self.state.drill_down(item_name, cursor_position) |
| 565 | + self.state.drill_down(item_name, cursor_position, scroll_y) |
561 | 566 | self.refresh_view() |
562 | 567 |
|
563 | | - def go_back(self) -> tuple[bool, int]: |
| 568 | + def go_back(self) -> tuple[bool, int, float]: |
564 | 569 | """ |
565 | 570 | Go back to previous view. |
566 | 571 |
|
567 | 572 | Returns: |
568 | | - Tuple of (success, cursor_position) |
| 573 | + Tuple of (success, cursor_position, scroll_y) |
569 | 574 | - success: True if went back, False if already at top |
570 | 575 | - cursor_position: Where to restore cursor |
| 576 | + - scroll_y: Where to restore scroll position |
571 | 577 | """ |
572 | | - success, cursor_position = self.state.go_back() |
| 578 | + success, cursor_position, scroll_y = self.state.go_back() |
573 | 579 | if success: |
574 | 580 | self.refresh_view() |
575 | | - return (success, cursor_position) |
| 581 | + return (success, cursor_position, scroll_y) |
576 | 582 |
|
577 | 583 | def get_next_sort_field( |
578 | 584 | self, view_mode: ViewMode, current_sort: SortMode |
@@ -641,10 +647,15 @@ def _get_action_hints(self) -> str: |
641 | 647 | return f"Enter=Drill | m=✏️ Merchant (bulk) | c=✏️ Category (bulk) | s=Sort({sort_name}) | g=Group" |
642 | 648 | else: # DETAIL |
643 | 649 | # Check if we're in a drilled-down view or ungrouped view |
644 | | - if self.state.selected_merchant or self.state.selected_category or self.state.selected_group or self.state.selected_account: |
645 | | - return f"Esc/g=Back | m=✏️ Merchant | c=✏️ Category | h=Hide | Space=Select" |
| 650 | + if ( |
| 651 | + self.state.selected_merchant |
| 652 | + or self.state.selected_category |
| 653 | + or self.state.selected_group |
| 654 | + or self.state.selected_account |
| 655 | + ): |
| 656 | + return "Esc/g=Back | m=✏️ Merchant | c=✏️ Category | h=Hide | Space=Select" |
646 | 657 | else: |
647 | | - return f"g=Group | m=✏️ Merchant | c=✏️ Category | h=Hide | Space=Select" |
| 658 | + return "g=Group | m=✏️ Merchant | c=✏️ Category | h=Hide | Space=Select" |
648 | 659 |
|
649 | 660 | def queue_category_edits(self, transactions_df, new_category_id: str) -> int: |
650 | 661 | """ |
@@ -773,7 +784,7 @@ def handle_commit_result( |
773 | 784 | # This prevents data corruption where UI shows changes that didn't save |
774 | 785 | # Note: View already restored in app.py before commit started |
775 | 786 | # Just refresh to ensure UI shows current (unchanged) state |
776 | | - logger.debug(f"Failure path - refreshing view (state already restored in app.py)") |
| 787 | + logger.debug("Failure path - refreshing view (state already restored in app.py)") |
777 | 788 | self.refresh_view(force_rebuild=False) |
778 | 789 | else: |
779 | 790 | logger.info("All commits succeeded - applying edits locally") |
|
0 commit comments